[llvm] [IR] Add persistent metadata IDs for faster debug printing (PR #216838)

Yaxun Liu via llvm-commits llvm-commits at lists.llvm.org
Tue Aug 18 19:48:15 PDT 2026


https://github.com/yxsamliu updated https://github.com/llvm/llvm-project/pull/216838

>From 5e9e8930aea9bb156c3d85b1fc6448eb41098bb5 Mon Sep 17 00:00:00 2001
From: "Yaxun (Sam) Liu" <yaxun.liu at amd.com>
Date: Tue, 18 Aug 2026 11:06:55 -0400
Subject: [PATCH 1/2] [IR] Add persistent metadata IDs for fast printing

Debugging a pass pipeline often prints the same functions after many
passes. Before printing one function, the normal IR printer scans module
metadata to assign canonical slots. On large modules, this setup can take
more time than writing the function itself.

Add `-print-ir-fast`, a hidden option for debugging that keeps canonical
numbering for values and attributes but skips metadata enumeration.
Metadata IDs are assigned as nodes are printed and stored in the LLVM
context, so the same node keeps its ID across snapshots. Tracking
references prevent a recycled metadata address from reusing an old ID.

The fast path keeps instruction metadata attachments. Final module
output continues to use canonical compact numbering, so `Module::print`
and final textual IR are unchanged.

On a 4.8 MB IR file, repeated function printing is 2.4x faster and
`-print-changed=quiet` with instsimplify is 7.8x faster.
---
 llvm/lib/IR/AsmWriter.cpp                     | 91 ++++++++++++++-----
 llvm/lib/IR/LLVMContextImpl.h                 | 17 ++++
 .../ChangePrinters/print-changed-diff.ll      |  1 +
 llvm/test/Other/print-changed-fast.ll         | 41 +++++++++
 llvm/test/Other/print-ir-fast.ll              | 82 +++++++++++++++++
 llvm/unittests/IR/AsmWriterTest.cpp           | 59 ++++++++++++
 6 files changed, 267 insertions(+), 24 deletions(-)
 create mode 100644 llvm/test/Other/print-changed-fast.ll
 create mode 100644 llvm/test/Other/print-ir-fast.ll

diff --git a/llvm/lib/IR/AsmWriter.cpp b/llvm/lib/IR/AsmWriter.cpp
index c3202eea12c28..ab74cdc69d749 100644
--- a/llvm/lib/IR/AsmWriter.cpp
+++ b/llvm/lib/IR/AsmWriter.cpp
@@ -15,6 +15,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "LLVMContextImpl.h"
 #include "llvm/ADT/APFloat.h"
 #include "llvm/ADT/APInt.h"
 #include "llvm/ADT/ArrayRef.h"
@@ -111,6 +112,11 @@ static cl::opt<bool> PrintAddrspaceName("print-addrspace-name", cl::Hidden,
                                         cl::init(false),
                                         cl::desc("Print address space names"));
 
+static cl::opt<bool>
+    PrintIRFast("print-ir-fast",
+                cl::desc("Use persistent metadata IDs for IR debugging"),
+                cl::init(false), cl::Hidden);
+
 // Make virtual table appear in this compilation unit.
 AssemblyAnnotationWriter::~AssemblyAnnotationWriter() = default;
 
@@ -800,6 +806,7 @@ class llvm::SlotTracker : public AbstractSlotTrackerStorage {
   const Function* TheFunction = nullptr;
   bool FunctionProcessed = false;
   bool ShouldInitializeAllMetadata;
+  bool UsePersistentMetadataIDs;
 
   std::function<void(AbstractSlotTrackerStorage *, const Module *, bool)>
       ProcessModuleHookFn;
@@ -856,8 +863,11 @@ class llvm::SlotTracker : public AbstractSlotTrackerStorage {
   /// If \c ShouldInitializeAllMetadata, initializes all metadata in all
   /// functions, giving correct numbering for metadata referenced only from
   /// within a function (even if no functions have been initialized).
+  /// If \c UsePersistentMetadataIDs, skips metadata enumeration and uses IDs
+  /// stored in the LLVM context.
   explicit SlotTracker(const Function *F,
-                       bool ShouldInitializeAllMetadata = false);
+                       bool ShouldInitializeAllMetadata = false,
+                       bool UsePersistentMetadataIDs = false);
 
   /// Construct from a module summary index.
   explicit SlotTracker(const ModuleSummaryIndex *Index);
@@ -1057,16 +1067,23 @@ static SlotTracker *createSlotTracker(const Value *V) {
 // Module level constructor. Causes the contents of the Module (sans functions)
 // to be added to the slot table.
 SlotTracker::SlotTracker(const Module *M, bool ShouldInitializeAllMetadata)
-    : TheModule(M), ShouldInitializeAllMetadata(ShouldInitializeAllMetadata) {}
+    : TheModule(M), ShouldInitializeAllMetadata(ShouldInitializeAllMetadata),
+      UsePersistentMetadataIDs(false) {}
 
 // Function level constructor. Causes the contents of the Module and the one
 // function provided to be added to the slot table.
-SlotTracker::SlotTracker(const Function *F, bool ShouldInitializeAllMetadata)
+SlotTracker::SlotTracker(const Function *F, bool ShouldInitializeAllMetadata,
+                         bool UsePersistentMetadataIDs)
     : TheModule(F ? F->getParent() : nullptr), TheFunction(F),
-      ShouldInitializeAllMetadata(ShouldInitializeAllMetadata) {}
+      ShouldInitializeAllMetadata(ShouldInitializeAllMetadata),
+      UsePersistentMetadataIDs(UsePersistentMetadataIDs) {
+  assert((!ShouldInitializeAllMetadata || !UsePersistentMetadataIDs) &&
+         "persistent metadata IDs cannot preserve module metadata numbering");
+}
 
 SlotTracker::SlotTracker(const ModuleSummaryIndex *Index)
-    : TheModule(nullptr), ShouldInitializeAllMetadata(false), TheIndex(Index) {}
+    : TheModule(nullptr), ShouldInitializeAllMetadata(false),
+      UsePersistentMetadataIDs(false), TheIndex(Index) {}
 
 inline void SlotTracker::initializeIfNeeded() {
   if (TheModule) {
@@ -1095,7 +1112,8 @@ void SlotTracker::processModule() {
   for (const GlobalVariable &Var : TheModule->globals()) {
     if (!Var.hasName())
       CreateModuleSlot(&Var);
-    processGlobalObjectMetadata(Var);
+    if (!UsePersistentMetadataIDs)
+      processGlobalObjectMetadata(Var);
     auto Attrs = Var.getAttributes();
     if (Attrs.hasAttributes())
       CreateAttributeSetSlot(Attrs);
@@ -1109,21 +1127,22 @@ void SlotTracker::processModule() {
   for (const GlobalIFunc &I : TheModule->ifuncs()) {
     if (!I.hasName())
       CreateModuleSlot(&I);
-    processGlobalObjectMetadata(I);
+    if (!UsePersistentMetadataIDs)
+      processGlobalObjectMetadata(I);
   }
 
   // Add metadata used by named metadata.
-  for (const NamedMDNode &NMD : TheModule->named_metadata()) {
-    for (const MDNode *N : NMD.operands())
-      CreateMetadataSlot(N);
-  }
+  if (!UsePersistentMetadataIDs)
+    for (const NamedMDNode &NMD : TheModule->named_metadata())
+      for (const MDNode *N : NMD.operands())
+        CreateMetadataSlot(N);
 
   for (const Function &F : *TheModule) {
     if (!F.hasName())
       // Add all the unnamed functions to the table.
       CreateModuleSlot(&F);
 
-    if (ShouldInitializeAllMetadata)
+    if (ShouldInitializeAllMetadata && !UsePersistentMetadataIDs)
       processFunctionMetadata(F);
 
     // Add all the function attributes to the table.
@@ -1145,7 +1164,7 @@ void SlotTracker::processFunction() {
   fNext = 0;
 
   // Process function metadata if it wasn't hit at the module-level.
-  if (!ShouldInitializeAllMetadata)
+  if (!ShouldInitializeAllMetadata && !UsePersistentMetadataIDs)
     processFunctionMetadata(*TheFunction);
 
   // Add all the function arguments with no names.
@@ -1325,6 +1344,12 @@ int SlotTracker::getMetadataSlot(const MDNode *N) {
   // Check for uninitialized state and do lazy initialization.
   initializeIfNeeded();
 
+  if (UsePersistentMetadataIDs) {
+    if (isa<DIExpression>(N))
+      return -1;
+    return N->getContext().pImpl->getOrCreateMetadataPrintID(N);
+  }
+
   // Find the MDNode in the module map
   mdn_iterator MI = mdnMap.find(N);
   return MI == mdnMap.end() ? -1 : (int)MI->second;
@@ -2987,13 +3012,7 @@ AssemblyWriter::AssemblyWriter(formatted_raw_ostream &o, SlotTracker &Mac,
       ShouldPreserveUseListOrder(
           PreserveAssemblyUseListOrder.getNumOccurrences()
               ? PreserveAssemblyUseListOrder
-              : ShouldPreserveUseListOrder) {
-  if (!TheModule)
-    return;
-  for (const GlobalObject &GO : TheModule->global_objects())
-    if (const Comdat *C = GO.getComdat())
-      Comdats.insert(C);
-}
+              : ShouldPreserveUseListOrder) {}
 
 AssemblyWriter::AssemblyWriter(formatted_raw_ostream &o, SlotTracker &Mac,
                                const ModuleSummaryIndex *Index, bool IsForDebug)
@@ -3105,6 +3124,10 @@ void AssemblyWriter::writeOperandBundles(const CallBase *Call) {
 void AssemblyWriter::printModule(const Module *M) {
   Machine.initializeIfNeeded();
 
+  for (const GlobalObject &GO : M->global_objects())
+    if (const Comdat *C = GO.getComdat())
+      Comdats.insert(C);
+
   if (ShouldPreserveUseListOrder)
     UseListOrders = predictUseListOrder(M);
 
@@ -4892,7 +4915,6 @@ void AssemblyWriter::printInstruction(const Instruction &I) {
     printShuffleMask(Out, SVI->getType(), SVI->getShuffleMask());
   }
 
-  // Print Metadata info.
   SmallVector<std::pair<unsigned, MDNode *>, 4> InstMD;
   I.getAllMetadata(InstMD);
   printMetadataAttachments(InstMD, ", ");
@@ -5099,6 +5121,15 @@ void AssemblyWriter::printUseLists(const Function *F) {
 
 void Function::print(raw_ostream &ROS, AssemblyAnnotationWriter *AAW,
                      bool ShouldPreserveUseListOrder, bool IsForDebug) const {
+  if (PrintIRFast) {
+    SlotTracker SlotTable(this, /*ShouldInitializeAllMetadata=*/false,
+                          /*UsePersistentMetadataIDs=*/true);
+    formatted_raw_ostream OS(ROS);
+    AssemblyWriter W(OS, SlotTable, this->getParent(), AAW, IsForDebug,
+                     ShouldPreserveUseListOrder);
+    W.printFunction(this);
+    return;
+  }
   SlotTracker SlotTable(this->getParent());
   formatted_raw_ostream OS(ROS);
   AssemblyWriter W(OS, SlotTable, this->getParent(), AAW, IsForDebug,
@@ -5109,10 +5140,11 @@ void Function::print(raw_ostream &ROS, AssemblyAnnotationWriter *AAW,
 void BasicBlock::print(raw_ostream &ROS, AssemblyAnnotationWriter *AAW,
                      bool ShouldPreserveUseListOrder,
                      bool IsForDebug) const {
-  SlotTracker SlotTable(this->getParent());
+  SlotTracker SlotTable(this->getParent(),
+                        /*ShouldInitializeAllMetadata=*/false,
+                        /*UsePersistentMetadataIDs=*/PrintIRFast);
   formatted_raw_ostream OS(ROS);
-  AssemblyWriter W(OS, SlotTable, this->getModule(), AAW,
-                   IsForDebug,
+  AssemblyWriter W(OS, SlotTable, this->getModule(), AAW, IsForDebug,
                    ShouldPreserveUseListOrder);
   W.printBasicBlock(this);
 }
@@ -5262,6 +5294,17 @@ void DbgLabelRecord::print(raw_ostream &ROS, ModuleSlotTracker &MST,
 }
 
 void Value::print(raw_ostream &ROS, bool IsForDebug) const {
+  if (PrintIRFast) {
+    if (const auto *F = dyn_cast<Function>(this)) {
+      F->print(ROS, nullptr, /*ShouldPreserveUseListOrder=*/false, IsForDebug);
+      return;
+    }
+    if (const auto *BB = dyn_cast<BasicBlock>(this)) {
+      BB->print(ROS, nullptr, /*ShouldPreserveUseListOrder=*/false, IsForDebug);
+      return;
+    }
+  }
+
   bool ShouldInitializeAllMetadata = false;
   if (auto *I = dyn_cast<Instruction>(this))
     ShouldInitializeAllMetadata = isReferencingMDNode(*I);
diff --git a/llvm/lib/IR/LLVMContextImpl.h b/llvm/lib/IR/LLVMContextImpl.h
index 41c8a92c56eda..952a1c76cb5ce 100644
--- a/llvm/lib/IR/LLVMContextImpl.h
+++ b/llvm/lib/IR/LLVMContextImpl.h
@@ -1611,6 +1611,23 @@ class LLVMContextImpl {
   DenseMap<Metadata *, MetadataAsValue *> MetadataAsValues;
   DenseSet<DIArgList *, DIArgListInfo> DIArgLists;
 
+  // IDs remain stable for the context lifetime. Tracking references prevent a
+  // recycled MDNode address from reusing an old ID; slots are not reclaimed.
+  DenseMap<const MDNode *, unsigned> MetadataPrintIDs;
+  SmallVector<TrackingMDNodeRef, 0> MetadataPrintNodes;
+
+  unsigned getOrCreateMetadataPrintID(const MDNode *N) {
+    auto It = MetadataPrintIDs.find(N);
+    if (It != MetadataPrintIDs.end() &&
+        MetadataPrintNodes[It->second].get() == N)
+      return It->second;
+
+    unsigned ID = MetadataPrintNodes.size();
+    MetadataPrintNodes.emplace_back(const_cast<MDNode *>(N));
+    MetadataPrintIDs[N] = ID;
+    return ID;
+  }
+
 #define HANDLE_MDNODE_LEAF_UNIQUABLE(CLASS)                                    \
   DenseSet<CLASS *, CLASS##Info> CLASS##s;
 #include "llvm/IR/Metadata.def"
diff --git a/llvm/test/Other/ChangePrinters/print-changed-diff.ll b/llvm/test/Other/ChangePrinters/print-changed-diff.ll
index 6b3644f4c8247..a404978b203a3 100644
--- a/llvm/test/Other/ChangePrinters/print-changed-diff.ll
+++ b/llvm/test/Other/ChangePrinters/print-changed-diff.ll
@@ -40,6 +40,7 @@
 ; Check that only the passes that change the IR are printed and that the
 ; others (including g) are filtered out.
 ; RUN: opt -S -print-changed=diff-quiet -passes=instsimplify -filter-print-funcs=f  2>&1 -o /dev/null < %s | FileCheck %s --check-prefix=CHECK-DIFF-QUIET-FUNC-FILTER
+; RUN: opt -print-changed=diff-quiet -print-ir-fast -passes=instsimplify -filter-print-funcs=f -disable-output < %s 2>&1 | FileCheck %s --check-prefix=CHECK-DIFF-QUIET-FUNC-FILTER
 ;
 ; Check that the reporting of IRs respects is not affected by
 ; -print-module-scope
diff --git a/llvm/test/Other/print-changed-fast.ll b/llvm/test/Other/print-changed-fast.ll
new file mode 100644
index 0000000000000..41882b3983741
--- /dev/null
+++ b/llvm/test/Other/print-changed-fast.ll
@@ -0,0 +1,41 @@
+; RUN: opt -passes=instsimplify -filter-print-funcs=second \
+; RUN:   -print-changed=quiet -print-ir-fast -disable-output < %s 2>&1 | FileCheck %s --check-prefix=FAST
+; RUN: opt -passes=instsimplify -filter-print-funcs=second \
+; RUN:   -print-before=instsimplify -print-after=instsimplify -print-ir-fast \
+; RUN:   -disable-output < %s 2>&1 | FileCheck %s --check-prefix=STABLE
+
+declare i32 @opaque(i32)
+
+define i32 @first(i32 %arg) #0 {
+  %keep = call i32 @opaque(i32 %arg), !annotation !0
+  ret i32 %keep
+}
+
+define i32 @second(i32 %arg) #1 {
+  %constant = add i32 2, 3, !annotation !0
+  %keep = call i32 @opaque(i32 %arg), !annotation !1, !other !3
+  %result = add i32 %keep, %constant
+  ret i32 %result
+}
+
+!0 = !{!"first metadata"}
+!1 = !{!2}
+!2 = !{!"second metadata"}
+!3 = !{!"other metadata"}
+
+attributes #0 = { nounwind }
+attributes #1 = { noinline }
+
+; FAST: *** IR Dump After InstSimplifyPass on second ***
+; FAST: define i32 @second(i32 %arg) #1 {
+; FAST: %keep = call i32 @opaque(i32 %arg)
+; FAST-SAME: !annotation ![[ANNOTATION:[0-9]+]], !other ![[OTHER:[0-9]+]]
+
+; STABLE: *** IR Dump Before InstSimplifyPass on second ***
+; STABLE: %constant = add i32 2, 3, !annotation !{{[0-9]+}}
+; STABLE: %keep = call i32 @opaque(i32 %arg)
+; STABLE-SAME: !annotation ![[STABLE_ANNOTATION:[0-9]+]], !other ![[STABLE_OTHER:[0-9]+]]
+; STABLE: *** IR Dump After InstSimplifyPass on second ***
+; STABLE-NOT: %constant
+; STABLE: %keep = call i32 @opaque(i32 %arg)
+; STABLE-SAME: !annotation ![[STABLE_ANNOTATION]], !other ![[STABLE_OTHER]]
diff --git a/llvm/test/Other/print-ir-fast.ll b/llvm/test/Other/print-ir-fast.ll
new file mode 100644
index 0000000000000..6915213a504a7
--- /dev/null
+++ b/llvm/test/Other/print-ir-fast.ll
@@ -0,0 +1,82 @@
+; RUN: opt -S -passes=no-op-module < %s > %t.normal
+; RUN: opt -S -passes=no-op-module -print-ir-fast < %s > %t.fast
+; RUN: diff %t.normal %t.fast
+; RUN: FileCheck %s --check-prefix=NORMAL < %t.fast
+; RUN: opt -disable-output -passes=print -print-ir-fast < %s 2>&1 | FileCheck %s --check-prefix=NORMAL
+; RUN: opt -disable-output -passes='function(print)' -filter-print-funcs=second \
+; RUN:   -print-ir-fast < %s 2>&1 | FileCheck %s --check-prefix=FAST-FUNCTION
+; RUN: opt -disable-output -passes='function(no-op-function)' \
+; RUN:   -print-before=no-op-function -filter-print-funcs=second \
+; RUN:   -print-ir-fast < %s 2>&1 | FileCheck %s --check-prefix=FAST-FUNCTION
+; RUN: opt -disable-output -passes='function(no-op-function)' -print-after-all \
+; RUN:   -filter-print-funcs=second -print-ir-fast < %s 2>&1 | FileCheck %s --check-prefix=FAST-FUNCTION
+; RUN: opt -disable-output -passes=no-op-module -print-before=no-op-module \
+; RUN:   -filter-print-funcs=first,second -print-ir-fast < %s 2>&1 | FileCheck %s --check-prefix=FAST-MULTI
+; RUN: opt -disable-output -passes='loop(no-op-loop)' -print-before=no-op-loop \
+; RUN:   -filter-print-funcs=loop -print-ir-fast < %s 2>&1 | FileCheck %s --check-prefix=FAST-LOOP
+$group = comdat any
+
+ at named = global ptr @0, comdat($group), !annotation !5
+ at 0 = global i32 0
+ at 1 = global i32 1
+
+declare void @callee(ptr)
+
+define void @first() #0 {
+  call void @callee(ptr @0) #1
+  call void @callee(ptr @1) #1
+  ret void, !annotation !1
+}
+
+define void @second() {
+  call void @callee(ptr @1) #1, !annotation !3
+  ret void, !annotation !3
+}
+
+define void @loop() {
+entry:
+  call void @callee(ptr @0) #1
+  call void @callee(ptr @1) #1
+  br label %loop
+
+loop:
+  call void @callee(ptr @1) #1
+  br i1 false, label %loop, label %exit
+
+exit:
+  ret void
+}
+
+attributes #0 = { noinline }
+attributes #1 = { nounwind }
+
+!named = !{!0}
+!0 = !{!"named metadata"}
+!1 = !{!2}
+!2 = !{!"first metadata"}
+!3 = !{!4}
+!4 = !{!"second metadata"}
+!5 = !{!6}
+!6 = !{!"global metadata"}
+
+; NORMAL: @named = global ptr @0, comdat($group), !annotation ![[NORMAL_GLOBAL:[0-9]+]]
+; NORMAL: ret void, !annotation ![[NORMAL_FIRST:[0-9]+]]
+; NORMAL: call void @callee(ptr @1) #1, !annotation ![[NORMAL_SECOND:[0-9]+]]
+; NORMAL: ret void, !annotation ![[NORMAL_SECOND]]
+; NORMAL: !named = !{![[NORMAL_NAMED:[0-9]+]]}
+
+; FAST-FUNCTION: define void @second() {
+; FAST-FUNCTION: call void @callee(ptr @1) #1, !annotation ![[SECOND:[0-9]+]]
+; FAST-FUNCTION: ret void, !annotation ![[SECOND]]
+
+; FAST-MULTI: define void @first() #0 {
+; FAST-MULTI: call void @callee(ptr @0) #1
+; FAST-MULTI: call void @callee(ptr @1) #1
+; FAST-MULTI: define void @second() {
+; FAST-MULTI: call void @callee(ptr @1) #1
+
+; FAST-LOOP: ; Preheader:
+; FAST-LOOP: call void @callee(ptr @0) #1
+; FAST-LOOP: call void @callee(ptr @1) #1
+; FAST-LOOP: ; Loop:
+; FAST-LOOP: call void @callee(ptr @1) #1
diff --git a/llvm/unittests/IR/AsmWriterTest.cpp b/llvm/unittests/IR/AsmWriterTest.cpp
index 75305f4e2dea4..550cc7ffae233 100644
--- a/llvm/unittests/IR/AsmWriterTest.cpp
+++ b/llvm/unittests/IR/AsmWriterTest.cpp
@@ -5,6 +5,8 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 //===----------------------------------------------------------------------===//
+#include "llvm/ADT/ScopeExit.h"
+#include "llvm/AsmParser/Parser.h"
 #include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/IR/DebugInfoMetadata.h"
 #include "llvm/IR/Function.h"
@@ -12,6 +14,8 @@
 #include "llvm/IR/LLVMContext.h"
 #include "llvm/IR/MDBuilder.h"
 #include "llvm/IR/Module.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/SourceMgr.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 
@@ -62,6 +66,61 @@ TEST(AsmWriterTest, DumpDIExpression) {
   EXPECT_EQ("!DIExpression(DW_OP_constu, 4, DW_OP_minus, DW_OP_deref)", S);
 }
 
+TEST(AsmWriterTest, FastBasicBlockPrint) {
+  LLVMContext Ctx;
+  SMDiagnostic Err;
+  std::unique_ptr<Module> M = parseAssemblyString(R"(
+    @0 = global i32 0
+
+    declare void @use(ptr)
+
+    define void @f() !dbg !6 {
+      call void @use(ptr @0), !annotation !12
+        #dbg_value(i32 0, !9, !DIExpression(DW_OP_constu, 4), !11)
+      ret void, !annotation !12
+    }
+
+    !llvm.dbg.cu = !{!0}
+    !llvm.module.flags = !{!5}
+
+    !0 = distinct !DICompileUnit(language: DW_LANG_C, file: !1, producer: "test", isOptimized: true, runtimeVersion: 0, emissionKind: FullDebug, enums: !2)
+    !1 = !DIFile(filename: "t.ll", directory: "/")
+    !2 = !{}
+    !5 = !{i32 2, !"Debug Info Version", i32 3}
+    !6 = distinct !DISubprogram(name: "f", scope: null, file: !1, line: 1, type: !7, scopeLine: 1, spFlags: DISPFlagDefinition | DISPFlagOptimized, unit: !0, retainedNodes: !8)
+    !7 = !DISubroutineType(types: !2)
+    !8 = !{!9}
+    !9 = !DILocalVariable(name: "x", scope: !6, file: !1, line: 1, type: !10)
+    !10 = !DIBasicType(name: "i32", size: 32, encoding: DW_ATE_unsigned)
+    !11 = !DILocation(line: 1, column: 1, scope: !6)
+    !12 = !{!"annotation"}
+  )",
+                                                  Err, Ctx);
+  ASSERT_NE(M, nullptr);
+
+  cl::Option *PrintIRFastOption =
+      cl::getRegisteredOptions().lookup("print-ir-fast");
+  ASSERT_NE(PrintIRFastOption, nullptr);
+  PrintIRFastOption->reset();
+  ASSERT_FALSE(
+      PrintIRFastOption->addOccurrence(0, "print-ir-fast", StringRef()));
+  scope_exit ResetOption([&] { PrintIRFastOption->reset(); });
+
+  std::string First;
+  raw_string_ostream FirstOS(First);
+  M->getFunction("f")->getEntryBlock().print(FirstOS);
+
+  std::string Second;
+  raw_string_ostream SecondOS(Second);
+  M->getFunction("f")->getEntryBlock().print(SecondOS);
+
+  EXPECT_EQ(First, Second);
+  EXPECT_THAT(First, HasSubstr("call void @use(ptr @0), !annotation !"));
+  EXPECT_THAT(First, HasSubstr("#dbg_value(i32 0, !"));
+  EXPECT_THAT(First, HasSubstr("!DIExpression(DW_OP_constu, 4)"));
+  EXPECT_THAT(First, HasSubstr("ret void, !annotation !"));
+}
+
 TEST(AsmWriterTest, PrintAddrspaceWithNullOperand) {
   LLVMContext Ctx;
   Module M("test module", Ctx);

>From e7d10a9cfe5acc261a14b8535d79948d98da4d80 Mon Sep 17 00:00:00 2001
From: "Yaxun (Sam) Liu" <yaxun.liu at amd.com>
Date: Tue, 18 Aug 2026 19:08:32 -0400
Subject: [PATCH 2/2] [IR] Assign persistent metadata IDs eagerly

Persistent metadata IDs assigned on first print depend on print order and
need a context map plus tracking references.

Store IDs in spare MDNode header bits and assign them when non-temporary
nodes are created. Temporary nodes receive an ID when made permanent. This
keeps fast debug-print numbering independent of print order without growing
MDNode.
---
 llvm/include/llvm/IR/Metadata.h     |  5 ++++-
 llvm/lib/IR/LLVMContextImpl.h       | 22 ++++++++-----------
 llvm/lib/IR/Metadata.cpp            |  8 +++++++
 llvm/unittests/IR/AsmWriterTest.cpp | 34 +++++++++++++++++++++++++++++
 4 files changed, 55 insertions(+), 14 deletions(-)

diff --git a/llvm/include/llvm/IR/Metadata.h b/llvm/include/llvm/IR/Metadata.h
index 5b458fa14f0b1..a85256a2a543b 100644
--- a/llvm/include/llvm/IR/Metadata.h
+++ b/llvm/include/llvm/IR/Metadata.h
@@ -1083,7 +1083,10 @@ class MDNode : public Metadata {
     size_t IsLarge : 1;
     size_t SmallSize : 4;
     size_t SmallNumOps : 4;
-    size_t : sizeof(size_t) * CHAR_BIT - 10;
+    // Zero means unassigned; stored IDs are one greater than printed IDs.
+    size_t MetadataPrintID : sizeof(size_t) * CHAR_BIT - 10;
+
+    static constexpr size_t MaxMetadataPrintID = size_t(-1) >> 10;
 
     unsigned NumUnresolved = 0;
     using LargeStorageVector = SmallVector<MDOperand, 0>;
diff --git a/llvm/lib/IR/LLVMContextImpl.h b/llvm/lib/IR/LLVMContextImpl.h
index 952a1c76cb5ce..ff2e7a07fd11b 100644
--- a/llvm/lib/IR/LLVMContextImpl.h
+++ b/llvm/lib/IR/LLVMContextImpl.h
@@ -1611,21 +1611,17 @@ class LLVMContextImpl {
   DenseMap<Metadata *, MetadataAsValue *> MetadataAsValues;
   DenseSet<DIArgList *, DIArgListInfo> DIArgLists;
 
-  // IDs remain stable for the context lifetime. Tracking references prevent a
-  // recycled MDNode address from reusing an old ID; slots are not reclaimed.
-  DenseMap<const MDNode *, unsigned> MetadataPrintIDs;
-  SmallVector<TrackingMDNodeRef, 0> MetadataPrintNodes;
+  size_t NextMetadataPrintID = 0;
 
   unsigned getOrCreateMetadataPrintID(const MDNode *N) {
-    auto It = MetadataPrintIDs.find(N);
-    if (It != MetadataPrintIDs.end() &&
-        MetadataPrintNodes[It->second].get() == N)
-      return It->second;
-
-    unsigned ID = MetadataPrintNodes.size();
-    MetadataPrintNodes.emplace_back(const_cast<MDNode *>(N));
-    MetadataPrintIDs[N] = ID;
-    return ID;
+    size_t ID = N->getHeader().MetadataPrintID;
+    if (!ID) {
+      assert(NextMetadataPrintID < MDNode::Header::MaxMetadataPrintID &&
+             "too many metadata nodes");
+      ID = ++NextMetadataPrintID;
+      const_cast<MDNode *>(N)->getHeader().MetadataPrintID = ID;
+    }
+    return ID - 1;
   }
 
 #define HANDLE_MDNODE_LEAF_UNIQUABLE(CLASS)                                    \
diff --git a/llvm/lib/IR/Metadata.cpp b/llvm/lib/IR/Metadata.cpp
index 0a4141ee2362e..dc808d685651a 100644
--- a/llvm/lib/IR/Metadata.cpp
+++ b/llvm/lib/IR/Metadata.cpp
@@ -650,6 +650,8 @@ StringRef MDString::getString() const {
 void *MDNode::operator new(size_t Size, size_t NumOps, StorageType Storage) {
   // uint64_t is the most aligned type we need support (ensured by static_assert
   // above)
+  static_assert(sizeof(Header) == 2 * sizeof(size_t),
+                "MDNode header fields poorly packed");
   size_t AllocSize =
       alignTo(Header::getAllocSize(Storage, NumOps), alignof(uint64_t));
   char *Mem = reinterpret_cast<char *>(::operator new(AllocSize + Size));
@@ -667,6 +669,9 @@ void MDNode::operator delete(void *N) {
 MDNode::MDNode(LLVMContext &Context, unsigned ID, StorageType Storage,
                ArrayRef<Metadata *> Ops1, ArrayRef<Metadata *> Ops2)
     : Metadata(ID, Storage), Context(Context) {
+  if (!isTemporary())
+    Context.pImpl->getOrCreateMetadataPrintID(this);
+
   unsigned Op = 0;
   for (Metadata *MD : Ops1)
     setOperand(Op++, MD);
@@ -696,6 +701,7 @@ MDNode::Header::Header(size_t NumOps, StorageType Storage) {
   IsLarge = isLarge(NumOps);
   IsResizable = isResizable(Storage);
   SmallSize = getSmallSize(NumOps, IsResizable, IsLarge);
+  MetadataPrintID = 0;
   if (IsLarge) {
     SmallNumOps = 0;
     new (getLargePtr()) LargeStorageVector();
@@ -788,6 +794,7 @@ void MDNode::makeUniqued() {
 
   // Make this 'uniqued'.
   Storage = Uniqued;
+  getContext().pImpl->getOrCreateMetadataPrintID(this);
   countUnresolvedOperands();
   if (!getNumUnresolved()) {
     dropReplaceableUses();
@@ -1067,6 +1074,7 @@ void MDNode::storeDistinctInContext() {
   assert(!Context.hasReplaceableUses() && "Unexpected replaceable uses");
   assert(!getNumUnresolved() && "Unexpected unresolved nodes");
   Storage = Distinct;
+  getContext().pImpl->getOrCreateMetadataPrintID(this);
   assert(isResolved() && "Expected this to be resolved");
 
   // Reset the hash.
diff --git a/llvm/unittests/IR/AsmWriterTest.cpp b/llvm/unittests/IR/AsmWriterTest.cpp
index 550cc7ffae233..23d5ddeb0933a 100644
--- a/llvm/unittests/IR/AsmWriterTest.cpp
+++ b/llvm/unittests/IR/AsmWriterTest.cpp
@@ -80,6 +80,10 @@ TEST(AsmWriterTest, FastBasicBlockPrint) {
       ret void, !annotation !12
     }
 
+    define void @g() {
+      ret void
+    }
+
     !llvm.dbg.cu = !{!0}
     !llvm.module.flags = !{!5}
 
@@ -119,6 +123,36 @@ TEST(AsmWriterTest, FastBasicBlockPrint) {
   EXPECT_THAT(First, HasSubstr("#dbg_value(i32 0, !"));
   EXPECT_THAT(First, HasSubstr("!DIExpression(DW_OP_constu, 4)"));
   EXPECT_THAT(First, HasSubstr("ret void, !annotation !"));
+
+  MDNode *Earlier = MDNode::getDistinct(Ctx, MDString::get(Ctx, "earlier"));
+  M->getFunction("f")->getEntryBlock().getTerminator()->setMetadata("order",
+                                                                    Earlier);
+  MDNode *Later = MDNode::getDistinct(Ctx, MDString::get(Ctx, "later"));
+  M->getFunction("g")->getEntryBlock().getTerminator()->setMetadata("order",
+                                                                    Later);
+
+  std::string LaterOutput;
+  raw_string_ostream LaterOS(LaterOutput);
+  M->getFunction("g")->getEntryBlock().print(LaterOS);
+
+  std::string EarlierOutput;
+  raw_string_ostream EarlierOS(EarlierOutput);
+  M->getFunction("f")->getEntryBlock().print(EarlierOS);
+
+  StringRef MetadataPrefix = "!order !";
+  size_t EarlierPos = EarlierOutput.find(MetadataPrefix);
+  size_t LaterPos = LaterOutput.find(MetadataPrefix);
+  ASSERT_NE(EarlierPos, StringRef::npos);
+  ASSERT_NE(LaterPos, StringRef::npos);
+  StringRef EarlierIDText =
+      StringRef(EarlierOutput).drop_front(EarlierPos + MetadataPrefix.size());
+  StringRef LaterIDText =
+      StringRef(LaterOutput).drop_front(LaterPos + MetadataPrefix.size());
+  unsigned EarlierID;
+  unsigned LaterID;
+  ASSERT_FALSE(EarlierIDText.consumeInteger(10, EarlierID));
+  ASSERT_FALSE(LaterIDText.consumeInteger(10, LaterID));
+  EXPECT_LT(EarlierID, LaterID);
 }
 
 TEST(AsmWriterTest, PrintAddrspaceWithNullOperand) {



More information about the llvm-commits mailing list