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

via llvm-commits llvm-commits at lists.llvm.org
Mon Aug 24 14:49:23 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-tablegen

Author: Malavika Samak (malavikasamak)

<details>
<summary>Changes</summary>

For targets with many instructions (NVPTX in particular), the TableDrivenOperandPrinters fragments and overflow switch emitted inside printInstruction() 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)

Two mechanisms are introduced, both currently gated on NVPTX:

1. Overflow switch split by opcode category (EmitOpcodeGroupTable + EmitOpcodeGroupDispatch): partitions overflow instructions by TSFlags (IsLoad/IsStore/texture-family) then AsmString mnemonic prefix, emits a static dispatch table and per-category immediately-invoked lambdas.

2. TableDrivenOperandPrinters IIFE wrapping: each fragment slot is emitted as a [this]-capturing immediately-invoked lambda inside printInstruction(). The compiler treats each body as a separate internal function. The [this] capture gives lambdas full access to member methods (printOperand etc.) without requiring any header declarations.

A follow-up can generalize both mechanisms to any target exceeding a function-size threshold.

---
Full diff: https://github.com/llvm/llvm-project/pull/218530.diff


1 Files Affected:

- (modified) llvm/utils/TableGen/AsmWriterEmitter.cpp (+170-2) 


``````````diff
diff --git a/llvm/utils/TableGen/AsmWriterEmitter.cpp b/llvm/utils/TableGen/AsmWriterEmitter.cpp
index 86ee350834b5c..819cc7bb95d1f 100644
--- a/llvm/utils/TableGen/AsmWriterEmitter.cpp
+++ b/llvm/utils/TableGen/AsmWriterEmitter.cpp
@@ -81,6 +81,15 @@ class AsmWriterEmitter {
                                  std::vector<std::vector<unsigned>> &InstIdxs,
                                  std::vector<unsigned> &InstOpsUsed,
                                  bool PassSubtarget) const;
+
+  // Groups of overflow-switch instructions keyed by TSFlags / mnemonic prefix.
+  // Used to split large printInstruction() overflow switches into per-category
+  // [this]-capturing lambdas, keeping individual function bodies small.
+  using OpcodeGroupList =
+      SmallVector<std::pair<std::string, std::vector<AsmWriterInst>>, 16>;
+  OpcodeGroupList EmitOpcodeGroupTable(raw_ostream &O);
+  void EmitOpcodeGroupDispatch(raw_ostream &O, OpcodeGroupList &Groups,
+                               bool PassSubtarget);
 };
 
 } // end anonymous namespace
