[llvm] [IR] Speed up debug printing with persistent metadata IDs (PR #216838)

Yaxun Liu via llvm-commits llvm-commits at lists.llvm.org
Wed Aug 19 11:35:19 PDT 2026


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

>From 2f6ec89222a4714fde00ddcbbac4e9682f8c7b2a Mon Sep 17 00:00:00 2001
From: "Yaxun (Sam) Liu" <yaxun.liu at amd.com>
Date: Wed, 19 Aug 2026 13:27:59 -0400
Subject: [PATCH 1/3] [IR] Use persistent metadata IDs for pass snapshots

Pass snapshots repeatedly renumber metadata, which makes output harder
to compare and spends time rescanning module metadata.

Assign a persistent ID when a metadata node becomes permanent and use it
for module, function, loop, and SCC pass snapshots. Normal module
printing keeps compact numbering for canonical output.

The ID fits in existing MDNode header padding on 64-bit hosts. It adds
four bytes to the header on 32-bit hosts.
---
 llvm/include/llvm/IR/IRPrintingPasses.h       |   5 +
 llvm/include/llvm/IR/Metadata.h               |   2 +-
 llvm/include/llvm/IR/Module.h                 |   7 +
 .../include/llvm/IRPrinter/IRPrintingPasses.h |   5 +
 llvm/lib/Analysis/CallGraphSCCPass.cpp        |   4 +-
 llvm/lib/Analysis/LoopInfo.cpp                |   2 +-
 llvm/lib/IR/AsmWriter.cpp                     | 144 ++++++++++++++----
 llvm/lib/IR/IRPrintingPasses.cpp              |  36 +++--
 llvm/lib/IR/LLVMContextImpl.h                 |  12 ++
 llvm/lib/IR/LegacyPassManager.cpp             |  12 +-
 llvm/lib/IR/Metadata.cpp                      |   9 ++
 llvm/lib/IR/Pass.cpp                          |   4 +-
 llvm/lib/IRPrinter/IRPrintingPasses.cpp       |  26 +++-
 llvm/lib/Passes/PassRegistry.def              |   5 +-
 llvm/lib/Passes/StandardInstrumentations.cpp  |   2 +-
 .../legacy-callgraph-scc-pass-printer.ll      |  13 +-
 .../print-changed-persistent-metadata-ids.ll  |  77 ++++++++++
 .../Other/print-persistent-metadata-ids.ll    |  94 ++++++++++++
 llvm/unittests/IR/AsmWriterTest.cpp           |  83 ++++++++++
 19 files changed, 488 insertions(+), 54 deletions(-)
 create mode 100644 llvm/test/Other/print-changed-persistent-metadata-ids.ll
 create mode 100644 llvm/test/Other/print-persistent-metadata-ids.ll

diff --git a/llvm/include/llvm/IR/IRPrintingPasses.h b/llvm/include/llvm/IR/IRPrintingPasses.h
index 1b2d38d6190e9..a7803e39d3b89 100644
--- a/llvm/include/llvm/IR/IRPrintingPasses.h
+++ b/llvm/include/llvm/IR/IRPrintingPasses.h
@@ -31,6 +31,11 @@ LLVM_ABI ModulePass *
 createPrintModulePass(raw_ostream &OS, const std::string &Banner = "",
                       bool ShouldPreserveUseListOrder = false);
 
+LLVM_ABI ModulePass *createPrintModulePass(raw_ostream &OS,
+                                           const std::string &Banner,
+                                           bool ShouldPreserveUseListOrder,
+                                           bool UsePersistentMetadataIDs);
+
 /// Create and return a pass that prints functions to the specified
 /// \c raw_ostream as they are processed.
 LLVM_ABI FunctionPass *createPrintFunctionPass(raw_ostream &OS,
diff --git a/llvm/include/llvm/IR/Metadata.h b/llvm/include/llvm/IR/Metadata.h
index 5b458fa14f0b1..b108e42e06ce0 100644
--- a/llvm/include/llvm/IR/Metadata.h
+++ b/llvm/include/llvm/IR/Metadata.h
@@ -1083,7 +1083,7 @@ class MDNode : public Metadata {
     size_t IsLarge : 1;
     size_t SmallSize : 4;
     size_t SmallNumOps : 4;
-    size_t : sizeof(size_t) * CHAR_BIT - 10;
+    uint32_t MetadataPrintID = 0;
 
     unsigned NumUnresolved = 0;
     using LargeStorageVector = SmallVector<MDOperand, 0>;
diff --git a/llvm/include/llvm/IR/Module.h b/llvm/include/llvm/IR/Module.h
index 53927e96a232e..93393bbef2704 100644
--- a/llvm/include/llvm/IR/Module.h
+++ b/llvm/include/llvm/IR/Module.h
@@ -987,6 +987,13 @@ class LLVM_ABI Module {
              bool ShouldPreserveUseListOrder = false,
              bool IsForDebug = false) const;
 
+  /// Print the module using metadata IDs that stay stable across pass-debug
+  /// snapshots. Unlike canonical module output, these IDs may contain gaps.
+  void printWithPersistentMetadataIDs(raw_ostream &OS,
+                                      AssemblyAnnotationWriter *AAW = nullptr,
+                                      bool ShouldPreserveUseListOrder = false,
+                                      bool IsForDebug = false) const;
+
   /// Dump the module to stderr (for debugging).
   void dump() const;
 
diff --git a/llvm/include/llvm/IRPrinter/IRPrintingPasses.h b/llvm/include/llvm/IRPrinter/IRPrintingPasses.h
index e574d94ca2f22..175cecdfa3cd5 100644
--- a/llvm/include/llvm/IRPrinter/IRPrintingPasses.h
+++ b/llvm/include/llvm/IRPrinter/IRPrintingPasses.h
@@ -35,12 +35,17 @@ class PrintModulePass : public RequiredPassInfoMixin<PrintModulePass> {
   std::string Banner;
   bool ShouldPreserveUseListOrder;
   bool EmitSummaryIndex;
+  bool UsePersistentMetadataIDs;
 
 public:
   LLVM_ABI PrintModulePass();
   LLVM_ABI PrintModulePass(raw_ostream &OS, const std::string &Banner = "",
                            bool ShouldPreserveUseListOrder = false,
                            bool EmitSummaryIndex = false);
+  LLVM_ABI PrintModulePass(raw_ostream &OS, const std::string &Banner,
+                           bool ShouldPreserveUseListOrder,
+                           bool EmitSummaryIndex,
+                           bool UsePersistentMetadataIDs);
 
   LLVM_ABI PreservedAnalyses run(Module &M, AnalysisManager<Module> &);
 };
diff --git a/llvm/lib/Analysis/CallGraphSCCPass.cpp b/llvm/lib/Analysis/CallGraphSCCPass.cpp
index 3fd2fe02f6688..dd6f7212f038f 100644
--- a/llvm/lib/Analysis/CallGraphSCCPass.cpp
+++ b/llvm/lib/Analysis/CallGraphSCCPass.cpp
@@ -686,7 +686,7 @@ namespace {
       if (isFunctionInPrintList("*") && NeedModule) {
         PrintBannerOnce();
         OS << "\n";
-        SCC.getCallGraph().getModule().print(OS, nullptr);
+        SCC.getCallGraph().getModule().printWithPersistentMetadataIDs(OS);
         return false;
       }
       bool FoundFunction = false;
@@ -707,7 +707,7 @@ namespace {
       if (NeedModule && FoundFunction) {
         PrintBannerOnce();
         OS << "\n";
-        SCC.getCallGraph().getModule().print(OS, nullptr);
+        SCC.getCallGraph().getModule().printWithPersistentMetadataIDs(OS);
       }
       return false;
     }
diff --git a/llvm/lib/Analysis/LoopInfo.cpp b/llvm/lib/Analysis/LoopInfo.cpp
index 16a7690092ff5..43dc25ec0efcb 100644
--- a/llvm/lib/Analysis/LoopInfo.cpp
+++ b/llvm/lib/Analysis/LoopInfo.cpp
@@ -1035,7 +1035,7 @@ void llvm::printLoop(const Loop &L, raw_ostream &OS,
     OS << ")\n";
 
     // printing whole module
-    OS << *L.getHeader()->getModule();
+    L.getHeader()->getModule()->printWithPersistentMetadataIDs(OS);
     return;
   }
 
diff --git a/llvm/lib/IR/AsmWriter.cpp b/llvm/lib/IR/AsmWriter.cpp
index c3202eea12c28..5373eeb4373ce 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"
@@ -800,6 +801,8 @@ class llvm::SlotTracker : public AbstractSlotTrackerStorage {
   const Function* TheFunction = nullptr;
   bool FunctionProcessed = false;
   bool ShouldInitializeAllMetadata;
+  bool UsePersistentMetadataIDs;
+  bool TrackPersistentMetadataDefinitions;
 
   std::function<void(AbstractSlotTrackerStorage *, const Module *, bool)>
       ProcessModuleHookFn;
@@ -849,15 +852,19 @@ class llvm::SlotTracker : public AbstractSlotTrackerStorage {
   /// functions, giving correct numbering for metadata referenced only from
   /// within a function (even if no functions have been initialized).
   explicit SlotTracker(const Module *M,
-                       bool ShouldInitializeAllMetadata = false);
+                       bool ShouldInitializeAllMetadata = false,
+                       bool UsePersistentMetadataIDs = false);
 
   /// Construct from a function, starting out in incorp state.
   ///
   /// 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);
@@ -908,6 +915,7 @@ class llvm::SlotTracker : public AbstractSlotTrackerStorage {
   mdn_iterator mdn_end() { return mdnMap.end(); }
   unsigned mdn_size() const { return mdnMap.size(); }
   bool mdn_empty() const { return mdnMap.empty(); }
+  bool usePersistentMetadataIDs() const { return UsePersistentMetadataIDs; }
 
   /// AttributeSet map iterators.
   using as_iterator = DenseMap<AttributeSet, unsigned>::iterator;
@@ -932,6 +940,9 @@ class llvm::SlotTracker : public AbstractSlotTrackerStorage {
   /// CreateMetadataSlot - Insert the specified MDNode* into the slot table.
   void CreateMetadataSlot(const MDNode *N);
 
+  /// Record a persistent slot and the metadata nodes referenced by it.
+  void CreatePersistentMetadataSlot(const MDNode *N);
+
   /// CreateFunctionSlot - Insert the specified Value* into the slot table.
   void CreateFunctionSlot(const Value *V);
 
@@ -1056,17 +1067,31 @@ 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) {}
+SlotTracker::SlotTracker(const Module *M, bool ShouldInitializeAllMetadata,
+                         bool UsePersistentMetadataIDs)
+    : TheModule(M), ShouldInitializeAllMetadata(ShouldInitializeAllMetadata),
+      UsePersistentMetadataIDs(UsePersistentMetadataIDs),
+      TrackPersistentMetadataDefinitions(UsePersistentMetadataIDs) {
+  assert((!ShouldInitializeAllMetadata || !UsePersistentMetadataIDs) &&
+         "cannot initialize compact metadata slots with persistent IDs");
+}
 
 // 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),
+      TrackPersistentMetadataDefinitions(false) {
+  assert((!ShouldInitializeAllMetadata || !UsePersistentMetadataIDs) &&
+         "cannot initialize compact metadata slots with persistent IDs");
+}
 
 SlotTracker::SlotTracker(const ModuleSummaryIndex *Index)
-    : TheModule(nullptr), ShouldInitializeAllMetadata(false), TheIndex(Index) {}
+    : TheModule(nullptr), ShouldInitializeAllMetadata(false),
+      UsePersistentMetadataIDs(false),
+      TrackPersistentMetadataDefinitions(false), TheIndex(Index) {}
 
 inline void SlotTracker::initializeIfNeeded() {
   if (TheModule) {
@@ -1095,7 +1120,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 +1135,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 +1172,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.
@@ -1318,13 +1345,26 @@ void SlotTracker::setProcessHook(
 }
 
 /// getMetadataSlot - Get the slot number of a MDNode.
-void SlotTracker::createMetadataSlot(const MDNode *N) { CreateMetadataSlot(N); }
+void SlotTracker::createMetadataSlot(const MDNode *N) {
+  if (UsePersistentMetadataIDs)
+    CreatePersistentMetadataSlot(N);
+  else
+    CreateMetadataSlot(N);
+}
 
 /// getMetadataSlot - Get the slot number of a MDNode.
 int SlotTracker::getMetadataSlot(const MDNode *N) {
   // Check for uninitialized state and do lazy initialization.
   initializeIfNeeded();
 
+  if (UsePersistentMetadataIDs) {
+    if (isa<DIExpression>(N))
+      return -1;
+    if (TrackPersistentMetadataDefinitions)
+      CreatePersistentMetadataSlot(N);
+    return N->getContext().pImpl->getMetadataPrintID(N);
+  }
+
   // Find the MDNode in the module map
   mdn_iterator MI = mdnMap.find(N);
   return MI == mdnMap.end() ? -1 : (int)MI->second;
@@ -1435,6 +1475,22 @@ void SlotTracker::CreateMetadataSlot(const MDNode *N) {
       CreateMetadataSlot(Op);
 }
 
+void SlotTracker::CreatePersistentMetadataSlot(const MDNode *N) {
+  assert(N && "Can't insert a null Value into SlotTracker!");
+
+  if (isa<DIExpression>(N))
+    return;
+
+  unsigned ID = N->getContext().pImpl->getMetadataPrintID(N);
+  if (!mdnMap.try_emplace(N, ID).second)
+    return;
+  mdnNext = std::max(mdnNext, ID + 1);
+
+  for (const MDOperand &Op : N->operands())
+    if (const auto *OpNode = dyn_cast_or_null<MDNode>(Op.get()))
+      CreatePersistentMetadataSlot(OpNode);
+}
+
 void SlotTracker::CreateAttributeSetSlot(AttributeSet AS) {
   assert(AS.hasAttributes() && "Doesn't need a slot!");
 
@@ -5021,14 +5077,25 @@ void AssemblyWriter::writeMDNode(unsigned Slot, const MDNode *Node) {
 }
 
 void AssemblyWriter::writeAllMDNodes() {
-  SmallVector<const MDNode *, 16> Nodes;
-  Nodes.resize(Machine.mdn_size());
+  if (!Machine.usePersistentMetadataIDs()) {
+    SmallVector<const MDNode *, 16> Nodes;
+    Nodes.resize(Machine.mdn_size());
+    for (auto &I : llvm::make_range(Machine.mdn_begin(), Machine.mdn_end()))
+      Nodes[I.second] = cast<MDNode>(I.first);
+
+    for (unsigned i = 0, e = Nodes.size(); i != e; ++i)
+      writeMDNode(i, Nodes[i]);
+    return;
+  }
+
+  SmallVector<std::pair<unsigned, const MDNode *>, 16> Nodes;
+  Nodes.reserve(Machine.mdn_size());
   for (auto &I : llvm::make_range(Machine.mdn_begin(), Machine.mdn_end()))
-    Nodes[I.second] = cast<MDNode>(I.first);
+    Nodes.emplace_back(I.second, cast<MDNode>(I.first));
+  llvm::sort(Nodes);
 
-  for (unsigned i = 0, e = Nodes.size(); i != e; ++i) {
-    writeMDNode(i, Nodes[i]);
-  }
+  for (auto [Slot, Node] : Nodes)
+    writeMDNode(Slot, Node);
 }
 
 void AssemblyWriter::printMDNodeBody(const MDNode *Node) {
@@ -5099,7 +5166,8 @@ void AssemblyWriter::printUseLists(const Function *F) {
 
 void Function::print(raw_ostream &ROS, AssemblyAnnotationWriter *AAW,
                      bool ShouldPreserveUseListOrder, bool IsForDebug) const {
-  SlotTracker SlotTable(this->getParent());
+  SlotTracker SlotTable(this, /*ShouldInitializeAllMetadata=*/false,
+                        /*UsePersistentMetadataIDs=*/true);
   formatted_raw_ostream OS(ROS);
   AssemblyWriter W(OS, SlotTable, this->getParent(), AAW, IsForDebug,
                    ShouldPreserveUseListOrder);
@@ -5109,10 +5177,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=*/true);
   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);
 }
@@ -5126,6 +5195,18 @@ void Module::print(raw_ostream &ROS, AssemblyAnnotationWriter *AAW,
   W.printModule(this);
 }
 
+void Module::printWithPersistentMetadataIDs(raw_ostream &ROS,
+                                            AssemblyAnnotationWriter *AAW,
+                                            bool ShouldPreserveUseListOrder,
+                                            bool IsForDebug) const {
+  SlotTracker SlotTable(this, /*ShouldInitializeAllMetadata=*/false,
+                        /*UsePersistentMetadataIDs=*/true);
+  formatted_raw_ostream OS(ROS);
+  AssemblyWriter W(OS, SlotTable, this, AAW, IsForDebug,
+                   ShouldPreserveUseListOrder);
+  W.printModule(this);
+}
+
 void NamedMDNode::print(raw_ostream &ROS, bool IsForDebug) const {
   SlotTracker SlotTable(getParent());
   formatted_raw_ostream OS(ROS);
@@ -5262,10 +5343,19 @@ void DbgLabelRecord::print(raw_ostream &ROS, ModuleSlotTracker &MST,
 }
 
 void Value::print(raw_ostream &ROS, bool IsForDebug) const {
+  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);
-  else if (isa<Function>(this) || isa<MetadataAsValue>(this))
+  else if (isa<MetadataAsValue>(this))
     ShouldInitializeAllMetadata = true;
 
   ModuleSlotTracker MST(getModuleFromVal(this), ShouldInitializeAllMetadata);
diff --git a/llvm/lib/IR/IRPrintingPasses.cpp b/llvm/lib/IR/IRPrintingPasses.cpp
index 43671b0b9a1a3..61696bdbe24f7 100644
--- a/llvm/lib/IR/IRPrintingPasses.cpp
+++ b/llvm/lib/IR/IRPrintingPasses.cpp
@@ -30,20 +30,29 @@ class PrintModulePassWrapper : public ModulePass {
   raw_ostream &OS;
   std::string Banner;
   bool ShouldPreserveUseListOrder;
+  bool UsePersistentMetadataIDs;
 
 public:
   static char ID;
-  PrintModulePassWrapper() : ModulePass(ID), OS(dbgs()) {}
+  PrintModulePassWrapper()
+      : ModulePass(ID), OS(dbgs()), ShouldPreserveUseListOrder(false),
+        UsePersistentMetadataIDs(true) {}
   PrintModulePassWrapper(raw_ostream &OS, const std::string &Banner,
-                         bool ShouldPreserveUseListOrder)
+                         bool ShouldPreserveUseListOrder,
+                         bool UsePersistentMetadataIDs)
       : ModulePass(ID), OS(OS), Banner(Banner),
-        ShouldPreserveUseListOrder(ShouldPreserveUseListOrder) {}
+        ShouldPreserveUseListOrder(ShouldPreserveUseListOrder),
+        UsePersistentMetadataIDs(UsePersistentMetadataIDs) {}
 
   bool runOnModule(Module &M) override {
     if (llvm::isFunctionInPrintList("*")) {
       if (!Banner.empty())
         OS << Banner << "\n";
-      M.print(OS, nullptr, ShouldPreserveUseListOrder);
+      if (UsePersistentMetadataIDs)
+        M.printWithPersistentMetadataIDs(OS, nullptr,
+                                         ShouldPreserveUseListOrder);
+      else
+        M.print(OS, nullptr, ShouldPreserveUseListOrder);
     } else {
       bool BannerPrinted = false;
       for (const auto &F : M.functions()) {
@@ -80,10 +89,10 @@ class PrintFunctionPassWrapper : public FunctionPass {
   // This pass just prints a banner followed by the function as it's processed.
   bool runOnFunction(Function &F) override {
     if (isFunctionInPrintList(F.getName())) {
-      if (forcePrintModuleIR())
-        OS << Banner << " (function: " << F.getName() << ")\n"
-           << *F.getParent();
-      else
+      if (forcePrintModuleIR()) {
+        OS << Banner << " (function: " << F.getName() << ")\n";
+        F.getParent()->printWithPersistentMetadataIDs(OS);
+      } else
         OS << Banner << '\n' << static_cast<Value &>(F);
     }
 
@@ -109,7 +118,16 @@ INITIALIZE_PASS(PrintFunctionPassWrapper, "print-function",
 ModulePass *llvm::createPrintModulePass(llvm::raw_ostream &OS,
                                         const std::string &Banner,
                                         bool ShouldPreserveUseListOrder) {
-  return new PrintModulePassWrapper(OS, Banner, ShouldPreserveUseListOrder);
+  return createPrintModulePass(OS, Banner, ShouldPreserveUseListOrder,
+                               /*UsePersistentMetadataIDs=*/false);
+}
+
+ModulePass *llvm::createPrintModulePass(llvm::raw_ostream &OS,
+                                        const std::string &Banner,
+                                        bool ShouldPreserveUseListOrder,
+                                        bool UsePersistentMetadataIDs) {
+  return new PrintModulePassWrapper(OS, Banner, ShouldPreserveUseListOrder,
+                                    UsePersistentMetadataIDs);
 }
 
 FunctionPass *llvm::createPrintFunctionPass(llvm::raw_ostream &OS,
diff --git a/llvm/lib/IR/LLVMContextImpl.h b/llvm/lib/IR/LLVMContextImpl.h
index 41c8a92c56eda..f357d62d71c66 100644
--- a/llvm/lib/IR/LLVMContextImpl.h
+++ b/llvm/lib/IR/LLVMContextImpl.h
@@ -1611,6 +1611,18 @@ class LLVMContextImpl {
   DenseMap<Metadata *, MetadataAsValue *> MetadataAsValues;
   DenseSet<DIArgList *, DIArgListInfo> DIArgLists;
 
+  uint32_t NextMetadataPrintID = 0;
+
+  uint32_t allocateMetadataPrintID() {
+    assert(NextMetadataPrintID != UINT32_MAX && "too many metadata nodes");
+    return NextMetadataPrintID++;
+  }
+
+  uint32_t getMetadataPrintID(const MDNode *N) const {
+    assert(!N->isTemporary() && "temporary metadata has no print ID");
+    return N->getHeader().MetadataPrintID;
+  }
+
 #define HANDLE_MDNODE_LEAF_UNIQUABLE(CLASS)                                    \
   DenseSet<CLASS *, CLASS##Info> CLASS##s;
 #include "llvm/IR/Metadata.def"
diff --git a/llvm/lib/IR/LegacyPassManager.cpp b/llvm/lib/IR/LegacyPassManager.cpp
index bbe75ab275092..c100f86397dca 100644
--- a/llvm/lib/IR/LegacyPassManager.cpp
+++ b/llvm/lib/IR/LegacyPassManager.cpp
@@ -393,7 +393,9 @@ class MPPassManager : public Pass, public PMDataManager {
   /// createPrinterPass - Get a module printer pass.
   Pass *createPrinterPass(raw_ostream &O,
                           const std::string &Banner) const override {
-    return createPrintModulePass(O, Banner);
+    return createPrintModulePass(O, Banner,
+                                 /*ShouldPreserveUseListOrder=*/false,
+                                 /*UsePersistentMetadataIDs=*/true);
   }
 
   /// run - Execute all of the passes scheduled for execution.  Keep track of
@@ -481,7 +483,9 @@ class PassManagerImpl : public Pass,
   /// createPrinterPass - Get a module printer pass.
   Pass *createPrinterPass(raw_ostream &O,
                           const std::string &Banner) const override {
-    return createPrintModulePass(O, Banner);
+    return createPrintModulePass(O, Banner,
+                                 /*ShouldPreserveUseListOrder=*/false,
+                                 /*UsePersistentMetadataIDs=*/true);
   }
 
   /// run - Execute all of the passes scheduled for execution.  Keep track of
@@ -1547,7 +1551,7 @@ MPPassManager::runOnModule(Module &M) {
       BeforeStr.clear();
       AfterStr.clear();
       raw_svector_ostream OS(BeforeStr);
-      M.print(OS, /*AAW=*/nullptr);
+      M.printWithPersistentMetadataIDs(OS);
     }
 
     {
@@ -1580,7 +1584,7 @@ MPPassManager::runOnModule(Module &M) {
 
     if (ShouldPrintChanged) {
       raw_svector_ostream OS(AfterStr);
-      M.print(OS, /*AAW=*/nullptr);
+      M.printWithPersistentMetadataIDs(OS);
     }
     if (ReportChanged)
       reportChangedIR(BeforeStr, AfterStr, MP->getPassName(), PassID,
diff --git a/llvm/lib/IR/Metadata.cpp b/llvm/lib/IR/Metadata.cpp
index 0a4141ee2362e..c093fe66d27a5 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) == sizeof(size_t) + 2 * sizeof(uint32_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())
+    getHeader().MetadataPrintID = Context.pImpl->allocateMetadataPrintID();
+
   unsigned Op = 0;
   for (Metadata *MD : Ops1)
     setOperand(Op++, MD);
@@ -788,6 +793,7 @@ void MDNode::makeUniqued() {
 
   // Make this 'uniqued'.
   Storage = Uniqued;
+  getHeader().MetadataPrintID = getContext().pImpl->allocateMetadataPrintID();
   countUnresolvedOperands();
   if (!getNumUnresolved()) {
     dropReplaceableUses();
@@ -1066,7 +1072,10 @@ void MDNode::deleteTemporary(MDNode *N) {
 void MDNode::storeDistinctInContext() {
   assert(!Context.hasReplaceableUses() && "Unexpected replaceable uses");
   assert(!getNumUnresolved() && "Unexpected unresolved nodes");
+  const bool WasTemporary = isTemporary();
   Storage = Distinct;
+  if (WasTemporary)
+    getHeader().MetadataPrintID = getContext().pImpl->allocateMetadataPrintID();
   assert(isResolved() && "Expected this to be resolved");
 
   // Reset the hash.
diff --git a/llvm/lib/IR/Pass.cpp b/llvm/lib/IR/Pass.cpp
index 08d58379b7e69..9b13f42ab0dc7 100644
--- a/llvm/lib/IR/Pass.cpp
+++ b/llvm/lib/IR/Pass.cpp
@@ -49,7 +49,9 @@ ModulePass::~ModulePass() = default;
 
 Pass *ModulePass::createPrinterPass(raw_ostream &OS,
                                     const std::string &Banner) const {
-  return createPrintModulePass(OS, Banner);
+  return createPrintModulePass(OS, Banner,
+                               /*ShouldPreserveUseListOrder=*/false,
+                               /*UsePersistentMetadataIDs=*/true);
 }
 
 PassManagerType ModulePass::getPotentialPassManagerType() const {
diff --git a/llvm/lib/IRPrinter/IRPrintingPasses.cpp b/llvm/lib/IRPrinter/IRPrintingPasses.cpp
index adb192ac4a916..73194c706c2ab 100644
--- a/llvm/lib/IRPrinter/IRPrintingPasses.cpp
+++ b/llvm/lib/IRPrinter/IRPrintingPasses.cpp
@@ -23,19 +23,32 @@
 
 using namespace llvm;
 
-PrintModulePass::PrintModulePass() : OS(dbgs()) {}
+PrintModulePass::PrintModulePass()
+    : OS(dbgs()), ShouldPreserveUseListOrder(false), EmitSummaryIndex(false),
+      UsePersistentMetadataIDs(true) {}
 PrintModulePass::PrintModulePass(raw_ostream &OS, const std::string &Banner,
                                  bool ShouldPreserveUseListOrder,
                                  bool EmitSummaryIndex)
+    : PrintModulePass(OS, Banner, ShouldPreserveUseListOrder, EmitSummaryIndex,
+                      /*UsePersistentMetadataIDs=*/false) {}
+
+PrintModulePass::PrintModulePass(raw_ostream &OS, const std::string &Banner,
+                                 bool ShouldPreserveUseListOrder,
+                                 bool EmitSummaryIndex,
+                                 bool UsePersistentMetadataIDs)
     : OS(OS), Banner(Banner),
       ShouldPreserveUseListOrder(ShouldPreserveUseListOrder),
-      EmitSummaryIndex(EmitSummaryIndex) {}
+      EmitSummaryIndex(EmitSummaryIndex),
+      UsePersistentMetadataIDs(UsePersistentMetadataIDs) {}
 
 PreservedAnalyses PrintModulePass::run(Module &M, ModuleAnalysisManager &AM) {
   if (llvm::isFunctionInPrintList("*")) {
     if (!Banner.empty())
       OS << Banner << "\n";
-    M.print(OS, nullptr, ShouldPreserveUseListOrder);
+    if (UsePersistentMetadataIDs)
+      M.printWithPersistentMetadataIDs(OS, nullptr, ShouldPreserveUseListOrder);
+    else
+      M.print(OS, nullptr, ShouldPreserveUseListOrder);
   } else {
     bool BannerPrinted = false;
     for (const auto &F : M.functions()) {
@@ -68,9 +81,10 @@ PrintFunctionPass::PrintFunctionPass(raw_ostream &OS, const std::string &Banner)
 PreservedAnalyses PrintFunctionPass::run(Function &F,
                                          FunctionAnalysisManager &) {
   if (isFunctionInPrintList(F.getName())) {
-    if (forcePrintModuleIR())
-      OS << Banner << " (function: " << F.getName() << ")\n" << *F.getParent();
-    else
+    if (forcePrintModuleIR()) {
+      OS << Banner << " (function: " << F.getName() << ")\n";
+      F.getParent()->printWithPersistentMetadataIDs(OS);
+    } else
       OS << Banner << '\n' << static_cast<Value &>(F);
   }
 
diff --git a/llvm/lib/Passes/PassRegistry.def b/llvm/lib/Passes/PassRegistry.def
index 177d8ecd3508d..7d8c830ea04f3 100644
--- a/llvm/lib/Passes/PassRegistry.def
+++ b/llvm/lib/Passes/PassRegistry.def
@@ -139,7 +139,10 @@ MODULE_PASS("pgo-icall-prom", PGOIndirectCallPromotion())
 MODULE_PASS("pgo-instr-gen", PGOInstrumentationGen())
 MODULE_PASS("pgo-instr-use", PGOInstrumentationUse())
 MODULE_PASS("pre-isel-intrinsic-lowering", PreISelIntrinsicLoweringPass(TM))
-MODULE_PASS("print", PrintModulePass(errs()))
+MODULE_PASS("print", PrintModulePass(errs(), /*Banner=*/"",
+                                     /*ShouldPreserveUseListOrder=*/false,
+                                     /*EmitSummaryIndex=*/false,
+                                     /*UsePersistentMetadataIDs=*/true))
 MODULE_PASS("print-callgraph", CallGraphPrinterPass(errs()))
 MODULE_PASS("print-callgraph-sccs", CallGraphSCCsPrinterPass(errs()))
 MODULE_PASS("print-lcg", LazyCallGraphPrinterPass(errs()))
diff --git a/llvm/lib/Passes/StandardInstrumentations.cpp b/llvm/lib/Passes/StandardInstrumentations.cpp
index 6f79f1b17a98d..aab9e83c11570 100644
--- a/llvm/lib/Passes/StandardInstrumentations.cpp
+++ b/llvm/lib/Passes/StandardInstrumentations.cpp
@@ -201,7 +201,7 @@ void printIR(raw_ostream &OS, const Function *F) {
 
 void printIR(raw_ostream &OS, const Module *M) {
   if (isFunctionInPrintList("*") || forcePrintModuleIR()) {
-    M->print(OS, nullptr);
+    M->printWithPersistentMetadataIDs(OS);
   } else {
     for (const auto &F : M->functions()) {
       printIR(OS, &F);
diff --git a/llvm/test/Other/legacy-callgraph-scc-pass-printer.ll b/llvm/test/Other/legacy-callgraph-scc-pass-printer.ll
index fda04f6740575..d382e093932a4 100644
--- a/llvm/test/Other/legacy-callgraph-scc-pass-printer.ll
+++ b/llvm/test/Other/legacy-callgraph-scc-pass-printer.ll
@@ -1,11 +1,22 @@
 ; RUN: llc -mtriple=x86_64-unknown-linux-gnu -enable-ipra \
 ; RUN:     -print-after=DummyCGSCCPass -o - %s 2>&1 | FileCheck %s
+; RUN: llc -mtriple=x86_64-unknown-linux-gnu -enable-ipra \
+; RUN:     -print-after=DummyCGSCCPass -print-module-scope -o - %s 2>&1 \
+; RUN:     | FileCheck %s --check-prefix=PERSISTENT
 ; REQUIRES: x86-registered-target
 
 ; The legacy CallGraphSCCPass printer should emit the banner as its own line.
 ; CHECK-LABEL: *** IR Dump After DummyCGSCCPass (DummyCGSCCPass) ***
 ; CHECK-NEXT: define void @bar() {
 
+; PERSISTENT-LABEL: *** IR Dump After DummyCGSCCPass (DummyCGSCCPass) ***
+; PERSISTENT: define void @bar() {
+; PERSISTENT: ret void, !annotation !1
+; PERSISTENT: !1 = !{!"used"}
+
 define void @bar() {
-  ret void
+  ret void, !annotation !1
 }
+
+!0 = !{!"unused"}
+!1 = !{!"used"}
diff --git a/llvm/test/Other/print-changed-persistent-metadata-ids.ll b/llvm/test/Other/print-changed-persistent-metadata-ids.ll
new file mode 100644
index 0000000000000..bf755b01095c4
--- /dev/null
+++ b/llvm/test/Other/print-changed-persistent-metadata-ids.ll
@@ -0,0 +1,77 @@
+; RUN: opt -passes=instsimplify -filter-print-funcs=second \
+; RUN:   -print-changed=quiet -disable-output < %s 2>&1 | FileCheck %s --check-prefix=CHANGED
+; RUN: opt -passes=instsimplify -filter-print-funcs=second \
+; RUN:   -print-before=instsimplify -print-after=instsimplify \
+; RUN:   -disable-output < %s 2>&1 | FileCheck %s --check-prefix=STABLE
+; RUN: opt -passes='function(instsimplify),globaldce' -filter-print-funcs=second \
+; RUN:   -print-changed=quiet -print-module-scope -disable-output < %s 2>&1 \
+; RUN:   | FileCheck %s --check-prefix=CROSS-KIND
+; RUN: opt -passes='function(instsimplify),print' -disable-output < %s 2> %t
+; RUN: FileCheck %s --check-prefix=SPARSE < %t
+; RUN: opt -disable-output < %t
+; RUN: opt -S -passes='function(instsimplify)' < %s \
+; RUN:   | FileCheck %s --check-prefix=COMPACT
+
+declare i32 @opaque(i32)
+
+ at dead = internal global i32 0
+
+define i32 @first(i32 %arg) #0 {
+  %keep = call i32 @opaque(i32 %arg)
+  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 = !{!"removed metadata"}
+!1 = !{!2}
+!2 = !{!"second metadata"}
+!3 = !{!"other metadata"}
+
+attributes #0 = { nounwind }
+attributes #1 = { noinline }
+
+; CHANGED: *** IR Dump After InstSimplifyPass on second ***
+; CHANGED: define i32 @second(i32 %arg) #1 {
+; CHANGED: %keep = call i32 @opaque(i32 %arg)
+; CHANGED-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]]
+
+; CROSS-KIND: *** IR Dump After InstSimplifyPass on second ***
+; CROSS-KIND: @dead = internal global i32 0
+; CROSS-KIND: define i32 @second(i32 %arg) #1 {
+; CROSS-KIND: %keep = call i32 @opaque(i32 %arg)
+; CROSS-KIND-SAME: !annotation ![[CROSS_ANNOTATION:[0-9]+]], !other ![[CROSS_OTHER:[0-9]+]]
+; CROSS-KIND: *** IR Dump After GlobalDCEPass on [module] ***
+; CROSS-KIND-NOT: @dead
+; CROSS-KIND: define i32 @second(i32 %arg) #1 {
+; CROSS-KIND: %keep = call i32 @opaque(i32 %arg)
+; CROSS-KIND-SAME: !annotation ![[CROSS_ANNOTATION]], !other ![[CROSS_OTHER]]
+
+; SPARSE: define i32 @second(i32 %arg) #1 {
+; SPARSE: %keep = call i32 @opaque(i32 %arg)
+; SPARSE-SAME: !annotation !1, !other !3
+; SPARSE-NOT: !0 =
+; SPARSE: !1 = !{!2}
+; SPARSE: !2 = !{!"second metadata"}
+; SPARSE: !3 = !{!"other metadata"}
+
+; COMPACT: define i32 @second(i32 %arg) #1 {
+; COMPACT: %keep = call i32 @opaque(i32 %arg)
+; COMPACT-SAME: !annotation !0, !other !2
+; COMPACT: !0 = !{!1}
+; COMPACT: !1 = !{!"second metadata"}
+; COMPACT: !2 = !{!"other metadata"}
diff --git a/llvm/test/Other/print-persistent-metadata-ids.ll b/llvm/test/Other/print-persistent-metadata-ids.ll
new file mode 100644
index 0000000000000..0fa89a6744fd1
--- /dev/null
+++ b/llvm/test/Other/print-persistent-metadata-ids.ll
@@ -0,0 +1,94 @@
+; RUN: opt -S -passes=no-op-module < %s | FileCheck %s --check-prefix=COMPACT
+; RUN: opt -disable-output -passes=print < %s 2>&1 | FileCheck %s --check-prefix=PERSISTENT-MODULE
+; RUN: opt -disable-output -passes=print < %s 2> %t
+; RUN: opt -disable-output < %t
+; RUN: opt -disable-output -passes='function(print)' -filter-print-funcs=second \
+; RUN:   < %s 2>&1 | FileCheck %s --check-prefix=PERSISTENT-FUNCTION
+; RUN: opt -disable-output -passes='function(no-op-function)' \
+; RUN:   -print-before=no-op-function -filter-print-funcs=second \
+; RUN:   < %s 2>&1 | FileCheck %s --check-prefix=PERSISTENT-FUNCTION
+; RUN: opt -disable-output -passes='function(no-op-function)' -print-after-all \
+; RUN:   -filter-print-funcs=second < %s 2>&1 | FileCheck %s --check-prefix=PERSISTENT-FUNCTION
+; RUN: opt -disable-output -passes=no-op-module -print-before=no-op-module \
+; RUN:   -filter-print-funcs=first,second < %s 2>&1 | FileCheck %s --check-prefix=PERSISTENT-MULTI
+; RUN: opt -disable-output -passes='loop(no-op-loop)' -print-before=no-op-loop \
+; RUN:   -filter-print-funcs=loop < %s 2>&1 | FileCheck %s --check-prefix=PERSISTENT-LOOP
+; RUN: opt -disable-output -passes='print,function(print)' < %s 2>&1 | FileCheck %s --check-prefix=SAME-ID
+$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"}
+
+; COMPACT: @named = global ptr @0, comdat($group), !annotation !0
+; COMPACT: ret void, !annotation !3
+; COMPACT: call void @callee(ptr @1) #1, !annotation !5
+; COMPACT: ret void, !annotation !5
+; COMPACT: !named = !{!2}
+
+; PERSISTENT-MODULE: @named = global ptr @0, comdat($group), !annotation !5
+; PERSISTENT-MODULE: ret void, !annotation !1
+; PERSISTENT-MODULE: call void @callee(ptr @1) #1, !annotation !3
+; PERSISTENT-MODULE: ret void, !annotation !3
+; PERSISTENT-MODULE: !named = !{!0}
+
+; PERSISTENT-FUNCTION: define void @second() {
+; PERSISTENT-FUNCTION: call void @callee(ptr @1) #1, !annotation ![[SECOND:[0-9]+]]
+; PERSISTENT-FUNCTION: ret void, !annotation ![[SECOND]]
+
+; PERSISTENT-MULTI: define void @first() #0 {
+; PERSISTENT-MULTI: call void @callee(ptr @0) #1
+; PERSISTENT-MULTI: call void @callee(ptr @1) #1
+; PERSISTENT-MULTI: define void @second() {
+; PERSISTENT-MULTI: call void @callee(ptr @1) #1
+
+; PERSISTENT-LOOP: ; Preheader:
+; PERSISTENT-LOOP: call void @callee(ptr @0) #1
+; PERSISTENT-LOOP: call void @callee(ptr @1) #1
+; PERSISTENT-LOOP: ; Loop:
+; PERSISTENT-LOOP: call void @callee(ptr @1) #1
+
+; SAME-ID: define void @second() {
+; SAME-ID: call void @callee(ptr @1) #1, !annotation ![[SAME_SECOND:[0-9]+]]
+; SAME-ID: ![[SAME_SECOND]] = !{!{{[0-9]+}}}
+; SAME-ID: define void @second() {
+; SAME-ID: call void @callee(ptr @1) #1, !annotation ![[SAME_SECOND]]
diff --git a/llvm/unittests/IR/AsmWriterTest.cpp b/llvm/unittests/IR/AsmWriterTest.cpp
index 75305f4e2dea4..e1e74c723664e 100644
--- a/llvm/unittests/IR/AsmWriterTest.cpp
+++ b/llvm/unittests/IR/AsmWriterTest.cpp
@@ -5,6 +5,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 //===----------------------------------------------------------------------===//
+#include "llvm/AsmParser/Parser.h"
 #include "llvm/BinaryFormat/Dwarf.h"
 #include "llvm/IR/DebugInfoMetadata.h"
 #include "llvm/IR/Function.h"
@@ -12,6 +13,7 @@
 #include "llvm/IR/LLVMContext.h"
 #include "llvm/IR/MDBuilder.h"
 #include "llvm/IR/Module.h"
+#include "llvm/Support/SourceMgr.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 
@@ -62,6 +64,87 @@ TEST(AsmWriterTest, DumpDIExpression) {
   EXPECT_EQ("!DIExpression(DW_OP_constu, 4, DW_OP_minus, DW_OP_deref)", S);
 }
 
+TEST(AsmWriterTest, PersistentBasicBlockPrint) {
+  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
+    }
+
+    define void @g() {
+      ret void
+    }
+
+    !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);
+
+  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 !"));
+
+  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) {
   LLVMContext Ctx;
   Module M("test module", Ctx);

>From 6d8821fa47791370cdc0bf09c66188c6de5f0906 Mon Sep 17 00:00:00 2001
From: "Yaxun (Sam) Liu" <yaxun.liu at amd.com>
Date: Wed, 19 Aug 2026 14:13:03 -0400
Subject: [PATCH 2/3] [IR] Handle temporary metadata in persistent printing

Temporary metadata has no persistent ID, so persistent printing could
assert or reuse ID zero.

Assign temporary nodes print-local slots after permanent IDs. Use one
enum for compact, persistent-reference, and persistent-definition modes.
---
 llvm/lib/IR/AsmWriter.cpp           | 89 ++++++++++++++++++-----------
 llvm/lib/IR/LLVMContextImpl.h       |  2 +
 llvm/unittests/IR/AsmWriterTest.cpp | 41 +++++++++++++
 3 files changed, 98 insertions(+), 34 deletions(-)

diff --git a/llvm/lib/IR/AsmWriter.cpp b/llvm/lib/IR/AsmWriter.cpp
index 5373eeb4373ce..7a1cefb468072 100644
--- a/llvm/lib/IR/AsmWriter.cpp
+++ b/llvm/lib/IR/AsmWriter.cpp
@@ -786,6 +786,14 @@ AbstractSlotTrackerStorage::~AbstractSlotTrackerStorage() = default;
 //===----------------------------------------------------------------------===//
 // SlotTracker Class: Enumerate slot numbers for unnamed values
 //===----------------------------------------------------------------------===//
+namespace {
+enum class MetadataPrintMode {
+  Compact,
+  PersistentReferences,
+  PersistentDefinitions,
+};
+} // end anonymous namespace
+
 /// This class provides computation of slot numbers for LLVM Assembly writing.
 ///
 class llvm::SlotTracker : public AbstractSlotTrackerStorage {
@@ -801,8 +809,7 @@ class llvm::SlotTracker : public AbstractSlotTrackerStorage {
   const Function* TheFunction = nullptr;
   bool FunctionProcessed = false;
   bool ShouldInitializeAllMetadata;
-  bool UsePersistentMetadataIDs;
-  bool TrackPersistentMetadataDefinitions;
+  MetadataPrintMode MetadataMode;
 
   std::function<void(AbstractSlotTrackerStorage *, const Module *, bool)>
       ProcessModuleHookFn;
@@ -851,20 +858,20 @@ 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).
-  explicit SlotTracker(const Module *M,
-                       bool ShouldInitializeAllMetadata = false,
-                       bool UsePersistentMetadataIDs = false);
+  explicit SlotTracker(
+      const Module *M, bool ShouldInitializeAllMetadata = false,
+      MetadataPrintMode MetadataMode = MetadataPrintMode::Compact);
 
   /// Construct from a function, starting out in incorp state.
   ///
   /// 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 UsePersistentMetadataIDs = false);
+  /// Persistent modes skip metadata enumeration and use IDs stored in the
+  /// LLVM context.
+  explicit SlotTracker(
+      const Function *F, bool ShouldInitializeAllMetadata = false,
+      MetadataPrintMode MetadataMode = MetadataPrintMode::Compact);
 
   /// Construct from a module summary index.
   explicit SlotTracker(const ModuleSummaryIndex *Index);
@@ -915,7 +922,12 @@ class llvm::SlotTracker : public AbstractSlotTrackerStorage {
   mdn_iterator mdn_end() { return mdnMap.end(); }
   unsigned mdn_size() const { return mdnMap.size(); }
   bool mdn_empty() const { return mdnMap.empty(); }
-  bool usePersistentMetadataIDs() const { return UsePersistentMetadataIDs; }
+  bool usePersistentMetadataIDs() const {
+    return MetadataMode != MetadataPrintMode::Compact;
+  }
+  bool trackPersistentMetadataDefinitions() const {
+    return MetadataMode == MetadataPrintMode::PersistentDefinitions;
+  }
 
   /// AttributeSet map iterators.
   using as_iterator = DenseMap<AttributeSet, unsigned>::iterator;
@@ -1068,30 +1080,35 @@ 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,
-                         bool UsePersistentMetadataIDs)
+                         MetadataPrintMode MetadataMode)
     : TheModule(M), ShouldInitializeAllMetadata(ShouldInitializeAllMetadata),
-      UsePersistentMetadataIDs(UsePersistentMetadataIDs),
-      TrackPersistentMetadataDefinitions(UsePersistentMetadataIDs) {
-  assert((!ShouldInitializeAllMetadata || !UsePersistentMetadataIDs) &&
+      MetadataMode(MetadataMode) {
+  assert((!ShouldInitializeAllMetadata || !usePersistentMetadataIDs()) &&
          "cannot initialize compact metadata slots with persistent IDs");
+  if (usePersistentMetadataIDs()) {
+    assert(M && "persistent metadata IDs require a module");
+    mdnNext = M->getContext().pImpl->getNextMetadataPrintID();
+  }
 }
 
 // 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,
-                         bool UsePersistentMetadataIDs)
+                         MetadataPrintMode MetadataMode)
     : TheModule(F ? F->getParent() : nullptr), TheFunction(F),
       ShouldInitializeAllMetadata(ShouldInitializeAllMetadata),
-      UsePersistentMetadataIDs(UsePersistentMetadataIDs),
-      TrackPersistentMetadataDefinitions(false) {
-  assert((!ShouldInitializeAllMetadata || !UsePersistentMetadataIDs) &&
+      MetadataMode(MetadataMode) {
+  assert((!ShouldInitializeAllMetadata || !usePersistentMetadataIDs()) &&
          "cannot initialize compact metadata slots with persistent IDs");
+  if (usePersistentMetadataIDs()) {
+    assert(F && "persistent metadata IDs require a function");
+    mdnNext = F->getContext().pImpl->getNextMetadataPrintID();
+  }
 }
 
 SlotTracker::SlotTracker(const ModuleSummaryIndex *Index)
     : TheModule(nullptr), ShouldInitializeAllMetadata(false),
-      UsePersistentMetadataIDs(false),
-      TrackPersistentMetadataDefinitions(false), TheIndex(Index) {}
+      MetadataMode(MetadataPrintMode::Compact), TheIndex(Index) {}
 
 inline void SlotTracker::initializeIfNeeded() {
   if (TheModule) {
@@ -1120,7 +1137,7 @@ void SlotTracker::processModule() {
   for (const GlobalVariable &Var : TheModule->globals()) {
     if (!Var.hasName())
       CreateModuleSlot(&Var);
-    if (!UsePersistentMetadataIDs)
+    if (!usePersistentMetadataIDs())
       processGlobalObjectMetadata(Var);
     auto Attrs = Var.getAttributes();
     if (Attrs.hasAttributes())
@@ -1135,12 +1152,12 @@ void SlotTracker::processModule() {
   for (const GlobalIFunc &I : TheModule->ifuncs()) {
     if (!I.hasName())
       CreateModuleSlot(&I);
-    if (!UsePersistentMetadataIDs)
+    if (!usePersistentMetadataIDs())
       processGlobalObjectMetadata(I);
   }
 
   // Add metadata used by named metadata.
-  if (!UsePersistentMetadataIDs)
+  if (!usePersistentMetadataIDs())
     for (const NamedMDNode &NMD : TheModule->named_metadata())
       for (const MDNode *N : NMD.operands())
         CreateMetadataSlot(N);
@@ -1150,7 +1167,7 @@ void SlotTracker::processModule() {
       // Add all the unnamed functions to the table.
       CreateModuleSlot(&F);
 
-    if (ShouldInitializeAllMetadata && !UsePersistentMetadataIDs)
+    if (ShouldInitializeAllMetadata && !usePersistentMetadataIDs())
       processFunctionMetadata(F);
 
     // Add all the function attributes to the table.
@@ -1172,7 +1189,7 @@ void SlotTracker::processFunction() {
   fNext = 0;
 
   // Process function metadata if it wasn't hit at the module-level.
-  if (!ShouldInitializeAllMetadata && !UsePersistentMetadataIDs)
+  if (!ShouldInitializeAllMetadata && !usePersistentMetadataIDs())
     processFunctionMetadata(*TheFunction);
 
   // Add all the function arguments with no names.
@@ -1346,7 +1363,7 @@ void SlotTracker::setProcessHook(
 
 /// getMetadataSlot - Get the slot number of a MDNode.
 void SlotTracker::createMetadataSlot(const MDNode *N) {
-  if (UsePersistentMetadataIDs)
+  if (usePersistentMetadataIDs())
     CreatePersistentMetadataSlot(N);
   else
     CreateMetadataSlot(N);
@@ -1357,11 +1374,13 @@ int SlotTracker::getMetadataSlot(const MDNode *N) {
   // Check for uninitialized state and do lazy initialization.
   initializeIfNeeded();
 
-  if (UsePersistentMetadataIDs) {
+  if (usePersistentMetadataIDs()) {
     if (isa<DIExpression>(N))
       return -1;
-    if (TrackPersistentMetadataDefinitions)
+    if (trackPersistentMetadataDefinitions() || N->isTemporary())
       CreatePersistentMetadataSlot(N);
+    if (N->isTemporary())
+      return mdnMap.lookup(N);
     return N->getContext().pImpl->getMetadataPrintID(N);
   }
 
@@ -1481,10 +1500,12 @@ void SlotTracker::CreatePersistentMetadataSlot(const MDNode *N) {
   if (isa<DIExpression>(N))
     return;
 
-  unsigned ID = N->getContext().pImpl->getMetadataPrintID(N);
+  unsigned ID =
+      N->isTemporary() ? mdnNext : N->getContext().pImpl->getMetadataPrintID(N);
   if (!mdnMap.try_emplace(N, ID).second)
     return;
-  mdnNext = std::max(mdnNext, ID + 1);
+  if (N->isTemporary())
+    ++mdnNext;
 
   for (const MDOperand &Op : N->operands())
     if (const auto *OpNode = dyn_cast_or_null<MDNode>(Op.get()))
@@ -5167,7 +5188,7 @@ void AssemblyWriter::printUseLists(const Function *F) {
 void Function::print(raw_ostream &ROS, AssemblyAnnotationWriter *AAW,
                      bool ShouldPreserveUseListOrder, bool IsForDebug) const {
   SlotTracker SlotTable(this, /*ShouldInitializeAllMetadata=*/false,
-                        /*UsePersistentMetadataIDs=*/true);
+                        MetadataPrintMode::PersistentReferences);
   formatted_raw_ostream OS(ROS);
   AssemblyWriter W(OS, SlotTable, this->getParent(), AAW, IsForDebug,
                    ShouldPreserveUseListOrder);
@@ -5179,7 +5200,7 @@ void BasicBlock::print(raw_ostream &ROS, AssemblyAnnotationWriter *AAW,
                      bool IsForDebug) const {
   SlotTracker SlotTable(this->getParent(),
                         /*ShouldInitializeAllMetadata=*/false,
-                        /*UsePersistentMetadataIDs=*/true);
+                        MetadataPrintMode::PersistentReferences);
   formatted_raw_ostream OS(ROS);
   AssemblyWriter W(OS, SlotTable, this->getModule(), AAW, IsForDebug,
                    ShouldPreserveUseListOrder);
@@ -5200,7 +5221,7 @@ void Module::printWithPersistentMetadataIDs(raw_ostream &ROS,
                                             bool ShouldPreserveUseListOrder,
                                             bool IsForDebug) const {
   SlotTracker SlotTable(this, /*ShouldInitializeAllMetadata=*/false,
-                        /*UsePersistentMetadataIDs=*/true);
+                        MetadataPrintMode::PersistentDefinitions);
   formatted_raw_ostream OS(ROS);
   AssemblyWriter W(OS, SlotTable, this, AAW, IsForDebug,
                    ShouldPreserveUseListOrder);
diff --git a/llvm/lib/IR/LLVMContextImpl.h b/llvm/lib/IR/LLVMContextImpl.h
index f357d62d71c66..30be08c32afba 100644
--- a/llvm/lib/IR/LLVMContextImpl.h
+++ b/llvm/lib/IR/LLVMContextImpl.h
@@ -1618,6 +1618,8 @@ class LLVMContextImpl {
     return NextMetadataPrintID++;
   }
 
+  uint32_t getNextMetadataPrintID() const { return NextMetadataPrintID; }
+
   uint32_t getMetadataPrintID(const MDNode *N) const {
     assert(!N->isTemporary() && "temporary metadata has no print ID");
     return N->getHeader().MetadataPrintID;
diff --git a/llvm/unittests/IR/AsmWriterTest.cpp b/llvm/unittests/IR/AsmWriterTest.cpp
index e1e74c723664e..5a4421d5daef0 100644
--- a/llvm/unittests/IR/AsmWriterTest.cpp
+++ b/llvm/unittests/IR/AsmWriterTest.cpp
@@ -145,6 +145,47 @@ TEST(AsmWriterTest, PersistentBasicBlockPrint) {
   EXPECT_LT(EarlierID, LaterID);
 }
 
+TEST(AsmWriterTest, PersistentPrintTemporaryMetadata) {
+  LLVMContext Ctx;
+  Module M("test", Ctx);
+  Function *F = Function::Create(
+      FunctionType::get(Type::getVoidTy(Ctx), /*isVarArg=*/false),
+      Function::ExternalLinkage, "f", M);
+  BasicBlock *BB = BasicBlock::Create(Ctx, "entry", F);
+  ReturnInst *Ret = ReturnInst::Create(Ctx, BB);
+
+  MDNode *Permanent = MDNode::getDistinct(Ctx, MDString::get(Ctx, "permanent"));
+  TempMDNode Temporary = MDNode::getTemporary(Ctx, Permanent);
+  Ret->setMetadata("permanent", Permanent);
+  Ret->setMetadata("temporary", Temporary.get());
+
+  std::string FunctionText;
+  raw_string_ostream FunctionOS(FunctionText);
+  F->print(FunctionOS);
+  EXPECT_THAT(FunctionText, HasSubstr("!permanent !0"));
+  EXPECT_THAT(FunctionText, HasSubstr("!temporary !1"));
+
+  std::string BasicBlockText;
+  raw_string_ostream BasicBlockOS(BasicBlockText);
+  BB->print(BasicBlockOS);
+  EXPECT_THAT(BasicBlockText, HasSubstr("!permanent !0"));
+  EXPECT_THAT(BasicBlockText, HasSubstr("!temporary !1"));
+
+  std::string ModuleText;
+  raw_string_ostream ModuleOS(ModuleText);
+  M.printWithPersistentMetadataIDs(ModuleOS);
+  EXPECT_THAT(ModuleText, HasSubstr("!0 = distinct !{!\"permanent\"}"));
+  EXPECT_THAT(ModuleText, HasSubstr("!1 = <temporary!> !{!0}"));
+
+  MDNode *Later = MDNode::getDistinct(Ctx, MDString::get(Ctx, "later"));
+  Ret->setMetadata("later", Later);
+  std::string LaterText;
+  raw_string_ostream LaterOS(LaterText);
+  F->print(LaterOS);
+  EXPECT_THAT(LaterText, HasSubstr("!later !1"));
+  EXPECT_THAT(LaterText, HasSubstr("!temporary !2"));
+}
+
 TEST(AsmWriterTest, PrintAddrspaceWithNullOperand) {
   LLVMContext Ctx;
   Module M("test module", Ctx);

>From dfacb23afcc4c2b223d4df2bd42a49dec8bf8ce6 Mon Sep 17 00:00:00 2001
From: "Yaxun (Sam) Liu" <yaxun.liu at amd.com>
Date: Wed, 19 Aug 2026 14:34:01 -0400
Subject: [PATCH 3/3] [IR] Use fixed-width MDNode header bitfields

Use uint32_t for the existing MDNode header bitfields so the metadata
print ID remains a separate uint32_t field on all hosts.
---
 llvm/include/llvm/IR/Metadata.h | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/llvm/include/llvm/IR/Metadata.h b/llvm/include/llvm/IR/Metadata.h
index b108e42e06ce0..6739400e548aa 100644
--- a/llvm/include/llvm/IR/Metadata.h
+++ b/llvm/include/llvm/IR/Metadata.h
@@ -1079,10 +1079,10 @@ class MDNode : public Metadata {
   /// Explicity set alignment because bitfields by default have an
   /// alignment of 1 on z/OS.
   struct alignas(alignof(size_t)) Header {
-    size_t IsResizable : 1;
-    size_t IsLarge : 1;
-    size_t SmallSize : 4;
-    size_t SmallNumOps : 4;
+    uint32_t IsResizable : 1;
+    uint32_t IsLarge : 1;
+    uint32_t SmallSize : 4;
+    uint32_t SmallNumOps : 4;
     uint32_t MetadataPrintID = 0;
 
     unsigned NumUnresolved = 0;



More information about the llvm-commits mailing list