[llvm] [TableGen] Split large printInstruction() for NVPTX to avoid compiler limits (PR #218530)

Malavika Samak via llvm-commits llvm-commits at lists.llvm.org
Wed Sep 23 10:46:45 PDT 2026


https://github.com/malavikasamak updated https://github.com/llvm/llvm-project/pull/218530

>From f5d864372ad1556fc8449a77269e081e95691355 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] Emit function-pointer table for AsmWriter overflow
 instructions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Problem
-------
For targets with many instructions (NVPTX in particular), the overflow switch
emitted inside printInstruction() for operand sequences that cannot be encoded
in the table-driven bit-packing path can grow large enough to trigger:

  - MSVC C1001/C1053 ICE on AArch64 (function body too large)
  - R_RISCV_JAL relocation truncated to fit (JAL ±1 MB range exceeded)

Solution
--------
Move each unique overflow operand sequence into a small static printPattern_N
function and dispatch through a const function-pointer table indexed by opcode,
replacing the switch entirely:

- Add a `Receiver` parameter to `AsmWriterOperand::getCode()` so that code
  emitted inside static free functions calls `P->printOperand(...)` rather
  than the member function directly.
- `EmitOpcodePatternTable()`: collects overflow instructions, deduplicates
  identical operand sequences into shared `printPattern_N` statics, and emits
  a `<Target>InstPrinters[]` const function-pointer table covering every
  opcode. Non-overflow opcodes point to a no-op `printPattern_None`.
- `EmitOpcodePatternDispatch()`: replaces the overflow switch with a single
  unconditional indirect call through the table, keeping printInstruction()
  small regardless of instruction-set size.

The mechanism triggers for any target with overflow instructions, so it is not
NVPTX-specific. A follow-up can generalize the threshold-based triggering to
any target that exceeds a function-size limit.
---
 llvm/include/llvm/Target/Target.td           |   7 +
 llvm/lib/Target/NVPTX/NVPTX.td               |   1 +
 llvm/utils/TableGen/AsmWriterEmitter.cpp     | 148 +++++++++++++++++--
 llvm/utils/TableGen/Common/AsmWriterInst.cpp |   5 +-
 llvm/utils/TableGen/Common/AsmWriterInst.h   |   5 +-
 5 files changed, 150 insertions(+), 16 deletions(-)

diff --git a/llvm/include/llvm/Target/Target.td b/llvm/include/llvm/Target/Target.td
index 53139e9a2bfeae..0c969a4cb351ea 100644
--- a/llvm/include/llvm/Target/Target.td
+++ b/llvm/include/llvm/Target/Target.td
@@ -2060,6 +2060,13 @@ class AsmWriter {
   // will specify which alternative to use. For example "{x|y|z}" with Variant
   // == 1, will expand to "y".
   int Variant = 0;
+
+  // UseFunctionPointerTable - If set to 1, emit a function-pointer table
+  // instead of a switch statement for instructions that overflow the 64-bit
+  // table-driven printer.  This keeps printInstruction() small, avoiding
+  // MSVC C1001/C1053 ICEs and R_RISCV_JAL relocation-range overflows on
+  // targets with large instruction sets.
+  bit UseFunctionPointerTable = 0;
 }
 def DefaultAsmWriter : AsmWriter;
 
diff --git a/llvm/lib/Target/NVPTX/NVPTX.td b/llvm/lib/Target/NVPTX/NVPTX.td
index f9be7928ad1026..67982d05896372 100644
--- a/llvm/lib/Target/NVPTX/NVPTX.td
+++ b/llvm/lib/Target/NVPTX/NVPTX.td
@@ -140,6 +140,7 @@ def NVPTXInstrInfo : InstrInfo {
 
 def NVPTXAsmWriter : AsmWriter {
   int PassSubtarget = 1;
+  bit UseFunctionPointerTable = 1;
 }
 
 def NVPTX : Target {
diff --git a/llvm/utils/TableGen/AsmWriterEmitter.cpp b/llvm/utils/TableGen/AsmWriterEmitter.cpp
index f3e04bb0f0849e..683950f1a548c9 100644
--- a/llvm/utils/TableGen/AsmWriterEmitter.cpp
+++ b/llvm/utils/TableGen/AsmWriterEmitter.cpp
@@ -91,6 +91,16 @@ class AsmWriterEmitter {
                                  std::vector<std::vector<unsigned>> &InstIdxs,
                                  std::vector<unsigned> &InstOpsUsed,
                                  bool PassSubtarget) const;
+
+  // Emit a flat function-pointer table covering all opcodes.  Each unique
+  // operand-printing sequence in the overflow set becomes one printPattern_N
+  // static helper; instructions sharing a sequence share the same helper.
+  // The table is indexed directly by getOpcode(), so dispatch is a single
+  // unconditional indirect call with no switch or branch.
+  void EmitOpcodePatternTable(raw_ostream &O, StringRef TargetName,
+                              StringRef ClassName, bool PassSubtarget);
+  void EmitOpcodePatternDispatch(raw_ostream &O, StringRef TargetName,
+                                 StringRef ClassName, bool PassSubtarget);
 };
 
 } // end anonymous namespace
