[llvm] [DWARFLinker] Deduplicate .debug_frame CIEs in the parallel linker (PR #195393)

via llvm-commits llvm-commits at lists.llvm.org
Fri May 1 17:45:54 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-debuginfo

Author: Jonas Devlieghere (JDevlieghere)

<details>
<summary>Changes</summary>

Each LinkContext held its own EmittedCIEs map, so linking the same object twice (or two objects with identical CIEs) produced one CIE per LinkContext instead of one shared CIE.

Hoist the registry to linker scope and emit .debug_frame serially in ObjectContexts order. Because only LinkContexts emit into .debug_frame, the serial order matches the final section layout, so a CIE's owning LinkContext always has a StartOffset <= every referencing FDE's LinkContext. This guarantees that the CIE appears before its FDEs in the output. The registry records the CIE's owning section and local offset. FDEs register a cross-section DebugOffsetPatch so the CIE pointer resolves to OwnerStartOffset + LocalOffset at output time.

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


6 Files Affected:

- (modified) llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.cpp (+47-40) 
- (modified) llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.h (+13-1) 
- (removed) llvm/test/tools/dsymutil/X86/DWARFLinkerParallel/frame-1.test (-36) 
- (removed) llvm/test/tools/dsymutil/X86/DWARFLinkerParallel/frame-2.test (-46) 
- (modified) llvm/test/tools/dsymutil/X86/frame-1.test (+1-2) 
- (modified) llvm/test/tools/dsymutil/X86/frame-2.test (+1-2) 


``````````diff
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.cpp b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.cpp
index 238756a721a1b..b94241b6681fb 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.cpp
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.cpp
@@ -186,8 +186,6 @@ Error DWARFLinkerImpl::link() {
       // Link object file.
       if (Error Err = Context->link(ArtificialTypeUnit.get()))
         GlobalData.error(std::move(Err), Context->InputDWARFFile.FileName);
-
-      Context->InputDWARFFile.unload();
     }
   } else {
     DefaultThreadPool Pool(llvm::parallel::strategy);
@@ -196,13 +194,36 @@ Error DWARFLinkerImpl::link() {
         // Link object file.
         if (Error Err = Context->link(ArtificialTypeUnit.get()))
           GlobalData.error(std::move(Err), Context->InputDWARFFile.FileName);
-
-        Context->InputDWARFFile.unload();
       });
 
     Pool.wait();
   }
 