@@ -490,6 +499,15 @@ void AsmWriterEmitter::EmitPrintInstruction(
   StringRef ClassName = AsmWriter->getValueAsString("AsmWriterClassName");
   bool PassSubtarget = AsmWriter->getValueAsInt("PassSubtarget");
 
+  // For targets with large instruction sets, split the overflow switch into
+  // per-category lambdas to avoid compiler ICE on function-body size limits.
+  // The dispatch table is emitted here (before printInstruction() opens) since
+  // it is plain static data; the lambda bodies are emitted inside
+  // printInstruction() so member-method calls resolve via the captured this.
+  OpcodeGroupList OpcodeGroups;
+  if (Target.getName() == "NVPTX")
+    OpcodeGroups = EmitOpcodeGroupTable(O);
+
   // 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
@@ -525,6 +543,61 @@ void AsmWriterEmitter::EmitPrintInstruction(
     unsigned NumBits = Log2_32_Ceil(Commands.size());
     assert(NumBits <= BitsLeft && "consistency error");
 
+    // For NVPTX (large instruction set), wrap each TableDrivenOperandPrinters
+    // fragment in an immediately-invoked [this]-capturing lambda. The compiler
+    // treats each lambda body as a separate internal function, keeping
+    // printInstruction() itself small enough to avoid MSVC ICE on aarch64
+    // and RISCV JAL relocation overflow on size-constrained targets.
+    // FIXME: generalize to any target exceeding a function-size threshold.
+    if (Target.getName() == "NVPTX") {
+      // Rewrite embedded `return;` as `return false;`: early exit from the
+      // lambda means "this instruction is fully handled, stop processing".
+      auto processCmd = [](std::string S) -> std::pair<std::string, bool> {
+        size_t P = S.find("return;");
+        if (P == std::string::npos)
+          return {S, false};
+        S.replace(P, 7, "return false;\n");
+        return {S.substr(0, P + 14), true};
+      };
+
+      uint64_t Mask = (1ULL << NumBits) - 1;
+      O << "\n  // Fragment " << i << ": IIFE to reduce printInstruction() size.\n";
+      O << "  if (![this](const MCInst *MI, uint64_t Address, "
+        << (PassSubtarget ? "const MCSubtargetInfo &STI, " : "")
+        << "raw_ostream &O, uint64_t Bits) -> bool {\n";
+
+      if (Commands.size() == 1) {
+        auto [Body, HasRet] = processCmd(Commands[0]);
+        O << Body;
+        if (!HasRet)
+          O << "    return true;\n";
+      } else if (Commands.size() == 2) {
+        auto [B0, R0] = processCmd(Commands[0]);
+        auto [B1, R1] = processCmd(Commands[1]);
+        O << "    if ((Bits >> " << (OpcodeInfoBits - BitsLeft) << ") & "
+          << Mask << ") {\n" << B1;
+        if (!R1) O << "      return true;\n";
+        O << "    } else {\n" << B0;
+        if (!R0) O << "      return true;\n";
+        O << "    }\n";
+      } else {
+        O << "    switch ((Bits >> " << (OpcodeInfoBits - BitsLeft) << ") & "
+          << Mask << ") {\n"
+          << "    default: llvm_unreachable(\"Invalid command number.\");\n";
+        for (unsigned j = 0; j < Commands.size(); ++j) {
+          auto [Body, HasRet] = processCmd(Commands[j]);
+          O << "    case " << j << ":\n" << Body;
+          if (!HasRet) O << "      return true;\n";
+        }
+        O << "    }\n";
+      }
+
+      O << "  }(MI, Address, " << (PassSubtarget ? "STI, " : "")
+        << "O, Bits))\n    return;\n\n";
+      BitsLeft -= NumBits;
+      continue;
+    }
+
     // Emit code to extract this field from Bits.
     O << "\n  // Fragment " << i << " encoded into " << NumBits << " bits for "
       << Commands.size() << " unique commands.\n";
@@ -566,8 +639,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, OpcodeGroups, PassSubtarget);
+  } else if (!Instructions.empty()) {
     O << "  switch (MI->getOpcode()) {\n";
     O << "  default: llvm_unreachable(\"Unexpected opcode.\");\n";
     while (!Instructions.empty())
@@ -579,6 +653,100 @@ void AsmWriterEmitter::EmitPrintInstruction(
   O << "}\n";
 }
 
+AsmWriterEmitter::OpcodeGroupList
+AsmWriterEmitter::EmitOpcodeGroupTable(raw_ostream &O) {
+  OpcodeGroupList Groups;
+
+  // Pre-filter: same predicate as the erase_if in EmitPrintInstruction.
+  std::vector<AsmWriterInst> OpcodeInsts;
+  for (const AsmWriterInst &AWI : Instructions)
+    if (!AWI.Operands.empty())
+      OpcodeInsts.push_back(AWI);
+
+  if (OpcodeInsts.empty())
+    return Groups;
+
+  // Assign each instruction to a group: TSFlags take priority, then the
+  // AsmString mnemonic prefix is used as a fine-grained fallback.
+  auto getGroupKey = [](const AsmWriterInst &AWI) -> std::string {
+    const Record *Def = AWI.CGI->TheDef;
+    if (Def->getValueAsBit("IsLoad")) return "loads";
+    if (Def->getValueAsBit("IsStore")) return "stores";
+    auto isSuldSet = [](const BitsInit *B) {
+      for (unsigned i = 0; i < B->getNumBits(); ++i)
+        if (auto *Bit = dyn_cast<BitInit>(B->getBit(i)); Bit && Bit->getValue())
+          return true;
+      return false;
+    };
+    if (Def->getValueAsBit("IsTex") || Def->getValueAsBit("IsSust") ||
+        Def->getValueAsBit("IsSurfTexQuery") ||
+        isSuldSet(Def->getValueAsBitsInit("IsSuld")))
+      return "texture";
+    StringRef S = Def->getValueAsString("AsmString").ltrim(" \t");
+    if (S.empty() || S[0] == '$' || S[0] == '{' || S[0] == '@') return "misc";
+    size_t End = S.find_first_of(".${ \t\\");
+    std::string Key = S.substr(0, End == StringRef::npos ? S.size() : End).str();
+    return Key.empty() ? "misc" : Key;
+  };
+
+  for (const AsmWriterInst &AWI : OpcodeInsts) {
+    std::string Key = getGroupKey(AWI);
+    auto It = llvm::find_if(Groups, [&](auto &G) { return G.first == Key; });
+    if (It == Groups.end()) {
+      Groups.push_back({Key, {}});
+      It = Groups.end() - 1;
+    }
+    It->second.push_back(AWI);
+  }
+
+  // Dispatch table: 0 = not in overflow set, 1..N = group index.
+  // 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");
+  std::vector<uint8_t> Table(NumberedInstructions.size(), 0);
+  for (unsigned GIdx = 0; GIdx < Groups.size(); ++GIdx)
+    for (const AsmWriterInst &AWI : Groups[GIdx].second)
+      Table[AWI.CGIIndex] = GIdx + 1;
+
+  O << "static const uint8_t NVPTXOpcodeGroup[] = {\n";
+  for (unsigned i = 0; i < NumberedInstructions.size(); ++i)
+    O << "  " << unsigned(Table[i]) << ",\t// "
+      << NumberedInstructions[i]->getName() << "\n";
+  O << "};\n\n";
+
+  return Groups;
+}
+
+void AsmWriterEmitter::EmitOpcodeGroupDispatch(raw_ostream &O,
+                                               OpcodeGroupList &Groups,
+                                               bool PassSubtarget) {
+  // Two-level dispatch: table lookup then IIFE per arm. Emitting each group
+  // body as an immediately-invoked lambda means:
+  //  - The lambda body compiles as a separate internal function, so
+  //    printInstruction() itself stays small (no function-size regression).
+  //  - The lambda captures [this], so getCode()-generated member calls
+  //    (e.g. printOperand) resolve correctly without a receiver parameter.
+  //  - Each lambda is only instantiated in the arm actually taken.
+  O << "  uint8_t OpcGroup = NVPTXOpcodeGroup[MI->getOpcode()];\n"
+    << "  if (OpcGroup != 0) {\n"
+    << "    switch (OpcGroup) {\n";
+  unsigned GIdx = 0;
+  for (auto &[Key, Insts] : Groups) {
+    O << "    case " << (++GIdx) << ": [this](const MCInst *MI, uint64_t Address, "
+      << (PassSubtarget ? "const MCSubtargetInfo &STI, " : "")
+      << "raw_ostream &O) {\n"
+      << "      switch (MI->getOpcode()) {\n"
+      << "      default: break;\n";
+    std::vector<AsmWriterInst> GroupInsts = Insts;
+    std::reverse(GroupInsts.begin(), GroupInsts.end());
+    while (!GroupInsts.empty())
+      EmitInstructions(GroupInsts, O, PassSubtarget);
+    O << "      }\n    }(MI, Address, " << (PassSubtarget ? "STI, " : "")
+      << "O); break;\n";
+  }
+  O << "    default: llvm_unreachable(\"Unexpected opcode group.\");\n"
+    << "    }\n  }\n";
+}
+
 static void
 emitRegisterNameString(raw_ostream &O, StringRef AltName,
                        const std::deque<CodeGenRegister> &Registers) {

``````````

</details>


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


More information about the llvm-commits mailing list