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

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
c264e07c2f). 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.
This commit is contained in:
Ben Dunbobbin 2026-06-26 16:03:36 +01:00 committed by GitHub
parent d445267fbb
commit fe5f49f84f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 123 additions and 16 deletions

View file

@ -0,0 +1,91 @@
REQUIRES: ld.lld
# Show that DTLTO input file serialization 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 serializes such input bitcode to a
# temporary individual file on disk. This serialization 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 serialization 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 serialized.
# Each backend compilation should produce:
# * a native object
# * a thinlto.bc file
# * a serialized temporary bitcode file (an 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: {{^f[1-3]}}
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: {{^f[1-3]}}
# 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 serialized 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: {{^f[1-3]}}
# 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 serialized
# * cached f1.o must be serialized because f3.o imports from it.
# * cached f2.o should not be serialized 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: {{^f[1-3]}}
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: {{^f[1-3]}}
#--- 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; }

View file

@ -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"
@ -38,11 +39,11 @@ namespace lto {
/// 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().
/// participation and cache status. 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 whose content is
/// serialized later by serializeLTOInputs().
class LLVM_ABI DTLTO : public LTO {
using Base = LTO;
@ -98,16 +99,17 @@ public:
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
/// 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 serializeLTOInputs();
// Remove temporary files created to enable distribution.
void cleanup() override;
@ -163,6 +165,8 @@ public:
private:
// Backend compilation jobs, one per module.
SmallVector<Job> Jobs;
// Input module IDs that must be serialized to individual files.
DenseSet<StringRef> InputModuleIDsToSerialize;
// Task index offset for first ThinLTO job.
unsigned ThinLTOTaskOffset;
// Optional cache for native objects.
@ -210,10 +214,13 @@ private:
/// 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 serialized
/// 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

View file

@ -154,6 +154,10 @@ Error lto::DTLTO::prepareDtltoJob(StringRef ModulePath, unsigned Task) {
if (Error Err = checkCacheHit(J))
return Err;
if (!J.Cached) {
InputModuleIDsToSerialize.insert(J.ModuleID);
for (StringRef ImportPath : J.ImportsFilesList)
InputModuleIDsToSerialize.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;
InputModuleIDsToSerialize.clear();
if (ModuleMap.empty())
return Error::success();

View file

@ -207,15 +207,18 @@ Error lto::DTLTO::serializeLTOInputs() {
for (auto &Input : InputFiles) {
if (!Input->isThinLTO() || !Input->getSerializeForDistribution())
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 (!InputModuleIDsToSerialize.contains(ModuleID))
continue;
TimeTraceScope TimeScope("Serialize 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();
}