[llvm] [TableGen] Split large printInstruction() for NVPTX to avoid compiler limits (PR #218530)
Malavika Samak via llvm-commits
llvm-commits at lists.llvm.org
Thu Sep 10 11:07:12 PDT 2026
https://github.com/malavikasamak updated https://github.com/llvm/llvm-project/pull/218530
>From 983d618bce085ad079da41ac680b87832be95a9e Mon Sep 17 00:00:00 2001
From: Malavika Samak <mmalavika at nvidia.com>
Date: Wed, 2 Sep 2026 13:54:11 -0700
Subject: [PATCH] [TableGen][NVPTX] Split large printInstruction() using static
helpers and function-pointer table
NVPTX's printInstruction() overflow switch contains ~12,000 cases, growing
the function to ~1.2 MB. This causes two build failures on non-x86 hosts:
- MSVC C1001/C1053 (function body too large for back-end)
- R_RISCV_JAL relocation truncated: the linker cannot encode a >1 MB
forward jump from the JAL dispatch at function entry
Fix: if the AsmWriter class sets PrintInstructionSplitThreshold (a new
target-agnostic field in llvm/include/llvm/Target/Target.td), the
overflow instructions are split into contiguous range-based groups and
each group is emitted as an out-of-line static helper function.
printInstruction() dispatches through a function-pointer table, so the
compiler cannot inline the helpers back at -O2.
Design points:
- Target.td: new `int PrintInstructionSplitThreshold = 0;` in AsmWriter.
Targets opt in by setting a non-zero value in their AsmWriter def.
- NVPTX.td: `let PrintInstructionSplitThreshold = 500;` in NVPTXAsmWriter.
- Range-based grouping: instructions are sorted by opcode number and
split into equal-sized buckets of at most Threshold entries each.
No target-specific knowledge (TSFlags, mnemonic prefixes) required.
- Function-pointer table: each group becomes a static void
printOpcodeGroup_N(NVPTXInstPrinter *P, ...) function. A
static void (*const NVPTXOpcodeGroupPrinters[])(NVPTXInstPrinter*, ...)
table holds the pointers. printInstruction() dispatches via an indirect
call, which the compiler cannot inline.
- AsmWriterOperand::getCode() gains an optional Receiver parameter
(default "") so static helper bodies emit "P->printOperand(...)" instead
of "printOperand(...)", avoiding undefined implicit-this in free functions.
- The NVPTX-specific IIFE wrapping of TableDrivenOperandPrinters fragments
is removed; fragments are emitted normally as for all other backends.
- No target name checks remain in AsmWriterEmitter.cpp.
---
llvm/include/llvm/Target/Target.td | 8 ++
llvm/lib/Target/NVPTX/NVPTX.td | 5 +
llvm/utils/TableGen/AsmWriterEmitter.cpp | 137 ++++++++++++++++++-
llvm/utils/TableGen/Common/AsmWriterInst.cpp | 5 +-
llvm/utils/TableGen/Common/AsmWriterInst.h | 5 +-
5 files changed, 150 insertions(+), 10 deletions(-)
diff --git a/llvm/include/llvm/Target/Target.td b/llvm/include/llvm/Target/Target.td
index c652af023c720..3629840f89115 100644
--- a/llvm/include/llvm/Target/Target.td
+++ b/llvm/include/llvm/Target/Target.td
@@ -2053,6 +2053,14 @@ class AsmWriter {
// will specify which alternative to use. For example "{x|y|z}" with Variant
// == 1, will expand to "y".
int Variant = 0;
+
+ // If non-zero, and the number of overflow-switch instructions exceeds this
+ // value, the overflow switch is split into groups of at most this many
+ // instructions. Each group is handled by an out-of-line static helper
+ // function called through a function-pointer table, keeping
+ // printInstruction() itself small to avoid MSVC ICE and RISC-V JAL
+ // relocation overflow on targets with very large instruction sets.
+ int PrintInstructionSplitThreshold = 0;
}
def DefaultAsmWriter : AsmWriter;
diff --git a/llvm/lib/Target/NVPTX/NVPTX.td b/llvm/lib/Target/NVPTX/NVPTX.td
index cb9aef2a1cd42..74d9b98c4ca4f 100644
--- a/llvm/lib/Target/NVPTX/NVPTX.td
+++ b/llvm/lib/Target/NVPTX/NVPTX.td
@@ -123,6 +123,11 @@ def NVPTXInstrInfo : InstrInfo {
def NVPTXAsmWriter : AsmWriter {
int PassSubtarget = 1;
+ // Split the overflow switch into groups of at most 500 instructions each,
+ // using out-of-line static helpers + a function-pointer table. This prevents
+ // MSVC ICE and RISC-V JAL relocation overflow caused by the large NVPTX
+ // instruction set.
+ let PrintInstructionSplitThreshold = 500;
}
def NVPTX : Target {
diff --git a/llvm/utils/TableGen/AsmWriterEmitter.cpp b/llvm/utils/TableGen/AsmWriterEmitter.cpp
index 86ee350834b5c..47eeddeafd147 100644
--- a/llvm/utils/TableGen/AsmWriterEmitter.cpp
+++ b/llvm/utils/TableGen/AsmWriterEmitter.cpp
@@ -81,13 +81,25 @@ class AsmWriterEmitter {
std::vector<std::vector<unsigned>> &InstIdxs,
std::vector<unsigned> &InstOpsUsed,
bool PassSubtarget) const;
+
+ // Groups of overflow-switch instructions split by opcode-number range.
+ // Used to split large printInstruction() overflow switches: each group is
+ // emitted as an out-of-line static helper function invoked through a
+ // function-pointer table, so the compiler cannot inline them back into
+ // printInstruction() at -O2.
+ using OpcodeGroupList = SmallVector<std::vector<AsmWriterInst>, 16>;
+ OpcodeGroupList EmitOpcodeGroupTable(raw_ostream &O, StringRef TargetName,
+ StringRef ClassName, bool PassSubtarget);
+ void EmitOpcodeGroupDispatch(raw_ostream &O, StringRef TargetName,
+ bool PassSubtarget);
};
} // end anonymous namespace
static void
PrintCases(std::vector<std::pair<std::string, AsmWriterOperand>> &OpsToPrint,
- raw_ostream &O, bool PassSubtarget) {
+ raw_ostream &O, bool PassSubtarget,
+ const std::string &Receiver = "") {
O << " case " << OpsToPrint.back().first << ":";
AsmWriterOperand TheOp = OpsToPrint.back().second;
OpsToPrint.pop_back();
@@ -101,14 +113,15 @@ PrintCases(std::vector<std::pair<std::string, AsmWriterOperand>> &OpsToPrint,
}
// Finally, emit the code.
- O << "\n " << TheOp.getCode(PassSubtarget);
+ O << "\n " << TheOp.getCode(PassSubtarget, Receiver);
O << "\n break;\n";
}
/// EmitInstructions - Emit the last instruction in the vector and any other
/// instructions that are suitably similar to it.
static void EmitInstructions(std::vector<AsmWriterInst> &Insts, raw_ostream &O,
- bool PassSubtarget) {
+ bool PassSubtarget,
+ const std::string &Receiver = "") {
AsmWriterInst FirstInst = Insts.back();
Insts.pop_back();
@@ -136,7 +149,7 @@ static void EmitInstructions(std::vector<AsmWriterInst> &Insts, raw_ostream &O,
for (unsigned i = 0, e = FirstInst.Operands.size(); i != e; ++i) {
if (i != DifferingOperand) {
// If the operand is the same for all instructions, just print it.
- O << " " << FirstInst.Operands[i].getCode(PassSubtarget);
+ O << " " << FirstInst.Operands[i].getCode(PassSubtarget, Receiver);
} else {
// If this is the operand that varies between all of the instructions,
// emit a switch for just this operand now.
@@ -154,7 +167,7 @@ static void EmitInstructions(std::vector<AsmWriterInst> &Insts, raw_ostream &O,
}
std::reverse(OpsToPrint.begin(), OpsToPrint.end());
while (!OpsToPrint.empty())
- PrintCases(OpsToPrint, O, PassSubtarget);
+ PrintCases(OpsToPrint, O, PassSubtarget, Receiver);
O << " }";
}
O << "\n";
@@ -490,6 +503,16 @@ void AsmWriterEmitter::EmitPrintInstruction(
StringRef ClassName = AsmWriter->getValueAsString("AsmWriterClassName");
bool PassSubtarget = AsmWriter->getValueAsInt("PassSubtarget");
+ // For targets with large instruction sets, split the overflow switch into
+ // out-of-line static helpers called through a function-pointer table.
+ // The table and helpers are emitted before printInstruction() opens.
+ OpcodeGroupList OpcodeGroups;
+ int SplitThreshold =
+ AsmWriter->getValueAsInt("PrintInstructionSplitThreshold");
+ if (SplitThreshold > 0)
+ OpcodeGroups =
+ EmitOpcodeGroupTable(O, Target.getName(), ClassName, PassSubtarget);
+
// This function has some huge switch statements that causing excessive
// compile time in LLVM profile instrumenation build. This print function
// usually is not frequently called in compilation. Here we disable the
@@ -566,8 +589,9 @@ void AsmWriterEmitter::EmitPrintInstruction(
// information for those instructions that are left. This is a less dense
// encoding, but we expect the main 64-bit table to handle the majority of
// instructions.
- if (!Instructions.empty()) {
- // Find the opcode # of inline asm.
+ if (!OpcodeGroups.empty()) {
+ EmitOpcodeGroupDispatch(O, Target.getName(), PassSubtarget);
+ } else if (!Instructions.empty()) {
O << " switch (MI->getOpcode()) {\n";
O << " default: llvm_unreachable(\"Unexpected opcode.\");\n";
while (!Instructions.empty())
@@ -579,6 +603,105 @@ void AsmWriterEmitter::EmitPrintInstruction(
O << "}\n";
}
+AsmWriterEmitter::OpcodeGroupList
+AsmWriterEmitter::EmitOpcodeGroupTable(raw_ostream &O, StringRef TargetName,
+ StringRef ClassName,
+ bool PassSubtarget) {
+ OpcodeGroupList Groups;
+
+ // Collect overflow instructions (same predicate as erase_if below).
+ std::vector<AsmWriterInst> OpcodeInsts;
+ for (const AsmWriterInst &AWI : Instructions)
+ if (!AWI.Operands.empty())
+ OpcodeInsts.push_back(AWI);
+
+ if (OpcodeInsts.empty())
+ return Groups;
+
+ // Sort by opcode number for reproducible range-based grouping.
+ llvm::sort(OpcodeInsts, [](const AsmWriterInst &A, const AsmWriterInst &B) {
+ return A.CGIIndex < B.CGIIndex;
+ });
+
+ // Split into contiguous buckets of at most Threshold instructions.
+ int Threshold =
+ Target.getAsmWriter()->getValueAsInt("PrintInstructionSplitThreshold");
+ assert(Threshold > 0 && "EmitOpcodeGroupTable called with zero threshold");
+ for (unsigned I = 0, E = OpcodeInsts.size(); I < E; I += Threshold) {
+ unsigned End = std::min<unsigned>(I + Threshold, E);
+ Groups.emplace_back(OpcodeInsts.begin() + I, OpcodeInsts.begin() + End);
+ }
+
+ // uint8_t caps at 254 non-zero groups; assert before silent truncation.
+ assert(Groups.size() < 255 &&
+ "Too many opcode groups for uint8_t dispatch table");
+
+ // Emit the opcode-to-group lookup table (0 = not in overflow set).
+ std::string TableName = (TargetName + "OpcodeGroup").str();
+ std::vector<uint8_t> Table(NumberedInstructions.size(), 0);
+ for (unsigned GIdx = 0; GIdx < Groups.size(); ++GIdx)
+ for (const AsmWriterInst &AWI : Groups[GIdx])
+ Table[AWI.CGIIndex] = GIdx + 1;
+
+ O << "static const uint8_t " << TableName << "[] = {\n";
+ for (unsigned i = 0; i < NumberedInstructions.size(); ++i)
+ O << " " << unsigned(Table[i]) << ",\t// "
+ << NumberedInstructions[i]->getName() << "\n";
+ O << "};\n\n";
+
+ // Emit one out-of-line static helper per group. Passing the printer object
+ // explicitly (rather than capturing it) prevents the compiler from merging
+ // these back into printInstruction() at -O2.
+ std::string FullClassName = (TargetName + ClassName).str();
+ std::string Receiver = "P->";
+ for (unsigned GIdx = 0; GIdx < Groups.size(); ++GIdx) {
+ O << "static void printOpcodeGroup_" << GIdx << "(\n"
+ << " " << FullClassName << " *P, const MCInst *MI,\n"
+ << " uint64_t Address, ";
+ if (PassSubtarget)
+ O << "const MCSubtargetInfo &STI, ";
+ O << "raw_ostream &O) {\n"
+ << " switch (MI->getOpcode()) {\n"
+ << " default: llvm_unreachable(\"Unexpected opcode.\");\n";
+ std::vector<AsmWriterInst> GroupInsts = Groups[GIdx];
+ std::reverse(GroupInsts.begin(), GroupInsts.end());
+ while (!GroupInsts.empty())
+ EmitInstructions(GroupInsts, O, PassSubtarget, Receiver);
+ O << " }\n}\n\n";
+ }
+
+ // Emit the function-pointer table. Indirect calls prevent inlining.
+ std::string PtrTableName = (TargetName + "OpcodeGroupPrinters").str();
+ O << "static void (*const " << PtrTableName << "[])(\n"
+ << " " << FullClassName << "*, const MCInst*, uint64_t,\n"
+ << " " << (PassSubtarget ? "const MCSubtargetInfo&, " : "")
+ << "raw_ostream&) = {\n";
+ for (unsigned GIdx = 0; GIdx < Groups.size(); ++GIdx) {
+ O << " printOpcodeGroup_" << GIdx;
+ if (GIdx + 1 < Groups.size())
+ O << ",";
+ O << "\n";
+ }
+ O << "};\n\n";
+
+ return Groups;
+}
+
+void AsmWriterEmitter::EmitOpcodeGroupDispatch(raw_ostream &O,
+ StringRef TargetName,
+ bool PassSubtarget) {
+ // Two-level dispatch: lookup table selects the group, then an indirect call
+ // through a function-pointer table invokes the out-of-line helper.
+ // Indirect calls cannot be inlined, so printInstruction() stays small.
+ std::string TableName = (TargetName + "OpcodeGroup").str();
+ std::string PtrTableName = (TargetName + "OpcodeGroupPrinters").str();
+ O << " if (uint8_t G = " << TableName << "[MI->getOpcode()])\n"
+ << " " << PtrTableName << "[G - 1](this, MI, Address, ";
+ if (PassSubtarget)
+ O << "STI, ";
+ O << "O);\n";
+}
+
static void
emitRegisterNameString(raw_ostream &O, StringRef AltName,
const std::deque<CodeGenRegister> &Registers) {
diff --git a/llvm/utils/TableGen/Common/AsmWriterInst.cpp b/llvm/utils/TableGen/Common/AsmWriterInst.cpp
index 97430a1eec8a1..acb210c791270 100644
--- a/llvm/utils/TableGen/Common/AsmWriterInst.cpp
+++ b/llvm/utils/TableGen/Common/AsmWriterInst.cpp
@@ -20,7 +20,8 @@ using namespace llvm;
static bool isIdentChar(char C) { return isAlnum(C) || C == '_'; }
-std::string AsmWriterOperand::getCode(bool PassSubtarget) const {
+std::string AsmWriterOperand::getCode(bool PassSubtarget,
+ const std::string &Receiver) const {
if (OperandType == isLiteralTextOperand) {
if (Str.size() == 1)
return "O << '" + Str + "';";
@@ -30,7 +31,7 @@ std::string AsmWriterOperand::getCode(bool PassSubtarget) const {
if (OperandType == isLiteralStatementOperand)
return Str;
- std::string Result = Str + "(MI";
+ std::string Result = Receiver + Str + "(MI";
if (PCRel)
Result += ", Address";
if (MIOpNo != ~0U)
diff --git a/llvm/utils/TableGen/Common/AsmWriterInst.h b/llvm/utils/TableGen/Common/AsmWriterInst.h
index 26745a8459570..055ff651ba442 100644
--- a/llvm/utils/TableGen/Common/AsmWriterInst.h
+++ b/llvm/utils/TableGen/Common/AsmWriterInst.h
@@ -74,7 +74,10 @@ struct AsmWriterOperand {
}
/// getCode - Return the code that prints this operand.
- std::string getCode(bool PassSubtarget) const;
+ /// Receiver, if non-empty, is prepended to member-function calls (e.g.
+ /// "P->" turns "printOperand(...)" into "P->printOperand(...)").
+ std::string getCode(bool PassSubtarget,
+ const std::string &Receiver = "") const;
};
class AsmWriterInst {
More information about the llvm-commits
mailing list