[llvm] [TableGen] Pool generated assembly matcher class sequences (PR #202621)
David Zbarsky via llvm-commits
llvm-commits at lists.llvm.org
Tue Jun 9 06:59:48 PDT 2026
https://github.com/dzbarsky created https://github.com/llvm/llvm-project/pull/202621
Generated assembly match entries currently embed a fixed-size operand-class array in every row. Large targets repeat the same padded class sequences thousands of times.
Emit each distinct padded sequence once in MatchClassTable and replace the inline array with a compact offset. Hoist the sequence base pointer outside the operand loop so each candidate computes it once.
In the stripped Darwin arm64 all-tools multicall binary this reduces size from 150,569,536 to 147,944,136 bytes, saving 2,625,400 bytes (1.74%). The pointer hoist is size-neutral.
Validation:
- Built the complete multicall binary with Bazel remote execution: https://app.buildbuddy.io/invocation/c5bb88dd-e94d-4fea-9af3-16ab9eb12e99
- Assembled x86-64, AArch64, RISC-V, ARM, and AMDGPU inputs with baseline and patched binaries; all object files were byte-identical.
- LLVM has no benchmark for target assembly instruction matching. SandboxIRBench exercises the LLVM IR parser and is not applicable.
- A matcher-dominated two-million-nop x86 assembly workload increased mean user time from 702.5 ms to 710.7 ms (+1.2%).
- An end-to-end -O0 compile of 3,000 generated C functions showed no regression in user time (188.0 ms baseline, 182.9 ms patched); wall time was too noisy to interpret.
Work towards #202616
>From 532112c782ef6add71b192d485417cf28809ec54 Mon Sep 17 00:00:00 2001
From: David Zbarsky <dzbarsky at gmail.com>
Date: Mon, 8 Jun 2026 13:26:52 -0400
Subject: [PATCH] [TableGen] Pool generated assembly matcher class sequences
Generated assembly match entries currently embed a fixed-size operand-class array in every row. Large targets repeat the same padded class sequences thousands of times.
Emit each distinct padded sequence once in MatchClassTable and replace the inline array with a compact offset. Hoist the sequence base pointer outside the operand loop so each candidate computes it once.
In the stripped Darwin arm64 all-tools multicall binary this reduces size from 150,569,536 to 147,944,136 bytes, saving 2,625,400 bytes (1.74%). The pointer hoist is size-neutral.
Validation:
- Built the complete multicall binary with Bazel remote execution: https://app.buildbuddy.io/invocation/c5bb88dd-e94d-4fea-9af3-16ab9eb12e99
- Assembled x86-64, AArch64, RISC-V, ARM, and AMDGPU inputs with baseline and patched binaries; all object files were byte-identical.
- LLVM has no benchmark for target assembly instruction matching. SandboxIRBench exercises the LLVM IR parser and is not applicable.
- A matcher-dominated two-million-nop x86 assembly workload increased mean user time from 702.5 ms to 710.7 ms (+1.2%).
- An end-to-end -O0 compile of 3,000 generated C functions showed no regression in user time (188.0 ms baseline, 182.9 ms patched); wall time was too noisy to interpret.
---
llvm/utils/TableGen/AsmMatcherEmitter.cpp | 75 +++++++++++++++++------
1 file changed, 57 insertions(+), 18 deletions(-)
diff --git a/llvm/utils/TableGen/AsmMatcherEmitter.cpp b/llvm/utils/TableGen/AsmMatcherEmitter.cpp
index 4c61d64ec215a..7689fccf34e08 100644
--- a/llvm/utils/TableGen/AsmMatcherEmitter.cpp
+++ b/llvm/utils/TableGen/AsmMatcherEmitter.cpp
@@ -116,6 +116,7 @@
#include "llvm/TableGen/StringMatcher.h"
#include "llvm/TableGen/StringToOffsetTable.h"
#include "llvm/TableGen/TableGenBackend.h"
+#include <algorithm>
#include <cassert>
#include <cctype>
#include <forward_list>
@@ -388,6 +389,23 @@ struct ClassInfo {
}
};
+using MatchClassSequence = std::vector<const ClassInfo *>;
+
+struct LessMatchClassSequence {
+ bool operator()(const MatchClassSequence &LHS,
+ const MatchClassSequence &RHS) const {
+ return std::lexicographical_compare(
+ LHS.begin(), LHS.end(), RHS.begin(), RHS.end(),
+ [](const ClassInfo *LHS, const ClassInfo *RHS) {
+ if (!LHS)
+ return RHS != nullptr;
+ if (!RHS)
+ return false;
+ return LHS->Name < RHS->Name;
+ });
+ }
+};
+
class AsmVariantInfo {
public:
StringRef RegisterPrefix;
@@ -3610,16 +3628,31 @@ void AsmMatcherEmitter::run(raw_ostream &OS) {
}
OS << "};\n\n";
- // Emit the static match table; unused classes get initialized to 0 which is
- // guaranteed to be InvalidMatchClass.
- //
- // FIXME: We can reduce the size of this table very easily. First, we change
- // it so that store the kinds in separate bit-fields for each index, which
- // only needs to be the max width used for classes at that index (we also need
- // to reject based on this during classification). If we then make sure to
- // order the match kinds appropriately (putting mnemonics last), then we
- // should only end up using a few bits for each class, especially the ones
- // following the mnemonic.
+ std::map<MatchClassSequence, unsigned, LessMatchClassSequence>
+ ClassSequenceOffsets;
+ for (const auto &MI : Info.Matchables) {
+ MatchClassSequence Sequence;
+ for (const MatchableInfo::AsmOperand &Op : MI->AsmOperands)
+ Sequence.push_back(Op.Class);
+ Sequence.resize(MaxNumOperands, nullptr);
+ ClassSequenceOffsets.try_emplace(std::move(Sequence), 0);
+ }
+
+ SmallVector<const ClassInfo *> MatchClasses;
+ for (auto &[Sequence, Offset] : ClassSequenceOffsets) {
+ Offset = MatchClasses.size();
+ append_range(MatchClasses, Sequence);
+ }
+
+ const char *MatchClassType = getMinimalTypeForRange(
+ std::distance(Info.Classes.begin(), Info.Classes.end()) +
+ 2 /* Include 'InvalidMatchClass' and 'OptionalMatchClass' */);
+ OS << "static const " << MatchClassType << " MatchClassTable[] = {\n";
+ for (const ClassInfo *Class : MatchClasses)
+ OS << " " << (Class ? Class->Name : "InvalidMatchClass") << ",\n";
+ OS << "};\n\n";
+
+ // Emit the static match table.
OS << "namespace {\n";
OS << " struct MatchEntry {\n";
OS << " " << getMinimalTypeForRange(MaxMnemonicIndex) << " Mnemonic;\n";
@@ -3627,11 +3660,8 @@ void AsmMatcherEmitter::run(raw_ostream &OS) {
OS << " " << getMinimalTypeForRange(NumConverters) << " ConvertFn;\n";
OS << " " << getMinimalTypeForRange(FeatureBitsets.size())
<< " RequiredFeaturesIdx;\n";
- OS << " "
- << getMinimalTypeForRange(
- std::distance(Info.Classes.begin(), Info.Classes.end()) +
- 2 /* Include 'InvalidMatchClass' and 'OptionalMatchClass' */)
- << " Classes[" << MaxNumOperands << "];\n";
+ OS << " " << getMinimalTypeForRange(MatchClasses.size())
+ << " ClassOffset;\n";
OS << " StringRef getMnemonic() const {\n";
OS << " return StringRef(MnemonicTable + Mnemonic + 1,\n";
OS << " MnemonicTable[Mnemonic]);\n";
@@ -3680,11 +3710,18 @@ void AsmMatcherEmitter::run(raw_ostream &OS) {
for (const auto &F : MI->RequiredFeatures)
OS << '_' << F->TheDef->getName();
- OS << ", { ";
+ MatchClassSequence Sequence;
+ for (const MatchableInfo::AsmOperand &Op : MI->AsmOperands)
+ Sequence.push_back(Op.Class);
+ Sequence.resize(MaxNumOperands, nullptr);
+ auto ClassSequence = ClassSequenceOffsets.find(Sequence);
+ assert(ClassSequence != ClassSequenceOffsets.end());
+
+ OS << ", " << ClassSequence->second << " /* { ";
ListSeparator LS;
for (const MatchableInfo::AsmOperand &Op : MI->AsmOperands)
OS << LS << Op.Class->Name;
- OS << " }, },\n";
+ OS << " } */, },\n";
}
OS << "};\n\n";
@@ -3819,11 +3856,13 @@ void AsmMatcherEmitter::run(raw_ostream &OS) {
if (HasOptionalOperands)
OS << " OptionalOperandsMask.reset(0, "
<< MaxNumOperands + HasMnemonicFirst << ");\n";
+ OS << " const auto *FormalClasses = "
+ "MatchClassTable + it->ClassOffset;\n";
OS << " for (unsigned FormalIdx = " << (HasMnemonicFirst ? "0" : "SIndex")
<< ", ActualIdx = " << (HasMnemonicFirst ? "1" : "SIndex")
<< "; FormalIdx != " << MaxNumOperands << "; ++FormalIdx) {\n";
OS << " auto Formal = "
- << "static_cast<MatchClassKind>(it->Classes[FormalIdx]);\n";
+ << "static_cast<MatchClassKind>(FormalClasses[FormalIdx]);\n";
OS << " DEBUG_WITH_TYPE(\"asm-matcher\",\n";
OS << " dbgs() << \" Matching formal operand class \" "
"<< getMatchClassName(Formal)\n";
More information about the llvm-commits
mailing list