[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