+  // Emit .debug_frame serially in ObjectContexts order so that CIEs are
+  // deduplicated across LinkContexts while still appearing before any FDE
+  // that references them in the final output. Only LinkContext instances
+  // emit into .debug_frame, so iterating ObjectContexts here matches the
+  // final section layout produced by forEachObjectSectionsSet(). Each
+  // context's input DWARF is unloaded as soon as its frame data has been
+  // consumed to limit memory usage.
+  if (!GlobalData.getOptions().UpdateIndexTablesOnly) {
+    LinkContext::CIERegistry CIEs;
+    llvm::parallel::TaskGroup TGroup;
+    // Use a task group here so per-thread allocators used by downstream
+    // emission code are called from the ThreadPoolExecutor threads.
+    TGroup.spawn([&]() {
+      for (std::unique_ptr<LinkContext> &Context : ObjectContexts) {
+        if (!Context->CompileUnits.empty())
+          if (Error Err = Context->cloneAndEmitDebugFrame(CIEs))
+            GlobalData.error(std::move(Err), Context->InputDWARFFile.FileName);
+        Context->InputDWARFFile.unload();
+      }
+    });
+  } else {
+    for (std::unique_ptr<LinkContext> &Context : ObjectContexts)
+      Context->InputDWARFFile.unload();
+  }
+
   if (ArtificialTypeUnit != nullptr && !ArtificialTypeUnit->getTypePool()
                                             .getRoot()
                                             ->getValue()
@@ -581,18 +602,6 @@ Error DWARFLinkerImpl::LinkContext::link(TypeUnit *ArtificialTypeUnit) {
 
     if (Error Err = emitInvariantSections())
       return Err;
-  } else if (!CompileUnits.empty()) {
-    // Emit .debug_frame section.
-
-    Error ResultErr = Error::success();
-    llvm::parallel::TaskGroup TGroup;
-    // We use task group here as PerThreadBumpPtrAllocator should be called from
-    // the threads created by ThreadPoolExecutor.
-    TGroup.spawn([&]() {
-      if (Error Err = cloneAndEmitDebugFrame())
-        ResultErr = std::move(Err);
-    });
-    return ResultErr;
   }
 
   return Error::success();
@@ -738,7 +747,7 @@ Error DWARFLinkerImpl::LinkContext::emitInvariantSections() {
   return Error::success();
 }
 
-Error DWARFLinkerImpl::LinkContext::cloneAndEmitDebugFrame() {
+Error DWARFLinkerImpl::LinkContext::cloneAndEmitDebugFrame(CIERegistry &CIEs) {
   if (!GlobalData.getTargetTriple().has_value())
     return Error::success();
 
@@ -765,14 +774,9 @@ Error DWARFLinkerImpl::LinkContext::cloneAndEmitDebugFrame() {
   DataExtractor Data(OrigFrameData, InputDWARFObj.isLittleEndian(), 0);
   uint64_t InputOffset = 0;
 
-  // Store the data of the CIEs defined in this object, keyed by their
-  // offsets.
+  // CIEs defined in this input object, keyed by their input offsets.
   DenseMap<uint64_t, StringRef> LocalCIES;
 
-  /// The CIEs that have been emitted in the output section. The actual CIE
-  /// data serves a the key to this StringMap.
-  StringMap<uint32_t> EmittedCIEs;
-
   while (Data.isValidOffset(InputOffset)) {
     uint64_t EntryOffset = InputOffset;
     uint32_t InitialLength = Data.getU32(&InputOffset);
@@ -806,7 +810,6 @@ Error DWARFLinkerImpl::LinkContext::cloneAndEmitDebugFrame() {
     }
 
     // This is an FDE, and we have a mapping.
-    // Have we already emitted a corresponding CIE?
     StringRef CIEData = LocalCIES[CIEId];
     if (CIEData.empty())
       return createFileError(
@@ -814,25 +817,29 @@ Error DWARFLinkerImpl::LinkContext::cloneAndEmitDebugFrame() {
           createStringError(std::errc::invalid_argument,
                             "Inconsistent debug_frame content. Dropping."));
 
-    uint64_t OffsetToCIERecord = OutSection.OS.tell();
-
-    // Look if we already emitted a CIE that corresponds to the
-    // referenced one (the CIE data is the key of that lookup).
-    auto IteratorInserted =
-        EmittedCIEs.insert(std::make_pair(CIEData, OffsetToCIERecord));
-    OffsetToCIERecord = IteratorInserted.first->getValue();
-
-    // Emit CIE for this ID if it is not emitted yet.
-    if (IteratorInserted.second)
+    // Check whether an identical CIE has already been emitted (possibly by
+    // an earlier LinkContext). If not, emit it into this section and record
+    // the location in the shared registry.
+    dwarf::FormParams FP = OutSection.getFormParams();
+    uint64_t LocalOffset = OutSection.OS.tell();
+    if (LocalOffset > FP.getDwarfMaxOffset())
+      return createFileError(
+          InputDWARFObj.getFileName(),
+          createStringError(".debug_frame section offset "
+                            "0x" +
+                            Twine::utohexstr(LocalOffset) + " exceeds the " +
+                            dwarf::FormatString(FP.Format) + " limit"));
+    auto [It, Inserted] = CIEs.try_emplace(CIEData, &OutSection,
+                                           static_cast<uint32_t>(LocalOffset));
+    if (Inserted)
       OutSection.OS << CIEData;
+    SectionDescriptor *CIEOwnerSection = It->second.first;
+    uint32_t OffsetToCIERecord = It->second.second;
 
-    // Remember offset to the FDE record, so that we might update
-    // field referencing CIE record(containing OffsetToCIERecord),
-    // when final offsets are known. OffsetToCIERecord(which is written later)
-    // is local to the current .debug_frame section, it should be updated
-    // with final offset of the .debug_frame section.
+    // The FDE's CIE pointer will be patched at output time to
+    // CIEOwnerSection->StartOffset + OffsetToCIERecord.
     OutSection.notePatch(
-        DebugOffsetPatch{OutSection.OS.tell() + 4, &OutSection, true});
+        DebugOffsetPatch{OutSection.OS.tell() + 4, CIEOwnerSection, true});
 
     // Emit the FDE with updated address and CIE pointer.
     // (4 + AddrSize) is the size of the CIEId + initial_location
diff --git a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.h b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.h
index 2dba5df559375..adc0d62659ad6 100644
--- a/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.h
+++ b/llvm/lib/DWARFLinker/Parallel/DWARFLinkerImpl.h
@@ -244,6 +244,18 @@ class DWARFLinkerImpl : public DWARFLinker {
       return Size;
     }
 
+    /// Shared registry for .debug_frame CIEs that have already been emitted
+    /// by some LinkContext. The key is the raw CIE bytes; the value is the
+    /// SectionDescriptor that owns the emitted CIE and its local offset
+    /// within that section. Populated serially in ObjectContexts order so
+    /// that the first LinkContext to reference a CIE is its owner, which
+    /// guarantees the CIE appears before any referencing FDE in the
+    /// concatenated output. SectionDescriptor pointers remain valid until
+    /// linking completes because they live in std::map-held shared_ptrs.
+    ///
+    /// Not thread-safe: callers must serialize all access.
+    using CIERegistry = StringMap<std::pair<SectionDescriptor *, uint32_t>>;
+
     /// Link compile units for this context.
     Error link(TypeUnit *ArtificialTypeUnit);
 
@@ -256,7 +268,7 @@ class DWARFLinkerImpl : public DWARFLinker {
     Error emitInvariantSections();
 
     /// Clone and emit .debug_frame.
-    Error cloneAndEmitDebugFrame();
+    Error cloneAndEmitDebugFrame(CIERegistry &CIEs);
 
     /// Emit FDE record.
     void emitFDE(uint32_t CIEOffset, uint32_t AddrSize, uint64_t Address,
diff --git a/llvm/test/tools/dsymutil/X86/DWARFLinkerParallel/frame-1.test b/llvm/test/tools/dsymutil/X86/DWARFLinkerParallel/frame-1.test
deleted file mode 100644
index 6b02f32f8541c..0000000000000
--- a/llvm/test/tools/dsymutil/X86/DWARFLinkerParallel/frame-1.test
+++ /dev/null
@@ -1,36 +0,0 @@
-# RUN: rm -rf %t
-# RUN: mkdir -p %t
-# RUN: llc -filetype=obj %p/../../Inputs/frame-dw2.ll -o %t/frame-dw2.o
-# RUN: dsymutil --linker parallel -f -oso-prepend-path=%t -y %s -o - | \
-# RUN:   llvm-dwarfdump -debug-frame - | FileCheck %s
-
-# This test is meant to verify that identical CIEs will get reused
-# in the same file but not inbetween files. For this to happen, we
-# link twice the same file using this made-up debug map:
-
----
-triple:          'i386-apple-darwin'
-objects:
-  - filename: frame-dw2.o
-    symbols:
-      - { sym: _bar, objAddr: 0x0, binAddr: 0x1000, size: 0x12 }
-      - { sym: _baz, objAddr: 0x0, binAddr: 0x2000, size: 0x12 }
-  - filename: frame-dw2.o
-    symbols:
-      - { sym: _bar, objAddr: 0x0, binAddr: 0x3000, size: 0x12 }
-      - { sym: _baz, objAddr: 0x0, binAddr: 0x4000, size: 0x12 }
-...
-
-# CHECK: .debug_frame contents:
-# CHECK: 00000000 {{[0-9a-f]*}} ffffffff CIE
-# CHECK-NOT: FDE
-# CHECK:  FDE cie=00000000 pc=00001000...00001
-# CHECK-NOT: FDE
-# CHECK:  FDE cie=00000000 pc=00002000...00002
-# CHECK:  [[CIECU2:[0-9a-f]*]] {{[0-9a-f]*}} ffffffff CIE
-# CHECK-NOT: FDE
-# CHECK:  FDE cie=[[CIECU2]] pc=00003000...00003
-# CHECK-NOT: FDE
-# CHECK:  FDE cie=[[CIECU2]] pc=00004000...00004
-# CHECK-NOT: FDE
-# CHECK: .eh_frame contents:
diff --git a/llvm/test/tools/dsymutil/X86/DWARFLinkerParallel/frame-2.test b/llvm/test/tools/dsymutil/X86/DWARFLinkerParallel/frame-2.test
deleted file mode 100644
index 871c5d0fa40b8..0000000000000
--- a/llvm/test/tools/dsymutil/X86/DWARFLinkerParallel/frame-2.test
+++ /dev/null
@@ -1,46 +0,0 @@
-# RUN: rm -rf %t
-# RUN: mkdir -p %t
-# RUN: llc -filetype=obj %p/../../Inputs/frame-dw2.ll -o %t/frame-dw2.o
-# RUN: llc -filetype=obj %p/../../Inputs/frame-dw4.ll -o %t/frame-dw4.o
-# RUN: dsymutil --linker parallel -f -oso-prepend-path=%t -y %s -o - | \
-# RUN:   llvm-dwarfdump -debug-frame - | FileCheck %s
-
-# Check the handling of multiple different CIEs. To have CIEs that
-# appear to be different, use a dwarf2 version of the file along with
-# a dwarf 4 version. The CIE header version (and layout) will be different.
----
-triple:          'i386-apple-darwin'
-objects:
-  - filename: frame-dw2.o
-    symbols:
-      - { sym: _bar, objAddr: 0x0, binAddr: 0x1000, size: 0x12 }
-      - { sym: _baz, objAddr: 0x0, binAddr: 0x2000, size: 0x12 }
-  - filename: frame-dw4.o
-    symbols:
-      - { sym: _baz, objAddr: 0x0, binAddr: 0x3000, size: 0x12 }
-  - filename: frame-dw2.o
-    symbols:
-      - { sym: _bar, objAddr: 0x0, binAddr: 0x4000, size: 0x12 }
-...
-
-# CHECK: .debug_frame contents:
-# CHECK: 00000000 {{[0-9a-f]*}} ffffffff CIE
-# CHECK-NEXT: Format: DWARF32
-# CHECK-NEXT: Version:{{.*}}1
-# CHECK-NOT: FDE
-# CHECK:  FDE cie=00000000 pc=00001000...00001
-# CHECK-NOT: FDE
-# CHECK:  FDE cie=00000000 pc=00002000...00002
-# CHECK-NOT: FDE
-# CHECK: [[CIEDW4:[0-9a-f]*]] 00000010 ffffffff CIE
-# CHECK-NEXT: Format: DWARF32
-# CHECK-NEXT: Version:{{.*}}4
-# CHECK-NOT: FDE
-# CHECK:  FDE cie=[[CIEDW4]] pc=00003000...00003
-# CHECK: [[CIEDW1_2:[0-9a-f]*]] 00000010 ffffffff CIE
-# CHECK-NEXT: Format: DWARF32
-# CHECK-NEXT: Version:{{.*}}1
-# CHECK-NOT: FDE
-# CHECK:  FDE cie=[[CIEDW1_2]] pc=00004000...00004
-# CHECK-NOT: FDE
-# CHECK: .eh_frame contents:
diff --git a/llvm/test/tools/dsymutil/X86/frame-1.test b/llvm/test/tools/dsymutil/X86/frame-1.test
index e2d72928b728f..36223d56be4c1 100644
--- a/llvm/test/tools/dsymutil/X86/frame-1.test
+++ b/llvm/test/tools/dsymutil/X86/frame-1.test
@@ -2,6 +2,7 @@
 # RUN: mkdir -p %t
 # RUN: llc -filetype=obj %p/../Inputs/frame-dw2.ll -o %t/frame-dw2.o
 # RUN: dsymutil --linker classic -f -oso-prepend-path=%t -y %s -o - | llvm-dwarfdump -debug-frame - | FileCheck %s
+# RUN: dsymutil --linker parallel -f -oso-prepend-path=%t -y %s -o - | llvm-dwarfdump -debug-frame - | FileCheck %s
 
 # This test is meant to verify that identical CIEs will get reused
 # in the same file but also inbetween files. For this to happen, we
@@ -29,5 +30,3 @@ objects:
 # CHECK:  FDE cie=00000000 pc=00003000...00003
 # CHECK-NOT: FDE
 # CHECK: .eh_frame contents:
-
-## FIXME: Support --linker parallel
diff --git a/llvm/test/tools/dsymutil/X86/frame-2.test b/llvm/test/tools/dsymutil/X86/frame-2.test
index 4db89ea815f71..0e50c8e72bb7c 100644
--- a/llvm/test/tools/dsymutil/X86/frame-2.test
+++ b/llvm/test/tools/dsymutil/X86/frame-2.test
@@ -3,6 +3,7 @@
 # RUN: llc -filetype=obj %p/../Inputs/frame-dw2.ll -o %t/frame-dw2.o
 # RUN: llc -filetype=obj %p/../Inputs/frame-dw4.ll -o %t/frame-dw4.o
 # RUN: dsymutil --linker classic -f -oso-prepend-path=%t -y %s -o - | llvm-dwarfdump -debug-frame - | FileCheck %s
+# RUN: dsymutil --linker parallel -f -oso-prepend-path=%t -y %s -o - | llvm-dwarfdump -debug-frame - | FileCheck %s
 
 # Check the handling of multiple different CIEs. To have CIEs that
 # appear to be different, use a dwarf2 version of the file along with
@@ -39,5 +40,3 @@ objects:
 # CHECK:  FDE cie=00000000 pc=00004000...00004
 # CHECK-NOT: FDE
 # CHECK: .eh_frame contents:
-
-## FIXME: Support --linker parallel

``````````

</details>


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


More information about the llvm-commits mailing list