[llvm] [TableGen] Pool duplicate scheduling class descriptors (PR #202659)

David Zbarsky via llvm-commits llvm-commits at lists.llvm.org
Thu Jun 11 17:59:13 PDT 2026


https://github.com/dzbarsky updated https://github.com/llvm/llvm-project/pull/202659

>From d0be57d0e136badb565b9088e3793bc2d68da643 Mon Sep 17 00:00:00 2001
From: David Zbarsky <dzbarsky at gmail.com>
Date: Tue, 9 Jun 2026 03:08:33 -0400
Subject: [PATCH] [TableGen] Pool duplicate scheduling class descriptors

Release builds currently emit one MCSchedClassDesc table for every
processor model. The RISC-V backend emits 158,654 descriptors, but only
4,206 descriptors are unique.

Build one target-wide descriptor pool in SubtargetEmitter and emit a
uint16_t index table for each processor model when the pooled
representation is smaller. Keep the existing per-model tables in debug
and dump-enabled builds so scheduling class names remain available.
MCSchedModel::getSchedClassDesc resolves the index before returning the
descriptor. Assert the release and debug MCSchedClassDesc sizes used by
the size calculation, the generated index width, and every generated
index-table length.

On an arm64 Release build with all RISC-V processors, stripped sizes
change as follows:

  llc:       57,253,952 -> 55,388,272 bytes (-1,865,680)
  llvm-mca:   6,242,424 ->  4,376,744 bytes (-1,865,680)

Pooling the debug tables was also measured. The debug-pooling version changes +320/-64 lines in five files, while this release-only version changes +224/-7 lines in four files. In matched current builds, fully stripped llc and llvm-mca have identical sizes. The release-only raw binaries are 1,488 and 1,520 bytes larger, respectively; __TEXT,__const is 16 and 0 bytes larger, __DATA_CONST,__const is 184 bytes larger in both, and linked fixup counts are identical. Keep the direct debug tables because debug pooling does not reduce the fully stripped binaries.

All 158,654 reconstructed RISC-V descriptors matched the direct tables.
llc output was byte-identical for the default CPU, sifive-p670,
spacemit-x100, and xiangshan-kunminghu. llvm-mca output was also
byte-identical.

A 100-pair alternating llvm-mca benchmark used the SCR7 ALU test with
100,000 iterations. Direct and pooled medians were both 1.31 seconds.
The paired mean difference was +0.0039 seconds, with a bootstrapped 95%
interval of -0.0031 to +0.0110 seconds. TableGen generation time changed
from a 2.630-second median to 2.645 seconds.

The new TableGen test checks pooled descriptors, per-model indices, and
debug-table preservation.
---
 llvm/include/llvm/MC/MCSchedule.h            |   9 ++
 llvm/lib/MC/MCSchedule.cpp                   |   1 +
 llvm/test/TableGen/SchedClassTablePooling.td |  74 ++++++++++
 llvm/utils/TableGen/SubtargetEmitter.cpp     | 147 ++++++++++++++++++-
 4 files changed, 224 insertions(+), 7 deletions(-)
 create mode 100644 llvm/test/TableGen/SchedClassTablePooling.td

diff --git a/llvm/include/llvm/MC/MCSchedule.h b/llvm/include/llvm/MC/MCSchedule.h
index 8c21dbd0ae385..2b623128ef547 100644
--- a/llvm/include/llvm/MC/MCSchedule.h
+++ b/llvm/include/llvm/MC/MCSchedule.h
@@ -152,6 +152,12 @@ struct MCSchedClassDesc {
   }
 };
 
+#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+static_assert(sizeof(MCSchedClassDesc) == 20);
+#else
+static_assert(sizeof(MCSchedClassDesc) == 14);
+#endif
+
 /// Specify the cost of a register definition in terms of number of physical
 /// register allocated at register renaming stage. For example, AMD Jaguar.
 /// natively supports 128-bit data types, and operations on 256-bit registers
