[DTLTO] Improve performance of adding files to the link (#186366)

The in-process ThinLTO backend typically generates object files in
memory and adds them directly to the link, except when the ThinLTO cache
is in use. DTLTO is unusual in that it adds files to the link from disk
in all cases.

When the ThinLTO cache is not in use, ThinLTO adds files via an
`AddStreamFn` callback provided by the linker, which ultimately appends
to a `SmallVector` in LLD. When the cache is in use, the linker supplies
an `AddBufferFn` callback that adds files more efficiently (by moving
`MemoryBuffer` ownership).

This patch adds a mandatory `AddBufferFn` to the DTLTO ThinLTO backend.
The backend uses this to add files to the link more efficiently.
Additionally:
- Move AddStream from CGThinBackend to InProcessThinBackend, for reader
  clarity.
- Modify linker comments that implied the AddBuffer path is
  cache-specific.

For a Clang link (Debug build with sanitizers and instrumentation) using
an optimized toolchain (PGO non-LTO, llvmorg-22.1.0), measuring the mean
`Add DTLTO files to the link` time trace scope duration:
- On Windows (Windows 11 Pro Build 26200, AMD Family 25 @ ~4.5 GHz, 16
  cores/32 threads, 64 GB RAM), this patch reduces the mean from
  2799.148 ms to 157.972 ms.
- On Linux (Ubuntu 24.04.3 LTS Kernel 6.14, Ryzen 9 5950X, 16
  cores/32 threads, boost up to 5.09 GHz, 64 GB RAM), this patch reduces
  the mean from 255.291 ms to 41.630 ms.

Based on work by @romanova-ekaterina and @kbelochapka.
This commit is contained in:
Ben Dunbobbin 2026-03-27 17:51:49 +00:00 committed by GitHub
parent d271bd37ce
commit 80b304d14b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 85 additions and 67 deletions

View File

@ -39,6 +39,16 @@ using namespace llvm::object;
using namespace lld;
using namespace lld::coff;
static AddBufferFn
createAddBufferFn(std::vector<std::unique_ptr<MemoryBuffer>> &files,
std::vector<std::string> &filenames) {
return [&files, &filenames](unsigned task, const Twine &moduleName,
std::unique_ptr<MemoryBuffer> mb) {
files[task] = std::move(mb);
filenames[task] = moduleName.str();
};
}
std::string BitcodeCompiler::getThinLTOOutputFile(StringRef path) {
return lto::getThinLTOOutputFile(path, ctx.config.thinLTOPrefixReplaceOld,
ctx.config.thinLTOPrefixReplaceNew);
@ -124,7 +134,8 @@ BitcodeCompiler::BitcodeCompiler(COFFLinkerContext &c) : ctx(c) {
/*ShouldEmitImportFiles=*/false, ctx.config.outputFile,
ctx.config.dtltoDistributor, ctx.config.dtltoDistributorArgs,
ctx.config.dtltoCompiler, ctx.config.dtltoCompilerPrependArgs,
ctx.config.dtltoCompilerArgs, !ctx.config.saveTempsArgs.empty());
ctx.config.dtltoCompilerArgs, !ctx.config.saveTempsArgs.empty(),
createAddBufferFn(files, file_names));
} else if (ctx.config.thinLTOIndexOnly) {
auto OnIndexWrite = [&](StringRef S) { thinIndices.erase(S); };
backend = lto::createWriteIndexesThinBackend(
@ -200,11 +211,7 @@ std::vector<InputFile *> BitcodeCompiler::compile() {
FileCache cache;
if (!ctx.config.ltoCache.empty())
cache = check(localCache("ThinLTO", "Thin", ctx.config.ltoCache,
[&](size_t task, const Twine &moduleName,
std::unique_ptr<MemoryBuffer> mb) {
files[task] = std::move(mb);
file_names[task] = moduleName.str();
}));
createAddBufferFn(files, file_names)));
checkError(ltoObj->run(
[&](size_t task, const Twine &moduleName) {
@ -241,9 +248,10 @@ std::vector<InputFile *> BitcodeCompiler::compile() {
const char *Ext = emitASM ? ".s" : ".obj";
for (unsigned i = 0; i != maxTasks; ++i) {
StringRef bitcodeFilePath;
// Get the native object contents either from the cache or from memory. Do
// not use the cached MemoryBuffer directly, or the PDB will not be
// deterministic.
// Get the native object contents either from a MemoryBuffer, for example
// from the cache or an external DTLTO backend compilation, or by reading
// from memory. Do not use the provided MemoryBuffer directly, or the PDB
// will not be deterministic.
StringRef objBuf;
if (files[i]) {
objBuf = files[i]->getBuffer();

View File

@ -38,6 +38,16 @@ using namespace llvm::ELF;
using namespace lld;
using namespace lld::elf;
static AddBufferFn
createAddBufferFn(std::vector<std::unique_ptr<MemoryBuffer>> &files,
SmallVectorImpl<std::string> &filenames) {
return [&files, &filenames](unsigned task, const Twine &moduleName,
std::unique_ptr<MemoryBuffer> mb) {
files[task] = std::move(mb);
filenames[task] = moduleName.str();
};
}
static std::string getThinLTOOutputFile(Ctx &ctx, StringRef modulePath) {
return lto::getThinLTOOutputFile(modulePath, ctx.arg.thinLTOPrefixReplaceOld,
ctx.arg.thinLTOPrefixReplaceNew);
@ -188,7 +198,7 @@ BitcodeCompiler::BitcodeCompiler(Ctx &ctx) : ctx(ctx) {
ctx.arg.outputFile, ctx.arg.dtltoDistributor,
ctx.arg.dtltoDistributorArgs, ctx.arg.dtltoCompiler,
ctx.arg.dtltoCompilerPrependArgs, ctx.arg.dtltoCompilerArgs,
!ctx.arg.saveTempsArgs.empty());
!ctx.arg.saveTempsArgs.empty(), createAddBufferFn(files, filenames));
} else {
backend = lto::createInProcessThinBackend(
llvm::heavyweight_hardware_concurrency(ctx.arg.thinLTOJobs),
@ -329,11 +339,7 @@ SmallVector<std::unique_ptr<InputFile>, 0> BitcodeCompiler::compile() {
FileCache cache;
if (!ctx.arg.thinLTOCacheDir.empty())
cache = check(localCache("ThinLTO", "Thin", ctx.arg.thinLTOCacheDir,
[&](size_t task, const Twine &moduleName,
std::unique_ptr<MemoryBuffer> mb) {
files[task] = std::move(mb);
filenames[task] = moduleName.str();
}));
createAddBufferFn(files, filenames)));
if (!ctx.bitcodeFiles.empty())
checkError(ctx.e, ltoObj->run(
@ -386,8 +392,10 @@ SmallVector<std::unique_ptr<InputFile>, 0> BitcodeCompiler::compile() {
StringRef bitcodeFilePath;
StringRef objBuf;
if (files[i]) {
// When files[i] is not null, we get the native relocatable file from the
// cache. filenames[i] contains the original BitcodeFile's identifier.
// When files[i] is not null, it holds a native relocatable file provided
// as a MemoryBuffer, for example from the cache or from an external DTLTO
// backend compilation. filenames[i] contains the original BitcodeFile's
// identifier.
objBuf = files[i]->getBuffer();
bitcodeFilePath = filenames[i];
} else {

View File

@ -367,13 +367,15 @@ LLVM_ABI ThinBackend createInProcessThinBackend(
/// backend compilations.
/// SaveTemps is a debugging tool that prevents temporary files created by this
/// backend from being cleaned up.
/// AddBuffer is used to add a pre-existing native object buffer to the link.
LLVM_ABI ThinBackend createOutOfProcessThinBackend(
ThreadPoolStrategy Parallelism, IndexWriteCallback OnWrite,
bool ShouldEmitIndexFiles, bool ShouldEmitImportsFiles,
StringRef LinkerOutputFile, StringRef Distributor,
ArrayRef<StringRef> DistributorArgs, StringRef RemoteCompiler,
ArrayRef<StringRef> RemoteCompilerPrependArgs,
ArrayRef<StringRef> RemoteCompilerArgs, bool SaveTemps);
ArrayRef<StringRef> RemoteCompilerArgs, bool SaveTemps,
AddBufferFn AddBuffer);
/// This ThinBackend writes individual module indexes to files, instead of
/// running the individual backend jobs. This backend is for distributed builds

View File

@ -1540,7 +1540,6 @@ namespace {
/// generated files back into the link.
class CGThinBackend : public ThinBackendProc {
protected:
AddStreamFn AddStream;
DenseSet<GlobalValue::GUID> CfiFunctionDefs;
DenseSet<GlobalValue::GUID> CfiFunctionDecls;
bool ShouldEmitIndexFiles;
@ -1549,12 +1548,10 @@ public:
CGThinBackend(
const Config &Conf, ModuleSummaryIndex &CombinedIndex,
const DenseMap<StringRef, GVSummaryMapTy> &ModuleToDefinedGVSummaries,
AddStreamFn AddStream, lto::IndexWriteCallback OnWrite,
bool ShouldEmitIndexFiles, bool ShouldEmitImportsFiles,
ThreadPoolStrategy ThinLTOParallelism)
lto::IndexWriteCallback OnWrite, bool ShouldEmitIndexFiles,
bool ShouldEmitImportsFiles, ThreadPoolStrategy ThinLTOParallelism)
: ThinBackendProc(Conf, CombinedIndex, ModuleToDefinedGVSummaries,
OnWrite, ShouldEmitImportsFiles, ThinLTOParallelism),
AddStream(std::move(AddStream)),
ShouldEmitIndexFiles(ShouldEmitIndexFiles) {
auto &Defs = CombinedIndex.cfiFunctionDefs();
CfiFunctionDefs.insert_range(Defs.guids());
@ -1567,6 +1564,9 @@ public:
/// an in-process thread when invoked for each task.
class InProcessThinBackend : public CGThinBackend {
protected:
// Callback used to add generated native object files to the link by code
// generating directly into the returned output stream.
AddStreamFn AddStream;
FileCache Cache;
public:
@ -1576,10 +1576,10 @@ public:
const DenseMap<StringRef, GVSummaryMapTy> &ModuleToDefinedGVSummaries,
AddStreamFn AddStream, FileCache Cache, lto::IndexWriteCallback OnWrite,
bool ShouldEmitIndexFiles, bool ShouldEmitImportsFiles)
: CGThinBackend(Conf, CombinedIndex, ModuleToDefinedGVSummaries,
AddStream, OnWrite, ShouldEmitIndexFiles,
ShouldEmitImportsFiles, ThinLTOParallelism),
Cache(std::move(Cache)) {}
: CGThinBackend(Conf, CombinedIndex, ModuleToDefinedGVSummaries, OnWrite,
ShouldEmitIndexFiles, ShouldEmitImportsFiles,
ThinLTOParallelism),
AddStream(std::move(AddStream)), Cache(std::move(Cache)) {}
virtual Error runThinLTOBackendThread(
AddStreamFn AddStream, FileCache Cache, unsigned Task, BitcodeModule BM,
@ -2374,25 +2374,29 @@ class OutOfProcessThinBackend : public CGThinBackend {
// Cache
FileCache Cache;
// Callback to add a pre-existing native object buffer to the link.
AddBufferFn AddBuffer;
public:
OutOfProcessThinBackend(
const Config &Conf, ModuleSummaryIndex &CombinedIndex,
ThreadPoolStrategy ThinLTOParallelism,
const DenseMap<StringRef, GVSummaryMapTy> &ModuleToDefinedGVSummaries,
AddStreamFn AddStream, FileCache CacheFn, lto::IndexWriteCallback OnWrite,
FileCache CacheFn, lto::IndexWriteCallback OnWrite,
bool ShouldEmitIndexFiles, bool ShouldEmitImportsFiles,
StringRef LinkerOutputFile, StringRef Distributor,
ArrayRef<StringRef> DistributorArgs, StringRef RemoteCompiler,
ArrayRef<StringRef> RemoteCompilerPrependArgs,
ArrayRef<StringRef> RemoteCompilerArgs, bool SaveTemps)
: CGThinBackend(Conf, CombinedIndex, ModuleToDefinedGVSummaries,
AddStream, OnWrite, ShouldEmitIndexFiles,
ShouldEmitImportsFiles, ThinLTOParallelism),
ArrayRef<StringRef> RemoteCompilerArgs, bool SaveTemps,
AddBufferFn AddBuffer)
: CGThinBackend(Conf, CombinedIndex, ModuleToDefinedGVSummaries, OnWrite,
ShouldEmitIndexFiles, ShouldEmitImportsFiles,
ThinLTOParallelism),
LinkerOutputFile(LinkerOutputFile), DistributorPath(Distributor),
DistributorArgs(DistributorArgs), RemoteCompiler(RemoteCompiler),
RemoteCompilerPrependArgs(RemoteCompilerPrependArgs),
RemoteCompilerArgs(RemoteCompilerArgs), SaveTemps(SaveTemps),
Cache(std::move(CacheFn)) {}
Cache(std::move(CacheFn)), AddBuffer(std::move(AddBuffer)) {}
void setup(unsigned ThinLTONumTasks, unsigned ThinLTOTaskOffset,
llvm::Triple Triple) override {
@ -2723,11 +2727,12 @@ public:
Job.NativeObjectPath + ": " + EC.message(),
inconvertibleErrorCode());
MemoryBufferRef ObjFileMbRef = ObjFileMbOrErr->get()->getMemBufferRef();
if (Cache.isValid()) {
// Cache hits are taken care of earlier. At this point, we could only
// have cache misses.
assert(Job.CacheAddStream);
MemoryBufferRef ObjFileMbRef =
ObjFileMbOrErr->get()->getMemBufferRef();
// Obtain a file stream for a storing a cache entry.
auto CachedFileStreamOrErr =
Job.CacheAddStream(Job.Task, Job.ModuleID);
@ -2743,13 +2748,7 @@ public:
if (Error Err = CacheStream.commit())
return Err;
} else {
auto StreamOrErr = AddStream(Job.Task, Job.ModuleID);
if (Error Err = StreamOrErr.takeError())
report_fatal_error(std::move(Err));
auto &Stream = *StreamOrErr->get();
*Stream.OS << ObjFileMbRef.getBuffer();
if (Error Err = Stream.commit())
report_fatal_error(std::move(Err));
AddBuffer(Job.Task, Job.ModuleID, std::move(*ObjFileMbOrErr));
}
}
}
@ -2764,17 +2763,18 @@ ThinBackend lto::createOutOfProcessThinBackend(
StringRef LinkerOutputFile, StringRef Distributor,
ArrayRef<StringRef> DistributorArgs, StringRef RemoteCompiler,
ArrayRef<StringRef> RemoteCompilerPrependArgs,
ArrayRef<StringRef> RemoteCompilerArgs, bool SaveTemps) {
ArrayRef<StringRef> RemoteCompilerArgs, bool SaveTemps,
AddBufferFn AddBuffer) {
auto Func =
[=](const Config &Conf, ModuleSummaryIndex &CombinedIndex,
const DenseMap<StringRef, GVSummaryMapTy> &ModuleToDefinedGVSummaries,
AddStreamFn AddStream, FileCache Cache) {
AddStreamFn, FileCache Cache) {
return std::make_unique<OutOfProcessThinBackend>(
Conf, CombinedIndex, Parallelism, ModuleToDefinedGVSummaries,
AddStream, Cache, OnWrite, ShouldEmitIndexFiles,
ShouldEmitImportsFiles, LinkerOutputFile, Distributor,
DistributorArgs, RemoteCompiler, RemoteCompilerPrependArgs,
RemoteCompilerArgs, SaveTemps);
Conf, CombinedIndex, Parallelism, ModuleToDefinedGVSummaries, Cache,
OnWrite, ShouldEmitIndexFiles, ShouldEmitImportsFiles,
LinkerOutputFile, Distributor, DistributorArgs, RemoteCompiler,
RemoteCompilerPrependArgs, RemoteCompilerArgs, SaveTemps,
AddBuffer);
};
return ThinBackend(Func, Parallelism);
}

View File

@ -411,6 +411,24 @@ static int run(int argc, char **argv) {
auto DTLTOCompilerArgsSV = llvm::to_vector<0>(llvm::map_range(
DTLTOCompilerArgs, [](const std::string &S) { return StringRef(S); }));
auto AddStream =
[&](size_t Task,
const Twine &ModuleName) -> std::unique_ptr<CachedFileStream> {
std::string Path = OutputFilename + "." + utostr(Task);
std::error_code EC;
auto S = std::make_unique<raw_fd_ostream>(Path, EC, sys::fs::OF_None);
check(EC, Path);
return std::make_unique<CachedFileStream>(std::move(S), Path);
};
auto AddBuffer = [&](size_t Task, const Twine &ModuleName,
std::unique_ptr<MemoryBuffer> MB) {
auto Stream = AddStream(Task, ModuleName);
*Stream->OS << MB->getBuffer();
check(Stream->commit(), "Failed to commit cache");
};
ThinBackend Backend;
if (ThinLTODistributedIndexes)
Backend = createWriteIndexesThinBackend(llvm::hardware_concurrency(Threads),
@ -425,7 +443,7 @@ static int run(int argc, char **argv) {
llvm::heavyweight_hardware_concurrency(Threads),
/*OnWrite=*/{}, ThinLTOEmitIndexes, ThinLTOEmitImports, OutputFilename,
DTLTODistributor, DTLTODistributorArgsSV, DTLTOCompiler,
DTLTOCompilerPrependArgsSV, DTLTOCompilerArgsSV, SaveTemps);
DTLTOCompilerPrependArgsSV, DTLTOCompilerArgsSV, SaveTemps, AddBuffer);
} else
Backend = createInProcessThinBackend(
llvm::heavyweight_hardware_concurrency(Threads),
@ -496,24 +514,6 @@ static int run(int argc, char **argv) {
if (HasErrors)
return 1;
auto AddStream =
[&](size_t Task,
const Twine &ModuleName) -> std::unique_ptr<CachedFileStream> {
std::string Path = OutputFilename + "." + utostr(Task);
std::error_code EC;
auto S = std::make_unique<raw_fd_ostream>(Path, EC, sys::fs::OF_None);
check(EC, Path);
return std::make_unique<CachedFileStream>(std::move(S), Path);
};
auto AddBuffer = [&](size_t Task, const Twine &ModuleName,
std::unique_ptr<MemoryBuffer> MB) {
auto Stream = AddStream(Task, ModuleName);
*Stream->OS << MB->getBuffer();
check(Stream->commit(), "Failed to commit cache");
};
FileCache Cache;
if (!CacheDir.empty())
Cache = check(localCache("ThinLTO", "Thin", CacheDir, AddBuffer),