@@ -510,6 +520,15 @@ void AsmWriterEmitter::EmitPrintInstruction(
   const Record *AsmWriter = Target.getAsmWriter();
   StringRef ClassName = AsmWriter->getValueAsString("AsmWriterClassName");
   bool PassSubtarget = AsmWriter->getValueAsInt("PassSubtarget");
+  bool UseFnPtrTable = AsmWriter->getValueAsBit("UseFunctionPointerTable");
+
+  // When opt-in: pre-emit static printPattern_N helpers and function-pointer
+  // table before printInstruction().  A single indirect call in
+  // printInstruction() then replaces the overflow switch, keeping the function
+  // body small and avoiding MSVC C1001/C1053 ICEs and R_RISCV_JAL
+  // relocation-range overflows that arise on large instruction sets.
+  if (UseFnPtrTable && !Instructions.empty())
+    EmitOpcodePatternTable(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
@@ -579,29 +598,132 @@ void AsmWriterEmitter::EmitPrintInstruction(
   llvm::erase_if(Instructions,
                  [](AsmWriterInst &Inst) { return Inst.Operands.empty(); });
 
-  // Because this is a vector, we want to emit from the end.  Reverse all of the
-  // elements in the vector.
-  std::reverse(Instructions.begin(), Instructions.end());
-
-  std::vector<InstructionGroup> Groups = groupInstructions(Instructions);
-
   // Now that we've emitted all of the operand info that fit into 64 bits, emit
   // 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 (!Groups.empty()) {
-    // Find the opcode # of inline asm.
-    O << "  switch (MI->getOpcode()) {\n";
-    O << "  default: llvm_unreachable(\"Unexpected opcode.\");\n";
-    for (const InstructionGroup &G : Groups)
-      emitInstructions(G, O, PassSubtarget);
+  if (!Instructions.empty()) {
+    if (UseFnPtrTable) {
+      // Single unconditional indirect call; the function-pointer table was
+      // emitted above, before printInstruction().
+      EmitOpcodePatternDispatch(O, Target.getName(), ClassName, PassSubtarget);
+    } else {
+      // Because this is a vector, we want to emit from the end.  Reverse all
+      // of the elements in the vector.
+      std::reverse(Instructions.begin(), Instructions.end());
 
-    O << "  }\n";
+      std::vector<InstructionGroup> Groups = groupInstructions(Instructions);
+
+      O << "  switch (MI->getOpcode()) {\n";
+      O << "  default: llvm_unreachable(\"Unexpected opcode.\");\n";
+      for (const InstructionGroup &G : Groups)
+        emitInstructions(G, O, PassSubtarget);
+
+      O << "  }\n";
+    }
   }
 
   O << "}\n";
 }
 
+void AsmWriterEmitter::EmitOpcodePatternTable(raw_ostream &O,
+                                              StringRef TargetName,
+                                              StringRef ClassName,
+                                              bool PassSubtarget) {
+  // Collect overflow instructions that still have operands to print.
+  std::vector<AsmWriterInst> OpcodeInsts;
+  for (const AsmWriterInst &AWI : Instructions) {
+    if (!AWI.Operands.empty())
+      OpcodeInsts.push_back(AWI);
+  }
+
+  assert(!OpcodeInsts.empty() && "caller should have checked HasOverflow");
+
+  // Serialize each instruction's complete operand sequence into a string key.
+  // Instructions with identical keys share one printPattern_N function.
+  // getCode() with Receiver="P->" turns member calls like printOperand(...)
+  // into P->printOperand(...), which is correct for a static free function.
+  using PatternBody = std::vector<std::string>;
+  std::map<PatternBody, unsigned> PatternMap;
+  SmallVector<PatternBody, 64> Patterns;
+
+  // Table: index by CGIIndex, value = pattern index (~0U for non-overflow).
+  std::vector<unsigned> OpcodeToPattern(NumberedInstructions.size(), ~0U);
+
+  for (const AsmWriterInst &AWI : OpcodeInsts) {
+    PatternBody Body;
+    for (const AsmWriterOperand &Op : AWI.Operands)
+      Body.push_back(Op.getCode(PassSubtarget, "P->"));
+    auto [It, Inserted] = PatternMap.emplace(Body, Patterns.size());
+    if (Inserted)
+      Patterns.push_back(Body);
+    OpcodeToPattern[AWI.CGIIndex] = It->second;
+  }
+
+  std::string FullClassName = (TargetName + ClassName).str();
+  // Include ClassName in the table name so multiple AsmWriter variants in the
+  // same TU (e.g. AArch64GenAsmWriter.inc + AArch64GenAsmWriter1.inc) don't
+  // produce conflicting variable declarations.
+  std::string PtrTableName = FullClassName + "Printers";
+  std::string ParamList = "    " + FullClassName +
+                          " *, const MCInst *,\n"
+                          "    uint64_t," +
+                          (PassSubtarget ? " const MCSubtargetInfo &," : "") +
+                          " raw_ostream &";
+
+  // printPattern_None: no-op for instructions fully handled by table-driven
+  // path.
+  O << "static void printPattern_None(\n" << ParamList << ") {}\n\n";
+
+  // One printPattern_N per unique operand sequence.
+  for (unsigned PIdx = 0, E = Patterns.size(); PIdx < E; ++PIdx) {
+    O << "static void printPattern_" << PIdx << "(\n"
+      << "    " << FullClassName << " *P, const MCInst *MI,\n"
+      << "    uint64_t Address,";
+    if (PassSubtarget)
+      O << " const MCSubtargetInfo &STI,";
+    O << " raw_ostream &O) {\n";
+    for (const std::string &Line : Patterns[PIdx])
+      O << "  " << Line << "\n";
+    O << "}\n\n";
+  }
+
+  // Function-pointer table indexed by opcode, covering the full opcode range.
+  // Non-overflow opcodes → printPattern_None.
+  // Overflow opcodes     → their deduplicated printPattern_N.
+  O << "static void (*const " << PtrTableName << "[])(\n"
+    << ParamList << ") = {\n";
+  for (unsigned i = 0, E = NumberedInstructions.size(); i < E; ++i) {
+    unsigned PIdx = OpcodeToPattern[i];
+    if (PIdx == ~0U)
+      O << "  &printPattern_None";
+    else
+      O << "  &printPattern_" << PIdx;
+    if (i + 1 < E)
+      O << ",";
+    O << "\t// " << NumberedInstructions[i]->getName() << "\n";
+  }
+  O << "};\n\n";
+
+  LLVM_DEBUG(dbgs() << "[AsmWriter] " << TargetName << ": "
+                    << OpcodeInsts.size() << " overflow instructions -> "
+                    << Patterns.size() << " unique patterns\n");
+}
+
+void AsmWriterEmitter::EmitOpcodePatternDispatch(raw_ostream &O,
+                                                 StringRef TargetName,
+                                                 StringRef ClassName,
+                                                 bool PassSubtarget) {
+  // Single unconditional indirect call; no switch, no branch.
+  // The compiler cannot inline through a const function pointer, so
+  // printInstruction() itself stays small regardless of instruction-set size.
+  std::string PtrTableName = (TargetName + ClassName + "Printers").str();
+  O << "  " << PtrTableName << "[MI->getOpcode()](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 97430a1eec8a1e..4afdb67bd78444 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,
+                                      StringRef 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() + 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 26745a8459570a..fed7825fbfe65b 100644
--- a/llvm/utils/TableGen/Common/AsmWriterInst.h
+++ b/llvm/utils/TableGen/Common/AsmWriterInst.h
@@ -16,6 +16,7 @@
 #ifndef LLVM_UTILS_TABLEGEN_COMMON_ASMWRITERINST_H
 #define LLVM_UTILS_TABLEGEN_COMMON_ASMWRITERINST_H
 
+#include "llvm/ADT/StringRef.h"
 #include <string>
 #include <vector>
 
@@ -74,7 +75,9 @@ 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, StringRef Receiver = "") const;
 };
 
 class AsmWriterInst {



More information about the llvm-commits mailing list