@@ -337,6 +343,7 @@ struct MCSchedModel {
   const InstrItinerary *InstrItineraries;
 
   const MCExtraProcessorInfo *ExtraProcessorInfo;
+  const uint16_t *SchedClassTableIndices;
 
   bool hasExtraProcessorInfo() const { return ExtraProcessorInfo; }
 
@@ -373,6 +380,8 @@ struct MCSchedModel {
     assert(hasInstrSchedModel() && "No scheduling machine model");
 
     assert(SchedClassIdx < NumSchedClasses && "bad scheduling class idx");
+    if (SchedClassTableIndices)
+      SchedClassIdx = SchedClassTableIndices[SchedClassIdx];
     return &SchedClassTable[SchedClassIdx];
   }
 
diff --git a/llvm/lib/MC/MCSchedule.cpp b/llvm/lib/MC/MCSchedule.cpp
index e2124c1c76355..cebe0c2fb5c2d 100644
--- a/llvm/lib/MC/MCSchedule.cpp
+++ b/llvm/lib/MC/MCSchedule.cpp
@@ -54,6 +54,7 @@ const MCSchedModel MCSchedModel::Default = {DefaultIssueWidth,
                                             0,
                                             nullptr,
                                             nullptr,
+                                            nullptr,
                                             nullptr};
 
 int MCSchedModel::computeInstrLatency(const MCSubtargetInfo &STI,
diff --git a/llvm/test/TableGen/SchedClassTablePooling.td b/llvm/test/TableGen/SchedClassTablePooling.td
new file mode 100644
index 0000000000000..cbb7cca50df42
--- /dev/null
+++ b/llvm/test/TableGen/SchedClassTablePooling.td
@@ -0,0 +1,74 @@
+// RUN: llvm-tblgen -gen-subtarget -I %p/../../include %s -o - | FileCheck %s
+
+include "llvm/Target/Target.td"
+
+def MyTarget : Target;
+
+let OutOperandList = (outs), InOperandList = (ins) in {
+  def InstA : Instruction;
+  def InstB : Instruction;
+}
+
+let CompleteModel = 0 in {
+  def ModelA : SchedMachineModel;
+  def ModelB : SchedMachineModel;
+}
+
+let SchedModel = ModelA in {
+  def WriteA0 : SchedWriteRes<[]>;
+  def WriteB0 : SchedWriteRes<[]>;
+
+  def : InstRW<[WriteA0], (instrs InstA)>;
+  def : InstRW<[WriteB0], (instrs InstB)>;
+}
+
+let SchedModel = ModelB in {
+  def WriteA1 : SchedWriteRes<[]>;
+  def WriteB1 : SchedWriteRes<[]>;
+
+  def : InstRW<[WriteA1], (instrs InstA)>;
+  def : InstRW<[WriteB1], (instrs InstB)>;
+}
+
+def ProcessorA : ProcessorModel<"processor-a", ModelA, []>;
+def ProcessorB : ProcessorModel<"processor-b", ModelB, []>;
+
+// Debug builds retain the per-processor tables so class names are available.
+// CHECK: #if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+// CHECK: static const llvm::MCSchedClassDesc ModelASchedClasses[] = {
+// CHECK: static const llvm::MCSchedClassDesc ModelBSchedClasses[] = {
+
+// Release builds share identical descriptors through a compact index table.
+// CHECK: #else
+// CHECK: static const llvm::MCSchedClassDesc MyTargetSchedClassPool[] = {
+// CHECK: static_assert(sizeof(MyTargetSchedClassPool) / sizeof(MyTargetSchedClassPool[0]) <= (1ULL << 16));
+// CHECK: static const uint16_t ModelASchedClassIndices[] = {
+// CHECK: static_assert(sizeof(ModelASchedClassIndices[0]) == 2);
+// CHECK: static_assert(sizeof(ModelASchedClassIndices) / sizeof(ModelASchedClassIndices[0]) == 3);
+// CHECK: static const uint16_t ModelBSchedClassIndices[] = {
+// CHECK: static_assert(sizeof(ModelBSchedClassIndices[0]) == 2);
+// CHECK: static_assert(sizeof(ModelBSchedClassIndices) / sizeof(ModelBSchedClassIndices[0]) == 3);
+
+// CHECK: static const llvm::MCSchedModel ModelA = {
+// CHECK: #if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+// CHECK-NEXT:   ModelASchedClasses,
+// CHECK-NEXT: #else
+// CHECK-NEXT:   MyTargetSchedClassPool,
+// CHECK-NEXT: #endif
+// CHECK: #if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+// CHECK-NEXT:   nullptr,
+// CHECK-NEXT: #else
+// CHECK-NEXT:   ModelASchedClassIndices,
+// CHECK-NEXT: #endif
+
+// CHECK: static const llvm::MCSchedModel ModelB = {
+// CHECK: #if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+// CHECK-NEXT:   ModelBSchedClasses,
+// CHECK-NEXT: #else
+// CHECK-NEXT:   MyTargetSchedClassPool,
+// CHECK-NEXT: #endif
+// CHECK: #if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
+// CHECK-NEXT:   nullptr,
+// CHECK-NEXT: #else
+// CHECK-NEXT:   ModelBSchedClassIndices,
+// CHECK-NEXT: #endif
diff --git a/llvm/utils/TableGen/SubtargetEmitter.cpp b/llvm/utils/TableGen/SubtargetEmitter.cpp
index c3a9d69d6ec3e..a222286dc824d 100644
--- a/llvm/utils/TableGen/SubtargetEmitter.cpp
+++ b/llvm/utils/TableGen/SubtargetEmitter.cpp
@@ -36,7 +36,10 @@
 #include <cassert>
 #include <cstdint>
 #include <iterator>
+#include <limits>
+#include <map>
 #include <string>
+#include <tuple>
 #include <vector>
 
 using namespace llvm;
@@ -45,6 +48,17 @@ using namespace llvm;
 
 namespace {
 
+using SchedClassKey = std::tuple<uint16_t, bool, bool, bool, uint16_t, uint16_t,
+                                 uint16_t, uint16_t, uint16_t, uint16_t>;
+
+static SchedClassKey getSchedClassKey(const MCSchedClassDesc &Desc) {
+  return {Desc.NumMicroOps,     Desc.BeginGroup,
+          Desc.EndGroup,        Desc.RetireOOO,
+          Desc.WriteProcResIdx, Desc.NumWriteProcResEntries,
+          Desc.WriteLatencyIdx, Desc.NumWriteLatencyEntries,
+          Desc.ReadAdvanceIdx,  Desc.NumReadAdvanceEntries};
+}
+
 class SubtargetEmitter : TargetFeaturesEmitter {
   // Each processor has a SchedClassDesc table with an entry for each
   // SchedClass. The SchedClassDesc table indexes into a global write resource
@@ -55,6 +69,9 @@ class SubtargetEmitter : TargetFeaturesEmitter {
     std::vector<MCWriteLatencyEntry> WriteLatencies;
     std::vector<std::string> WriterNames;
     std::vector<MCReadAdvanceEntry> ReadAdvanceEntries;
+    std::vector<MCSchedClassDesc> SchedClassPool;
+    std::vector<std::vector<uint16_t>> ProcSchedClassIndices;
+    bool UseSchedClassPool = false;
 
     // Reserve an invalid entry at index 0
     SchedClassTables() {
@@ -124,8 +141,10 @@ class SubtargetEmitter : TargetFeaturesEmitter {
                            const CodeGenProcModel &ProcModel);
   void genSchedClassTables(const CodeGenProcModel &ProcModel,
                            SchedClassTables &SchedTables);
+  void buildSchedClassPool(SchedClassTables &SchedTables);
   void emitSchedClassTables(SchedClassTables &SchedTables, raw_ostream &OS);
-  void emitProcessorModels(raw_ostream &OS);
+  void emitProcessorModels(const SchedClassTables &SchedTables,
+                           raw_ostream &OS);
   void emitSchedModelHelpers(const std::string &ClassName, raw_ostream &OS);
   void emitSchedModelHelpersImpl(raw_ostream &OS,
                                  bool OnlyExpandMCInstPredicates = false);
@@ -1332,6 +1351,53 @@ void SubtargetEmitter::genSchedClassTables(const CodeGenProcModel &ProcModel,
   }
 }
 
+void SubtargetEmitter::buildSchedClassPool(SchedClassTables &SchedTables) {
+  constexpr size_t ReleaseSchedClassDescSize = 14;
+  std::map<SchedClassKey, uint16_t> PoolIndices;
+  size_t NumSchedClassDescs = 0;
+
+  SchedTables.ProcSchedClassIndices.clear();
+  SchedTables.ProcSchedClassIndices.resize(SchedTables.ProcSchedClasses.size());
+
+  for (const auto &[Idx, Proc] : enumerate(SchedModels.procModels())) {
+    if (!Proc.hasInstrSchedModel())
+      continue;
+
+    const std::vector<MCSchedClassDesc> &SCTab =
+        SchedTables.ProcSchedClasses[1 + Idx];
+    std::vector<uint16_t> &Indices = SchedTables.ProcSchedClassIndices[1 + Idx];
+    Indices.reserve(SCTab.size());
+    NumSchedClassDescs += SCTab.size();
+
+    for (const MCSchedClassDesc &Desc : SCTab) {
+      SchedClassKey Key = getSchedClassKey(Desc);
+      auto It = PoolIndices.find(Key);
+      if (It == PoolIndices.end()) {
+        if (SchedTables.SchedClassPool.size() >
+            std::numeric_limits<uint16_t>::max()) {
+          SchedTables.SchedClassPool.clear();
+          SchedTables.ProcSchedClassIndices.clear();
+          return;
+        }
+        uint16_t PoolIdx = SchedTables.SchedClassPool.size();
+        It = PoolIndices.emplace(std::move(Key), PoolIdx).first;
+        SchedTables.SchedClassPool.push_back(Desc);
+      }
+      Indices.push_back(It->second);
+    }
+  }
+
+  size_t DirectSize = NumSchedClassDescs * ReleaseSchedClassDescSize;
+  size_t PooledSize =
+      SchedTables.SchedClassPool.size() * ReleaseSchedClassDescSize +
+      NumSchedClassDescs * sizeof(uint16_t);
+  SchedTables.UseSchedClassPool = PooledSize < DirectSize;
+  if (!SchedTables.UseSchedClassPool) {
+    SchedTables.SchedClassPool.clear();
+    SchedTables.ProcSchedClassIndices.clear();
+  }
+}
+
 // Emit SchedClass tables for all processors and associated global tables.
 void SubtargetEmitter::emitSchedClassTables(SchedClassTables &SchedTables,
                                             raw_ostream &OS) {
@@ -1389,6 +1455,9 @@ void SubtargetEmitter::emitSchedClassTables(SchedClassTables &SchedTables,
   StringToOffsetTable StrTab;
   unsigned InvalidNameOff = StrTab.GetOrAddStringOffset("InvalidSchedClass");
 
+  if (SchedTables.UseSchedClassPool)
+    OS << "\n#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)\n";
+
   // Emit a SchedClass table for each processor.
   for (const auto &[Idx, Proc] : enumerate(SchedModels.procModels())) {
     if (!Proc.hasInstrSchedModel())
@@ -1430,10 +1499,53 @@ void SubtargetEmitter::emitSchedClassTables(SchedClassTables &SchedTables,
     OS << "}; // " << Proc.ModelName << "SchedClasses\n";
   }
 
+  if (SchedTables.UseSchedClassPool) {
+    OS << "#else\n";
+    OS << "\n// {NumMicroOps, BeginGroup, EndGroup, RetireOOO,"
+       << " WriteProcResIdx,#, WriteLatencyIdx,#, ReadAdvanceIdx,#}\n";
+    OS << "static const llvm::MCSchedClassDesc " << Target
+       << "SchedClassPool[] = {\n";
+    for (const auto &[PoolIdx, MCDesc] :
+         enumerate(SchedTables.SchedClassPool)) {
+      OS << "  {DBGFIELD(0) " << MCDesc.NumMicroOps << ", "
+         << (MCDesc.BeginGroup ? "true" : "false") << ", "
+         << (MCDesc.EndGroup ? "true" : "false") << ", "
+         << (MCDesc.RetireOOO ? "true" : "false") << ", "
+         << format("%2d", MCDesc.WriteProcResIdx) << ", "
+         << MCDesc.NumWriteProcResEntries << ", "
+         << format("%2d", MCDesc.WriteLatencyIdx) << ", "
+         << MCDesc.NumWriteLatencyEntries << ", "
+         << format("%2d", MCDesc.ReadAdvanceIdx) << ", "
+         << MCDesc.NumReadAdvanceEntries << "}, // #" << PoolIdx << '\n';
+    }
+    OS << "}; // " << Target << "SchedClassPool\n";
+    OS << "static_assert(sizeof(" << Target << "SchedClassPool) / sizeof("
+       << Target << "SchedClassPool[0]) <= (1ULL << 16));\n";
+
+    for (const auto &[Idx, Proc] : enumerate(SchedModels.procModels())) {
+      if (!Proc.hasInstrSchedModel())
+        continue;
+
+      ArrayRef<uint16_t> Indices = SchedTables.ProcSchedClassIndices[1 + Idx];
+      OS << "\nstatic const uint16_t " << Proc.ModelName
+         << "SchedClassIndices[] = {\n";
+      for (const auto [SCIdx, PoolIdx] : enumerate(Indices))
+        OS << "  " << PoolIdx << ", // #" << SCIdx << '\n';
+      OS << "}; // " << Proc.ModelName << "SchedClassIndices\n";
+      OS << "static_assert(sizeof(" << Proc.ModelName
+         << "SchedClassIndices[0]) == 2);\n";
+      OS << "static_assert(sizeof(" << Proc.ModelName
+         << "SchedClassIndices) / sizeof(" << Proc.ModelName
+         << "SchedClassIndices[0]) == " << Indices.size() << ");\n";
+    }
+    OS << "#endif\n";
+  }
+
   StrTab.EmitStringTableDef(OS, Target + "SchedClassNames");
 }
 
-void SubtargetEmitter::emitProcessorModels(raw_ostream &OS) {
+void SubtargetEmitter::emitProcessorModels(const SchedClassTables &SchedTables,
+                                           raw_ostream &OS) {
   // For each processor model.
   for (const CodeGenProcModel &PM : SchedModels.procModels()) {
     // Emit extra processor info if available.
@@ -1477,12 +1589,24 @@ void SubtargetEmitter::emitProcessorModels(raw_ostream &OS) {
 
     OS << "  " << PM.Index << ", // Processor ID\n";
     if (PM.hasInstrSchedModel())
-      OS << "  " << PM.ModelName << "ProcResources" << ",\n"
+      OS << "  " << PM.ModelName << "ProcResources" << ",\n";
+    else
+      OS << "  nullptr,\n";
+    if (PM.hasInstrSchedModel() && SchedTables.UseSchedClassPool)
+      OS << "#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)\n"
          << "  " << PM.ModelName << "SchedClasses" << ",\n"
-         << "  " << PM.ProcResourceDefs.size() + 1 << ",\n"
+         << "#else\n"
+         << "  " << Target << "SchedClassPool,\n"
+         << "#endif\n";
+    else if (PM.hasInstrSchedModel())
+      OS << "  " << PM.ModelName << "SchedClasses" << ",\n";
+    else
+      OS << "  nullptr,\n";
+    if (PM.hasInstrSchedModel())
+      OS << "  " << PM.ProcResourceDefs.size() + 1 << ",\n"
          << "  " << SchedModels.schedClasses().size() << ",\n";
     else
-      OS << "  nullptr, nullptr, 0, 0,"
+      OS << "  0, 0,"
          << " // No instruction-level machine model.\n";
     OS << "  DBGVAL_OR_NULLPTR(&" << Target
        << "SchedClassNames), // SchedClassNames\n";
@@ -1493,7 +1617,15 @@ void SubtargetEmitter::emitProcessorModels(raw_ostream &OS) {
     if (PM.hasExtraProcessorInfo())
       OS << "  &" << PM.ModelName << "ExtraInfo,\n";
     else
-      OS << "  nullptr // No extra processor descriptor\n";
+      OS << "  nullptr, // No extra processor descriptor\n";
+    if (PM.hasInstrSchedModel() && SchedTables.UseSchedClassPool)
+      OS << "#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)\n"
+         << "  nullptr,\n"
+         << "#else\n"
+         << "  " << PM.ModelName << "SchedClassIndices,\n"
+         << "#endif\n";
+    else
+      OS << "  nullptr,\n";
     OS << "};\n";
   }
 }
@@ -1526,10 +1658,11 @@ void SubtargetEmitter::emitSchedModel(raw_ostream &OS) {
   for (const CodeGenProcModel &ProcModel : SchedModels.procModels()) {
     genSchedClassTables(ProcModel, SchedTables);
   }
+  buildSchedClassPool(SchedTables);
   emitSchedClassTables(SchedTables, OS);
 
   // Emit the processor machine model
-  emitProcessorModels(OS);
+  emitProcessorModels(SchedTables, OS);
 
   OS << "\n#undef DBGFIELD\n";
   OS << "\n#undef DBGVAL_OR_NULLPTR\n";



More information about the llvm-commits mailing list