Files
Ben Dunbobbin 9ecb67b7bc [DTLTO] Overlap temporary file removal (#209423)
Deleting the temporary files produced by the DTLTO pipeline can be
expensive on Windows hosts. For a Clang link (Debug build with
sanitizers and instrumentation) using an optimized toolchain (PGO
non-LTO, llvmorg-22.1.0) on a Windows 11 Pro (Build 26200), AMD Family
25 @ ~4.5 GHz, 16 cores/32 threads, 64 GB RAM machine, the mean duration
of the "Remove DTLTO temporary files" time trace scope was 1267.789 ms
(measured over 10 runs).

This patch performs the deletions on a background thread, allowing them
to overlap with the tail of the link to hide this cost.

This is a re-implementation of the asynchronous cleanup idea from
https://github.com/llvm/llvm-project/pull/186988, which had to be
reverted in https://github.com/llvm/llvm-project/pull/189043 because
cleanup was not guaranteed to complete before LLD invoked
timeTraceProfilerCleanup(). In certain cases timeTraceProfilerCleanup()
was called before temporary file deletion had completed in LLD, which
caused memory leaks that were flagged by sanitizer builds.

Note that the DTLTO implementation has been refactored heavily since
https://github.com/llvm/llvm-project/pull/186988, so this is a
re-implementation along the same lines.

To solve the ordering issue, LLD now calls a hook that defaults to a
no-op, and DTLTO overrides it to drain the background deletion work.
LLD calls this hook before time-trace write/cleanup.

(cherry picked from commit 914d5a1841dedf2148e21bd6de3b92409fe79285)
2026-07-31 11:17:54 +02:00

350 lines
12 KiB
C++

//===- DTLTO.cpp - Integrated Distributed ThinLTO implementation ----------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
// \file
// This file implements support functions for Integrated Distributed ThinLTO,
// focusing on preparing complilation jobs for distribution.
//
//===----------------------------------------------------------------------===//
#include "llvm/DTLTO/DTLTO.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/LTO/LTO.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBufferRef.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/Process.h"
#include "llvm/Support/TimeProfiler.h"
#include "llvm/Support/raw_ostream.h"
#include <string>
#include <system_error>
#include <utility>
#include <vector>
using namespace llvm;
// Experimentation showed that serial deletion is most efficient, hence
// a single thread.
lto::DTLTO::BackgroundDeletion::BackgroundDeletion()
: DefaultThreadPool(hardware_concurrency(1)) {}
lto::DTLTO::BackgroundDeletion::~BackgroundDeletion() { waitForTasks(); }
void lto::DTLTO::BackgroundDeletion::waitForTasks() {
wait();
for (const std::string &Warning : Warnings)
errs() << "warning: could not remove the file " << Warning << "\n";
Warnings.clear();
}
void lto::DTLTO::BackgroundDeletion::removeFiles(
std::vector<std::string> &&Files, const Config &Conf) {
if (Files.empty())
return;
async([this, Files = std::move(Files), TTE = Conf.TimeTraceEnabled,
TTG = Conf.TimeTraceGranularity] {
if (LLVM_ENABLE_THREADS && TTE)
timeTraceProfilerInitialize(TTG, "Remove DTLTO temporary files");
{
TimeTraceScope TimeScope("Remove DTLTO temporary files");
for (const std::string &Path : Files) {
std::error_code EC = sys::fs::remove(Path, true);
if (!EC ||
EC == std::make_error_code(std::errc::no_such_file_or_directory))
continue;
Warnings.emplace_back("'" + Path + "': " + EC.message());
}
}
if (LLVM_ENABLE_THREADS && TTE)
timeTraceProfilerFinishThread();
});
}
void lto::DTLTO::waitForCleanup() { BackgroundDeleter.waitForTasks(); }
// Remove temporary files created to enable distribution.
void lto::DTLTO::cleanup() {
if (SaveTemps)
return;
BackgroundDeleter.removeFiles(std::move(CleanupList), Conf);
}
// Runs the DTLTO thin link phase, producing per-module summary indices,
// import lists, and cache keys for distribution.
Error lto::DTLTO::performThinLink() {
size_t NumTasks = getMaxTasks();
SummaryIndexFiles.resize(NumTasks);
ImportsFilesList.resize(NumTasks);
CacheKeysList.resize(NumTasks);
lto::Config &Cfg = getConfig();
Cfg.GetSummaryIndexOutputStream =
[&](size_t task) -> std::unique_ptr<raw_svector_ostream> {
return std::make_unique<raw_svector_ostream>(SummaryIndexFiles[task]);
};
Cfg.GetCacheKeyOutputString = [&](size_t task) -> std::string & {
return CacheKeysList[task];
};
Cfg.GetImportsListOutputArray =
[&](size_t task) -> std::vector<std::string> & {
return ImportsFilesList[task];
};
return Base::run(AddStreamFunc, {});
}
// Runs the DTLTO pipeline.
LLVM_ABI Error lto::DTLTO::run(AddStreamFn AddStream, FileCache CacheParam) {
scope_exit CleanUp([this]() { cleanup(); });
AddStreamFunc = AddStream;
Cache = std::move(CacheParam);
Conf.Dtlto = 1;
UID = itostr(sys::Process::getProcessId());
if (Error Err = performThinLink())
return Err;
ThinLTOTaskOffset = RegularLTO.ParallelCodeGenParallelismLevel;
DistributorParams.TargetTriple = RegularLTO.CombinedModule->getTargetTriple();
if (Error Err = prepareDtltoJobs())
return Err;
if (Error Err = extractLTOInputs())
return Err;
if (Error Err = performCodegen())
return Err;
if (Error Err = addObjectFilesToLink())
return Err;
return Error::success();
}
// Probes the LTO cache for a compiled native object for the given job.
Error lto::DTLTO::checkCacheHit(Job &J) {
if (!Cache.isValid())
return Error::success();
TimeTraceScope TimeScope("Check cache for DTLTO", J.SummaryIndexPath);
auto CacheAddStreamExp = Cache(J.Task, J.CacheKey, J.ModuleID);
if (Error Err = CacheAddStreamExp.takeError())
return Err;
AddStreamFn &CacheAddStream = *CacheAddStreamExp;
// If CacheAddStream is null, we have a cache hit and at this point
// object file is already passed back to the linker.
if (!CacheAddStream) {
J.Cached = true; // Cache hit, mark the job as cached.
CachedJobs.fetch_add(1);
} else {
// If CacheAddStream is not null, we have a cache miss and we need to
// run the backend for codegen. Save cache 'add stream'
// function for a later use.
J.CacheAddStream = std::move(CacheAddStream);
}
return Error::success();
}
// Prepares a single DTLTO backend compilation job for a ThinLTO module.
Error lto::DTLTO::prepareDtltoJob(StringRef ModulePath, unsigned Task) {
assert(Task >= ThinLTOTaskOffset && Task - ThinLTOTaskOffset < Jobs.size() &&
"Task index out of range for Jobs");
assert(Task < SummaryIndexFiles.size() && "Task index out of range");
SString ObjFilePath =
sys::path::parent_path(DistributorParams.LinkerOutputFile);
sys::path::append(ObjFilePath, sys::path::stem(ModulePath) + "." +
itostr(Task) + "." + UID + ".native.o");
SString SummaryIndexPathStr = ObjFilePath;
SummaryIndexPathStr += ".thinlto.bc";
SString ImportsPathStr = ModulePath;
ImportsPathStr += ".imports";
Job &J = Jobs[Task - ThinLTOTaskOffset];
J = {Task,
ModulePath,
Saver.save(ObjFilePath.str()),
Saver.save(SummaryIndexPathStr.str()),
Saver.save(ImportsPathStr.str()),
ImportsFilesList[Task],
CacheKeysList[Task],
nullptr,
false};
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))
return Err;
}
if (OnIndexWriteCb)
OnIndexWriteCb(J.SummaryIndexPath.str());
if (ShouldEmitImportFiles)
if (Error Err = save(join(J.ImportsFilesList, "\n"), J.ImportsPath))
return Err;
if (!SaveTemps) {
if (!J.Cached)
addToCleanup(J.NativeObjectPath.str());
if (!ShouldEmitIndexFiles)
addToCleanup(J.SummaryIndexPath.str());
if (!ShouldEmitImportFiles)
addToCleanup(J.ImportsPath.str());
}
return Error::success();
}
// Derive a set of Clang options that will be shared/common for all DTLTO
// backend compilations.
void lto::DTLTO::buildCommonRemoteCompilerOptions() {
const lto::Config &C = getConfig();
auto &Ops = DistributorParams.CodegenOptions;
Ops.push_back(Saver.save("-O" + Twine(C.OptLevel)));
if (C.Options.EmitAddrsig)
Ops.push_back("-faddrsig");
if (C.Options.FunctionSections)
Ops.push_back("-ffunction-sections");
if (C.Options.DataSections)
Ops.push_back("-fdata-sections");
if (C.PTO.LoopInterchange)
Ops.push_back("-floop-interchange");
if (C.RelocModel == Reloc::PIC_)
// Clang doesn't have -fpic for all triples.
if (!DistributorParams.TargetTriple.isOSBinFormatCOFF())
Ops.push_back("-fpic");
// Turn on/off warnings about profile cfg mismatch (default on)
// --lto-pgo-warn-mismatch.
if (!C.PGOWarnMismatch) {
Ops.push_back("-mllvm");
Ops.push_back("-no-pgo-warn-mismatch");
}
// Enable sample-based profile guided optimizations.
// Sample profile file path --lto-sample-profile=<value>.
if (!C.SampleProfile.empty()) {
Ops.push_back(Saver.save("-fprofile-sample-use=" + Twine(C.SampleProfile)));
DistributorParams.CommonInputs.insert(C.SampleProfile);
}
// We don't know which of options will be used by Clang.
Ops.push_back("-Wno-unused-command-line-argument");
// Forward any supplied options.
if (!DistributorParams.RemoteCompilerArgs.empty())
for (auto &a : DistributorParams.RemoteCompilerArgs)
Ops.push_back(a);
}
// Initializes DTLTO state and prepares a job for each ThinLTO module.
Error lto::DTLTO::prepareDtltoJobs() {
auto &ModuleMap =
ThinLTO.ModulesToCompile ? *ThinLTO.ModulesToCompile : ThinLTO.ModuleMap;
InputModuleIDsToExtract.clear();
if (ModuleMap.empty())
return Error::success();
Jobs.resize(ModuleMap.size());
for (auto [I, Mod] : enumerate(ModuleMap))
if (Error E = prepareDtltoJob(Mod.first, ThinLTOTaskOffset + I))
return E;
return Error::success();
}
// Runs the DTLTO code generation phase. Must be invoked after thinLink().
Error lto::DTLTO::performCodegen() {
if (Jobs.empty())
return Error::success();
// Build common remote compiler options.
buildCommonRemoteCompilerOptions();
DistributionDriver Distributor(DistributorParams, Jobs, SaveTemps,
[&](StringRef S) { addToCleanup(S); });
if (CachedJobs.load() < Jobs.size()) {
if (Error E = Distributor())
return E;
}
return Error::success();
}
// Adds compiled object files to the link for each non-cached job.
Error lto::DTLTO::addObjectFilesToLink() {
TimeTraceScope FilesScope("Add DTLTO files to the link");
for (auto &Job : Jobs) {
if (!Job.CacheKey.empty() && Job.Cached) {
assert(Cache.isValid());
continue;
}
// Load the native object from a file into a memory buffer
// and store its contents in the output buffer.
auto ObjFileMbOrErr =
MemoryBuffer::getFile(Job.NativeObjectPath, /*IsText=*/false,
/*RequiresNullTerminator=*/false);
if (std::error_code EC = ObjFileMbOrErr.getError())
return make_error<StringError>(
BCError + "cannot open native object file: " + 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);
// Obtain a file stream for a storing a cache entry.
auto CachedFileStreamOrErr = Job.CacheAddStream(Job.Task, Job.ModuleID);
if (!CachedFileStreamOrErr)
return joinErrors(
CachedFileStreamOrErr.takeError(),
createStringError(inconvertibleErrorCode(),
"Cannot get a cache file stream: %s",
Job.NativeObjectPath.data()));
// Store a file buffer into the cache stream.
auto &CacheStream = *(CachedFileStreamOrErr->get());
*(CacheStream.OS) << ObjFileMbRef.getBuffer();
if (Error Err = CacheStream.commit())
return Err;
} else {
if (AddBuffer) {
AddBuffer(Job.Task, Job.ModuleID, std::move(ObjFileMbOrErr.get()));
} else {
auto StreamOrErr = AddStreamFunc(Job.Task, Job.ModuleID);
if (Error Err = StreamOrErr.takeError())
return Err;
auto &Stream = *StreamOrErr->get();
*Stream.OS << ObjFileMbRef.getBuffer();
if (Error Err = Stream.commit())
return Err;
}
}
}
return Error::success();
}