[lld] [llvm] [DTLTO] Do not serialize inputs that hit in the ThinLTO cache (PR #204104)

via llvm-commits llvm-commits at lists.llvm.org
Wed Jun 24 04:38:15 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lld

Author: Ben Dunbobbin (bd1976bris)

<details>
<summary>Changes</summary>

To handle bitcode inputs that are not in individual files on disk, such as members of non-thin archives, DTLTO serializes those inputs to temporary individual bitcode files.

This patch changes LLVM to serialize only uncached input modules and any modules they import from.

For a link of Clang 22 (debug build with sanitizers and instrumentation), I performed measurements with and without this patch for an optimized toolchain (PGO non-LTO, based on recent main commit c264e07c2f3d9f25a2526e69926daea3a68be74b). The measurements were run on:
- Windows 11 Pro build 26200, AMD Family 25 at approximately 4.5 GHz,
  16 cores / 32 threads, and 64 GB RAM.
- Ubuntu 24.04.3 LTS, Ryzen 9 5950X with 32 threads, and 62 GiB RAM.

There was no difference in serialization time when the cache was disabled.

When the cache was enabled and all compilations hit in the cache, serialization was eliminated, as was the time spent deleting the previously serialized temporary files, which are no longer created. Mean wall-clock time improved by about 10% on both machines in this scenario.

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


6 Files Affected:

- (added) cross-project-tests/dtlto/cache-extraction.test (+101) 
- (modified) lld/test/ELF/dtlto/timetrace.test (+4-4) 
- (modified) llvm/include/llvm/DTLTO/DTLTO.h (+23-20) 
- (modified) llvm/include/llvm/LTO/LTO.h (+12-11) 
- (modified) llvm/lib/DTLTO/DTLTO.cpp (+7-1) 
- (modified) llvm/lib/DTLTO/DTLTOInputFiles.cpp (+12-10) 


``````````diff
diff --git a/cross-project-tests/dtlto/cache-extraction.test b/cross-project-tests/dtlto/cache-extraction.test
new file mode 100644
index 0000000000000..58428f7bb15eb
--- /dev/null
+++ b/cross-project-tests/dtlto/cache-extraction.test
@@ -0,0 +1,101 @@
+REQUIRES: ld.lld
+
+# Show that DTLTO input file extraction interacts correctly with the
+# ThinLTO cache.
+#
+# To handle bitcode inputs that are not in individual files on disk, such as
+# members of non-thin archives, DTLTO extracts such input bitcode to a
+# temporary individual file on disk. This extraction should only be
+# performed if the backend compilation is required. A backend compilation is
+# only required if there is a miss in the ThinLTO cache for that module, or a
+# module is imported by a module that has missed in the ThinLTO cache.
+#
+# Test DTLTO input extraction across three cases:
+#   1. None of the backend compilations hit in the ThinLTO cache.
+#   2. All of the backend compilations hit in the ThinLTO cache.
+#   3. Some backend compilations hit in the ThinLTO cache and some miss.
+
+RUN: rm -rf %t && split-file %s %t && cd %t
+
+RUN: %clang --target=x86_64-linux-gnu -O2 -flto=thin -c f1.c f2.c
+RUN: llvm-ar rcs f1.a f1.o
+RUN: llvm-ar rcs f2.a f2.o
+
+RUN: mkdir cache.dir
+
+DEFINE: %{link} =  \
+DEFINE:    %clang --target=x86_64-linux-gnu -fuse-ld=lld -shared -nostdlib \
+DEFINE:     -O2 -flto=thin \
+DEFINE:     -fthinlto-distributor=%python \
+DEFINE:     -Xthinlto-distributor=%llvm_src_root/utils/dtlto/local.py \
+DEFINE:     -Wl,--thinlto-cache-dir=cache.dir \
+DEFINE:     -Wl,--save-temps
+
+# 1. None of the backend compilations hit in the ThinLTO cache.
+#
+# Both f1.o and f2.o require backend compilation, so both should be extracted.
+# Each backend compilation should produce:
+#   * a native object
+#   * a thinlto.bc file
+#   * a temporary bitcode file (extracted archive member)
+RUN: mkdir none
+RUN: %{link} -Wl,--whole-archive f1.a f2.a -o none/out.elf
+RUN: ls none | sort | FileCheck %s --check-prefix=NONE
+NONE-NOT: {{^}}f1
+NONE-NOT: {{^}}f2
+NONE-NOT: {{^}}f3
+NONE:      {{^}}f1.a(f1.o at [[#F1_OFFSET:]]).1.[[#%X,HEXPID:]].1.[[#PID:]].native.o{{$}}
+NONE-NEXT: {{^}}f1.a(f1.o at [[#F1_OFFSET]]).1.[[#%X,HEXPID]].1.[[#PID]].native.o.thinlto.bc{{$}}
+NONE-NEXT: {{^}}f1.a(f1.o at [[#F1_OFFSET]]).1.[[#%X,HEXPID]].o{{$}}
+NONE-NEXT: {{^}}f2.a(f2.o at [[#F2_OFFSET:]]).2.[[#%X,HEXPID]].2.[[#PID]].native.o{{$}}
+NONE-NEXT: {{^}}f2.a(f2.o at [[#F2_OFFSET]]).2.[[#%X,HEXPID]].2.[[#PID]].native.o.thinlto.bc{{$}}
+NONE-NEXT: {{^}}f2.a(f2.o at [[#F2_OFFSET]]).2.[[#%X,HEXPID]].o{{$}}
+NONE-NOT: {{^}}f1
+NONE-NOT: {{^}}f2
+NONE-NOT: {{^}}f3
+
+# 2. All of the backend compilations hit in the ThinLTO cache.
+#
+# Both f1.o and f2.o are satisfied from the ThinLTO cache. No backend
+# compilation runs, so there should be no .native.o files, no .thinlto.bc files,
+# and no extracted archive-member bitcode files.
+RUN: mkdir all
+RUN: %{link} -Wl,--whole-archive f1.a f2.a -o all/out.elf
+RUN: ls all | sort | FileCheck %s --check-prefix=ALL
+ALL-NOT: {{^}}f1
+ALL-NOT: {{^}}f2
+ALL-NOT: {{^}}f3
+
+# 3. Some backend compilations hit in the ThinLTO cache and some miss.
+#
+# f1.o and f2.o are cached, but the new f3.o misses the cache and imports
+# from f1.o:
+#   * f3.o must be compiled and extracted
+#   * cached f1.o must be extracted because f3.o imports from it.
+#   * cached f2.o should not be extracted or compiled.
+RUN: %clang --target=x86_64-linux-gnu -O2 -flto=thin -c f3.c
+RUN: llvm-ar rcs f3.a f3.o
+RUN: mkdir some
+RUN: %{link} -Wl,--whole-archive f1.a f2.a f3.a -o some/out.elf
+RUN: ls some | sort | FileCheck %s --check-prefix=SOME
+SOME-NOT: {{^}}f1
+SOME-NOT: {{^}}f2
+SOME-NOT: {{^}}f3
+SOME:      {{^}}f1.a(f1.o at [[#F1_OFFSET:]]).1.[[#%X,HEXPID:]].o{{$}}
+SOME-NEXT: {{^}}f3.a(f3.o at [[#F3_OFFSET:]]).3.[[#%X,HEXPID]].3.[[#PID:]].native.o{{$}}
+SOME-NEXT: {{^}}f3.a(f3.o at [[#F3_OFFSET]]).3.[[#%X,HEXPID]].3.[[#PID]].native.o.thinlto.bc{{$}}
+SOME-NEXT: {{^}}f3.a(f3.o at [[#F3_OFFSET]]).3.[[#%X,HEXPID]].o{{$}}
+SOME-NOT: {{^}}f1
+SOME-NOT: {{^}}f2
+SOME-NOT: {{^}}f3
+
+#--- f1.c
+__attribute__((retain)) int f1(int x) { return x; }
+
+#--- f2.c
+extern int f1(int);
+__attribute__((retain)) int f2(int x) { return f1(x) + x; }
+
+#--- f3.c
+extern int f1(int);
+__attribute__((retain)) int f3(int x) { return f1(x) + x; }
diff --git a/lld/test/ELF/dtlto/timetrace.test b/lld/test/ELF/dtlto/timetrace.test
index f43f51284b5c4..47b8d3ea68e66 100644
--- a/lld/test/ELF/dtlto/timetrace.test
+++ b/lld/test/ELF/dtlto/timetrace.test
@@ -32,14 +32,14 @@ RUN: %python filter_order_and_pprint.py %t.json | FileCheck %s
 ## Check that DTLTO add input file events are recorded.
 CHECK:      "name": "Add input for DTLTO"
 CHECK:      "name": "Add input for DTLTO"
-CHECK:      "name": "Remove DTLTO temporary files"
-CHECK:      "name": "Serialize bitcode input for DTLTO"
+CHECK:      "name": "Extract bitcode input for DTLTO"
 CHECK-SAME:   "detail": "t1.a(t1.bc at [[#ARCHIVE_OFFSET:]]).1.[[PID:[A-F0-9]+]].o"
+CHECK:      "name": "Remove DTLTO temporary files"
 CHECK:      "name": "Total Add input for DTLTO"
 CHECK-SAME:   "count": 2,
-CHECK:      "name": "Total Remove DTLTO temporary files"
+CHECK:      "name": "Total Extract bitcode input for DTLTO"
 CHECK-SAME:   "count": 1,
-CHECK:      "name": "Total Serialize bitcode input for DTLTO"
+CHECK:      "name": "Total Remove DTLTO temporary files"
 CHECK-SAME:   "count": 1,
 
 #--- t1.ll
diff --git a/llvm/include/llvm/DTLTO/DTLTO.h b/llvm/include/llvm/DTLTO/DTLTO.h
index 54e4a414dae2a..2d532bc384685 100644
--- a/llvm/include/llvm/DTLTO/DTLTO.h
+++ b/llvm/include/llvm/DTLTO/DTLTO.h
@@ -16,6 +16,7 @@
 #ifndef LLVM_DTLTO_DTLTO_H
 #define LLVM_DTLTO_DTLTO_H
 
+#include "llvm/ADT/DenseSet.h"
 #include "llvm/ADT/SmallString.h"
 #include "llvm/LTO/LTO.h"
 #include "llvm/Support/ManagedStatic.h"
@@ -27,22 +28,19 @@
 namespace llvm {
 namespace lto {
 
-/// Prepares inputs for Distributed ThinLTO so that backend compilations can use
+/// Prepares inputs for Distributed ThinLTO so that backend compilations use
 /// individual bitcode paths and consistent module IDs.
 ///
 /// Each input must exist as an individual bitcode file on disk and be loadable
-/// via its ModuleID. Archive members and FatLTO objects do not satisfy that by
-/// default; this class writes bitcode out when needed and updates ModuleID.
-/// On Windows, module IDs are normalized to remove short 8.3 path components
+/// via its ModuleID. Archive members and FatLTO objects do not satisfy this
+/// requirement. For these inputs, this class extracts the individual bitcode
+/// to an individual temporary file, and updates ModuleID to that path. On
+/// Windows, module IDs are normalized to remove short 8.3 path components
 /// that are machine-local and break distribution; other normalization is left
 /// to DTLTO distributors.
 ///
-/// Input files are kept until the pipeline has determined per-module ThinLTO
-/// participation. addInput() performs: (1) register the input; (2) on Windows,
-/// normalize module ID for standalone bitcode; (3) for thin archive members,
-/// set module ID to the on-disk member path; (4) for other archives and FatLTO,
-/// set module ID to a unique path and serialize content in
-/// serializeLTOInputs().
+/// Input files are kept alive until the pipeline has determined per-module
+/// ThinLTO participation and cache status, see addInput() for details.
 class LLVM_ABI DTLTO : public LTO {
   using Base = LTO;
 
@@ -82,8 +80,7 @@ class LLVM_ABI DTLTO : public LTO {
   ///    (normalized on Windows) to the member file on disk.
   /// 4. For archive members and FatLTO objects, overwrite the module ID with a
   ///    unique path (normalized on Windows) naming a file that will contain the
-  ///    member content. The file is created and populated later (see
-  ///    serializeInputs()).
+  ///    content. The file is created/populated later (see extractLTOInputs()).
   Expected<std::shared_ptr<InputFile>>
   addInput(std::unique_ptr<InputFile> InputPtr) override;
 
@@ -98,16 +95,17 @@ class LLVM_ABI DTLTO : public LTO {
   virtual Error run(AddStreamFn AddStream, FileCache Cache = {}) override;
 
 private:
-  /// DTLTO archives support.
+  /// DTLTO archive support.
   ///
-  /// Save the contents of ThinLTO-enabled input files that must be serialized
+  /// Save the contents of ThinLTO-enabled input files that must be extracted
   /// for distribution, such as archive members and FatLTO objects, to
   /// individual bitcode files named after the module ID.
   ///
-  /// Must be called after all input files are added but before optimization
-  /// begins. If a file with that name already exists, it is likely a leftover
-  /// from a previously terminated linker process and can be safely overwritten.
-  Error serializeLTOInputs();
+  /// Must be called after all input files are added and cache hits are known,
+  /// but before optimization begins. Existing files are overwritten because
+  /// they are likely leftovers from a previously terminated linker process and
+  /// can be safely replaced.
+  LLVM_ABI Error extractLTOInputs();
 
   // Remove temporary files created to enable distribution.
   void cleanup() override;
@@ -163,6 +161,8 @@ class LLVM_ABI DTLTO : public LTO {
 private:
   // Backend compilation jobs, one per module.
   SmallVector<Job> Jobs;
+  // Input module IDs that must be extracted to individual files.
+  DenseSet<StringRef> InputModuleIDsToExtract;
   // Task index offset for first ThinLTO job.
   unsigned ThinLTOTaskOffset;
   // Optional cache for native objects.
@@ -210,10 +210,13 @@ class LLVM_ABI DTLTO : public LTO {
   ///    distributor will skip this job. On a cache miss, J.CacheAddStream is
   ///    set for later use when storing the compiled object.
   ///
-  /// 4. Writes the per-module summary index to disk only on cache miss. The
+  /// 4. Records the module ID and imported module IDs that must be extracted
+  ///    to individual files.
+  ///
+  /// 5. Writes the per-module summary index to disk only on cache miss. The
   ///    remote compiler will read this via -fthinlto-index=.
   ///
-  /// 5. Registers the job's temporary files for removal on abnormal process
+  /// 6. Registers the job's temporary files for removal on abnormal process
   ///    exit when SaveTemps is false (only for files that will be created).
   ///
   /// \param ModulePath The module identifier (bitcode path) for the ThinLTO
diff --git a/llvm/include/llvm/LTO/LTO.h b/llvm/include/llvm/LTO/LTO.h
index d6b6864047eac..0951039b310bd 100644
--- a/llvm/include/llvm/LTO/LTO.h
+++ b/llvm/include/llvm/LTO/LTO.h
@@ -134,11 +134,12 @@ class InputFile {
 
   MemoryBufferRef MbRef;
   bool IsFatLTOObject = false;
-  // For distributed compilation, each input must exist as an individual bitcode
-  // file on disk and be identified by its ModuleID. Archive members and FatLTO
-  // objects violate this. So, in these cases we flag that the bitcode must be
-  // written out to a new standalone file.
-  bool SerializeForDistribution = false;
+  // For distributed compilation, each input must exist as an individual
+  // bitcode file on disk identified by its ModuleID. For archive members and
+  // FatLTO objects, the input bitcode is a sub-section of a larger file. In
+  // these cases we flag that the bitcode must be written to a temporary
+  // standalone file. Effectively, extracted from its container.
+  bool ExtractForDistribution = false;
   bool IsThinLTO = false;
   StringRef ArchivePath;
   StringRef MemberName;
@@ -212,12 +213,12 @@ class InputFile {
   LLVM_ABI BitcodeModule &getPrimaryBitcodeModule();
   // Returns the memory buffer reference for this input file.
   MemoryBufferRef getFileBuffer() const { return MbRef; }
-  // Returns true if this input should be serialized to disk for distribution.
-  // See the comment on SerializeForDistribution for details.
-  bool getSerializeForDistribution() const { return SerializeForDistribution; }
-  // Mark whether this input should be serialized to disk for distribution.
-  // See the comment on SerializeForDistribution for details.
-  void setSerializeForDistribution(bool SFD) { SerializeForDistribution = SFD; }
+  // Returns true if this input should be extracted to disk for distribution.
+  // See the comment on ExtractForDistribution for details.
+  bool getExtractForDistribution() const { return ExtractForDistribution; }
+  // Mark whether this input should be extracted to disk for distribution.
+  // See the comment on ExtractForDistribution for details.
+  void setExtractForDistribution(bool EFD) { ExtractForDistribution = EFD; }
   // Returns true if this bitcode came from a FatLTO object.
   bool isFatLTOObject() const { return IsFatLTOObject; }
   // Mark this bitcode as coming from a FatLTO object.
diff --git a/llvm/lib/DTLTO/DTLTO.cpp b/llvm/lib/DTLTO/DTLTO.cpp
index 162f4a60ab9f9..bca05fa53e889 100644
--- a/llvm/lib/DTLTO/DTLTO.cpp
+++ b/llvm/lib/DTLTO/DTLTO.cpp
@@ -90,7 +90,7 @@ LLVM_ABI Error lto::DTLTO::run(AddStreamFn AddStream, FileCache CacheParam) {
 
   if (Error Err = prepareDtltoJobs())
     return Err;
-  if (Error Err = serializeLTOInputs())
+  if (Error Err = extractLTOInputs())
     return Err;
   if (Error Err = performCodegen())
     return Err;
@@ -154,6 +154,10 @@ Error lto::DTLTO::prepareDtltoJob(StringRef ModulePath, unsigned Task) {
   if (Error Err = checkCacheHit(J))
     return Err;
   if (!J.Cached) {
+    InputModuleIDsToExtract.insert(J.ModuleID);
+    for (StringRef ImportPath : J.ImportsFilesList)
+      InputModuleIDsToExtract.insert(ImportPath);
+
     TimeTraceScope JobScope("Emit individual index for DTLTO",
                             J.SummaryIndexPath);
     if (Error Err = save(SummaryIndexFiles[Task], J.SummaryIndexPath))
@@ -225,6 +229,8 @@ Error lto::DTLTO::prepareDtltoJobs() {
   auto &ModuleMap =
       ThinLTO.ModulesToCompile ? *ThinLTO.ModulesToCompile : ThinLTO.ModuleMap;
 
+  InputModuleIDsToExtract.clear();
+
   if (ModuleMap.empty())
     return Error::success();
 
diff --git a/llvm/lib/DTLTO/DTLTOInputFiles.cpp b/llvm/lib/DTLTO/DTLTOInputFiles.cpp
index 8751cb14d7a06..b940bad4b8635 100644
--- a/llvm/lib/DTLTO/DTLTOInputFiles.cpp
+++ b/llvm/lib/DTLTO/DTLTOInputFiles.cpp
@@ -163,8 +163,7 @@ lto::DTLTO::addInput(std::unique_ptr<InputFile> InputPtr) {
   }
 
   // For a member of a thin archive that is not a FatLTO object, there is an
-  // existing file on disk that can be used, so we can avoid having to
-  // serialize.
+  // existing file on disk that can be used, so we can avoid having to extract.
   Expected<bool> UseThinMember =
       Input->isFatLTOObject() ? false : isThinArchive(ArchivePath);
   if (!UseThinMember)
@@ -179,7 +178,7 @@ lto::DTLTO::addInput(std::unique_ptr<InputFile> InputPtr) {
   }
 
   // A new file on disk will be needed for archive members and FatLTO objects.
-  Input->setSerializeForDistribution(true);
+  Input->setExtractForDistribution(true);
 
   // Get the normalized output directory, if we haven't already.
   if (LinkerOutputDir.empty()) {
@@ -201,21 +200,24 @@ lto::DTLTO::addInput(std::unique_ptr<InputFile> InputPtr) {
   return Input;
 }
 
-// Save the contents of ThinLTO-enabled input files that must be serialized for
+// Save the contents of ThinLTO-enabled input files that must be extracted for
 // distribution.
-Error lto::DTLTO::serializeLTOInputs() {
+Error lto::DTLTO::extractLTOInputs() {
   for (auto &Input : InputFiles) {
-    if (!Input->isThinLTO() || !Input->getSerializeForDistribution())
+    if (!Input->isThinLTO() || !Input->getExtractForDistribution())
       continue;
+
     // Save the content of the input file to a file named after the module ID.
-    StringRef ModuleId = Input->getName();
-    TimeTraceScope TimeScope("Serialize bitcode input for DTLTO", ModuleId);
+    StringRef ModuleID = Input->getName();
+    if (!InputModuleIDsToExtract.contains(ModuleID))
+      continue;
+    TimeTraceScope TimeScope("Extract bitcode input for DTLTO", ModuleID);
     MemoryBufferRef Buf = Input->getFileBuffer();
-    if (Error Err = save(Buf.getBuffer(), ModuleId))
+    if (Error Err = save(Buf.getBuffer(), ModuleID))
       return Err;
     // Cleanup this file on abnormal process exit.
     if (!SaveTemps)
-      addToCleanup(ModuleId);
+      addToCleanup(ModuleID);
   }
   return Error::success();
 }

``````````

</details>


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


More information about the llvm-commits mailing list