[lld] [ELF] Parallelize input file loading (PR #191690)
Fangrui Song via llvm-commits
llvm-commits at lists.llvm.org
Fri Apr 17 23:46:48 PDT 2026
https://github.com/MaskRay updated https://github.com/llvm/llvm-project/pull/191690
>From 4ac162312b5eb935fba710d1e564d152f7b36def Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Sat, 11 Apr 2026 16:32:23 -0700
Subject: [PATCH 1/2] [ELF] Parallelize input file loading
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
During `createFiles`, addFile() records a LoadJob for each non-script
input (archive, relocatable, DSO, bitcode) with a state-machine snapshot
(inWholeArchive, inLib, asNeeded, withLOption, groupId) and expands them
on worker threads in loadFiles(). Linker scripts are still processed
inline since their `INPUT()` and `GROUP()` commands recursively call
addFile().
Outside createFiles(), e.g. addDependentLibrary() during the serial
doParseFiles() loop, deferLoad is false and the single job is drained
immediately via loadFiles().
Thread-safety:
- A mutex serializes BitcodeFile / fatLTO constructors that call
ctx.saver / ctx.uniqueSaver. Zero contention on pure ELF links.
- Thin-archive member buffers accumulate in per-job SmallVectors and
are merged into ctx.memoryBuffers in command-line order.
- groupId is pre-claimed during the serial walk and written to each
produced file after construction (the InputFile constructor no longer
reads nextGroupId).
Performance (--threads=8):
```
clang-relassert (267 thin archives, 10 .o, 2 .so):
965 +/- 32 ms -> 924 +/- 24 ms (1.05x, 80 runs)
(Apple M4) 249.7ms ± 2.5ms -> 221.2ms ± 1.4ms (1.13x, 10 runs)
chromium (532 .a, 3314 .o, 343 .so):
8.071 +/- 0.472 s -> 7.370 +/- 0.198 s (1.10x, 20 runs)
```
Parallelizing all file kinds (not just archives) matters for
.o-dominated workloads like chromium where archive-only parallelization
shows no improvement.
Output is byte-identical to the old lld and deterministic across
--threads values.
---
lld/ELF/Config.h | 22 +++-
lld/ELF/Driver.cpp | 238 +++++++++++++++++++++++------------------
lld/ELF/InputFiles.cpp | 2 +-
lld/ELF/InputFiles.h | 2 +-
4 files changed, 158 insertions(+), 106 deletions(-)
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index cf3cde1cf2696..1cc8d70d36f40 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -175,6 +175,21 @@ struct VersionDefinition {
SmallVector<SymbolVersion, 0> localPatterns;
};
+// Deferred file-load job: one per input, expanded by loadFiles().
+struct LoadJob {
+ enum Kind : uint8_t { Obj, Bitcode, Archive, Shared };
+ llvm::MemoryBufferRef mbref;
+ llvm::StringRef path;
+ Kind kind;
+ bool inWholeArchive;
+ bool lazy;
+ bool asNeeded;
+ bool withLOption;
+ uint32_t groupId;
+ SmallVector<std::unique_ptr<InputFile>, 0> out;
+ std::vector<std::unique_ptr<llvm::MemoryBuffer>> thinBufs;
+};
+
class LinkerDriver {
public:
LinkerDriver(Ctx &ctx);
@@ -186,17 +201,20 @@ class LinkerDriver {
private:
Ctx &ctx;
void createFiles(llvm::opt::InputArgList &args);
+ void loadFiles();
void inferMachineType();
template <class ELFT> void link(llvm::opt::InputArgList &args);
template <class ELFT> void compileBitcodeFiles(bool skipLinkedOutput);
- bool tryAddFatLTOFile(MemoryBufferRef mb, StringRef archiveName,
- uint64_t offsetInArchive, bool lazy);
// True if we are in --whole-archive and --no-whole-archive.
bool inWholeArchive = false;
// True if we are in --start-lib and --end-lib.
bool inLib = false;
+ // True inside createFiles(): defers to loadFiles().
+ bool deferLoad = false;
+ SmallVector<LoadJob, 0> loadJobs;
+
std::unique_ptr<BitcodeCompiler> lto;
SmallVector<std::unique_ptr<InputFile>, 0> files, ltoObjectFiles;
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index 24718d2d2186a..6f8a0a50dfa05 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -195,7 +195,8 @@ static std::tuple<ELFKind, uint16_t, uint8_t> parseEmulation(Ctx &ctx,
// Returns slices of MB by parsing MB as an archive file.
// Each slice consists of a member file in the archive.
std::vector<std::pair<MemoryBufferRef, uint64_t>> static getArchiveMembers(
- Ctx &ctx, MemoryBufferRef mb) {
+ Ctx &ctx, LoadJob &job) {
+ MemoryBufferRef mb = job.mbref;
std::unique_ptr<Archive> file =
CHECK(Archive::create(mb),
mb.getBufferIdentifier() + ": failed to parse archive");
@@ -218,32 +219,16 @@ std::vector<std::pair<MemoryBufferRef, uint64_t>> static getArchiveMembers(
<< ": Archive::children failed: " << std::move(err);
// Take ownership of memory buffers created for members of thin archives.
- std::vector<std::unique_ptr<MemoryBuffer>> mbs = file->takeThinBuffers();
- std::move(mbs.begin(), mbs.end(), std::back_inserter(ctx.memoryBuffers));
+ job.thinBufs = file->takeThinBuffers();
return v;
}
-static bool isBitcode(MemoryBufferRef mb) {
- return identify_magic(mb.getBuffer()) == llvm::file_magic::bitcode;
-}
-
-bool LinkerDriver::tryAddFatLTOFile(MemoryBufferRef mb, StringRef archiveName,
- uint64_t offsetInArchive, bool lazy) {
- if (!ctx.arg.fatLTOObjects)
- return false;
- Expected<MemoryBufferRef> fatLTOData =
- IRObjectFile::findBitcodeInMemBuffer(mb);
- if (errorToBool(fatLTOData.takeError()))
- return false;
- auto file = std::make_unique<BitcodeFile>(ctx, *fatLTOData, archiveName,
- offsetInArchive, lazy);
- file->obj->fatLTOObject(true);
- files.push_back(std::move(file));
- return true;
-}
-
// Opens a file and create a file object. Path has to be resolved already.
+// Every regular input (not binary-format or linker scripts) is recorded as a
+// LoadJob. Inside createFiles() jobs batch up and are expanded in parallel at
+// the end. Outside createFiles() (e.g. addDependentLibrary during parseFiles)
+// the single job is expanded immediately.
void LinkerDriver::addFile(StringRef path, bool withLOption) {
using namespace sys::fs;
@@ -253,93 +238,53 @@ void LinkerDriver::addFile(StringRef path, bool withLOption) {
MemoryBufferRef mbref = *buffer;
if (ctx.arg.formatBinary) {
- files.push_back(std::make_unique<BinaryFile>(ctx, mbref));
- if (!isInGroup)
- ++nextGroupId;
- return;
- }
-
- switch (identify_magic(mbref.getBuffer())) {
- case file_magic::unknown:
- readLinkerScript(ctx, mbref);
- return;
- case file_magic::archive: {
- auto members = getArchiveMembers(ctx, mbref);
- if (inWholeArchive) {
- for (const std::pair<MemoryBufferRef, uint64_t> &p : members) {
- if (isBitcode(p.first))
- files.push_back(std::make_unique<BitcodeFile>(ctx, p.first, path,
- p.second, false));
- else if (!tryAddFatLTOFile(p.first, path, p.second, false))
- files.push_back(createObjFile(ctx, p.first, path));
- }
+ LoadJob job = {mbref, path, LoadJob::Obj, false, false,
+ false, false, nextGroupId, {}, {}};
+ job.out.push_back(std::make_unique<BinaryFile>(ctx, mbref));
+ loadJobs.push_back(std::move(job));
+ } else {
+ auto magic = identify_magic(mbref.getBuffer());
+ if (magic == file_magic::unknown) {
+ readLinkerScript(ctx, mbref);
return;
}
-
- archiveFiles.emplace_back(path, members.size());
-
- // Handle archives and --start-lib/--end-lib using the same code path. This
- // scans all the ELF relocatable object files and bitcode files in the
- // archive rather than just the index file, with the benefit that the
- // symbols are only loaded once. For many projects archives see high
- // utilization rates and it is a net performance win. --start-lib scans
- // symbols in the same order that llvm-ar adds them to the index, so in the
- // common case the semantics are identical. If the archive symbol table was
- // created in a different order, or is incomplete, this strategy has
- // different semantics. Such output differences are considered user error.
- //
- // All files within the archive get the same group ID to allow mutual
- // references for --warn-backrefs.
- SaveAndRestore saved(isInGroup, true);
- for (const std::pair<MemoryBufferRef, uint64_t> &p : members) {
- auto magic = identify_magic(p.first.getBuffer());
- if (magic == file_magic::elf_relocatable) {
- if (!tryAddFatLTOFile(p.first, path, p.second, true))
- files.push_back(createObjFile(ctx, p.first, path, true));
- } else if (magic == file_magic::bitcode)
- files.push_back(
- std::make_unique<BitcodeFile>(ctx, p.first, path, p.second, true));
- else
- Warn(ctx) << path << ": archive member '"
- << p.first.getBufferIdentifier()
- << "' is neither ET_REL nor LLVM bitcode";
- }
- if (!saved.get())
- ++nextGroupId;
- return;
- }
- case file_magic::elf_shared_object: {
- if (ctx.arg.isStatic) {
- ErrAlways(ctx) << "attempted static link of dynamic object " << path;
+ LoadJob::Kind kind;
+ switch (magic) {
+ case file_magic::archive:
+ kind = LoadJob::Archive;
+ break;
+ case file_magic::elf_relocatable:
+ kind = LoadJob::Obj;
+ break;
+ case file_magic::bitcode:
+ kind = LoadJob::Bitcode;
+ break;
+ case file_magic::elf_shared_object:
+ if (ctx.arg.isStatic) {
+ Err(ctx) << "attempted static link of dynamic object " << path;
+ return;
+ }
+ kind = LoadJob::Shared;
+ break;
+ default:
+ Err(ctx) << path << ": unknown file type";
return;
}
-
- // Shared objects are identified by soname. soname is (if specified)
- // DT_SONAME and falls back to filename. If a file was specified by -lfoo,
- // the directory part is ignored. Note that path may be a temporary and
- // cannot be stored into SharedFile::soName.
- path = mbref.getBufferIdentifier();
- auto f = std::make_unique<SharedFile>(
- ctx, mbref, withLOption ? path::filename(path) : path);
- f->init();
- files.push_back(std::move(f));
- break;
+ loadJobs.push_back({mbref,
+ path,
+ kind,
+ inWholeArchive,
+ inLib,
+ ctx.arg.asNeeded,
+ withLOption,
+ nextGroupId,
+ {},
+ {}});
}
- case file_magic::bitcode:
- files.push_back(std::make_unique<BitcodeFile>(ctx, mbref, "", 0, inLib));
- break;
- case file_magic::elf_relocatable:
- if (!tryAddFatLTOFile(mbref, "", 0, inLib))
- files.push_back(createObjFile(ctx, mbref, "", inLib));
- break;
- default:
- ErrAlways(ctx) << path << ": unknown file type";
- return;
- }
- // All files within the same --{start,end}-group get the same group ID.
- // Otherwise, a new file will get a new group ID.
if (!isInGroup)
++nextGroupId;
+ if (!deferLoad)
+ loadFiles();
}
// Add a given library by searching it from input search paths.
@@ -2160,8 +2105,96 @@ static bool isFormatBinary(Ctx &ctx, StringRef s) {
return false;
}
+// Expand LoadJob entries recorded by addFile(). Called in batch from
+// createFiles() (parallel), or immediately from addFile() for late additions
+// like dependent libraries (single job, runs inline).
+void LinkerDriver::loadFiles() {
+ // BitcodeFile / fatLTO constructors call ctx.saver which is not thread-safe.
+ // SharedFile and ObjFile constructors are safe without the mutex.
+ std::mutex mu;
+ auto makeFile = [&](MemoryBufferRef mb, file_magic magic, StringRef arPath,
+ uint64_t offset,
+ bool lazy) -> std::unique_ptr<InputFile> {
+ if (magic == file_magic::bitcode) {
+ std::lock_guard<std::mutex> lk(mu);
+ return std::make_unique<BitcodeFile>(ctx, mb, arPath, offset, lazy);
+ }
+ if (ctx.arg.fatLTOObjects) {
+ Expected<MemoryBufferRef> fatLTOData =
+ IRObjectFile::findBitcodeInMemBuffer(mb);
+ if (!errorToBool(fatLTOData.takeError())) {
+ std::lock_guard<std::mutex> lk(mu);
+ auto f = std::make_unique<BitcodeFile>(ctx, *fatLTOData, arPath, offset,
+ lazy);
+ f->obj->fatLTOObject(true);
+ return f;
+ }
+ }
+ return createObjFile(ctx, mb, arPath, lazy);
+ };
+
+ {
+ llvm::TimeTraceScope timeScope("Parallel load");
+ parallelFor(0, loadJobs.size(), [&](size_t i) {
+ LoadJob &job = loadJobs[i];
+ switch (job.kind) {
+ case LoadJob::Archive: {
+ // Scan all archive members rather than using the archive symbol index.
+ // Most archives see high utilization rates and this is a net win. All
+ // members within one archive share the same group ID.
+ auto members = getArchiveMembers(ctx, job);
+ job.out.reserve(members.size());
+ bool lazy = !job.inWholeArchive;
+ for (const auto &[mb, offset] : members) {
+ auto mm = identify_magic(mb.getBuffer());
+ if (mm == file_magic::elf_relocatable || mm == file_magic::bitcode ||
+ job.inWholeArchive)
+ job.out.push_back(makeFile(mb, mm, job.path, offset, lazy));
+ else
+ Warn(ctx) << job.path << ": archive member '"
+ << mb.getBufferIdentifier()
+ << "' is neither ET_REL nor LLVM bitcode";
+ }
+ break;
+ }
+ case LoadJob::Shared: {
+ StringRef bufPath = job.mbref.getBufferIdentifier();
+ auto f = std::make_unique<SharedFile>(
+ ctx, job.mbref,
+ job.withLOption ? path::filename(bufPath) : bufPath);
+ f->init();
+ f->isNeeded = !job.asNeeded;
+ job.out.push_back(std::move(f));
+ break;
+ }
+ case LoadJob::Obj:
+ case LoadJob::Bitcode:
+ job.out.push_back(makeFile(job.mbref,
+ job.kind == LoadJob::Bitcode
+ ? file_magic::bitcode
+ : file_magic::elf_relocatable,
+ "", 0, job.lazy));
+ break;
+ }
+ for (auto &m : job.out)
+ m->groupId = job.groupId;
+ });
+ }
+
+ for (auto &job : loadJobs) {
+ if (job.kind == LoadJob::Archive)
+ archiveFiles.emplace_back(job.path, (unsigned)job.out.size());
+ files.append(std::make_move_iterator(job.out.begin()),
+ std::make_move_iterator(job.out.end()));
+ ctx.memoryBuffers.append(std::make_move_iterator(job.thinBufs.begin()),
+ std::make_move_iterator(job.thinBufs.end()));
+ }
+ loadJobs.clear();
+}
+
void LinkerDriver::createFiles(opt::InputArgList &args) {
llvm::TimeTraceScope timeScope("Load input files");
+ SaveAndRestore saveDefer(deferLoad, true);
// For --{push,pop}-state.
std::vector<std::tuple<bool, bool, bool>> stack;
@@ -2283,6 +2316,7 @@ void LinkerDriver::createFiles(opt::InputArgList &args) {
if (defaultScript && !hasScript)
readLinkerScript(ctx, *defaultScript);
+ loadFiles();
if (files.empty() && !hasInput && errCount(ctx) == 0)
ErrAlways(ctx) << "no input files";
}
diff --git a/lld/ELF/InputFiles.cpp b/lld/ELF/InputFiles.cpp
index c2737ea57d0f6..bf5a65154184f 100644
--- a/lld/ELF/InputFiles.cpp
+++ b/lld/ELF/InputFiles.cpp
@@ -207,7 +207,7 @@ static void updateSupportedARMFeatures(Ctx &ctx,
}
InputFile::InputFile(Ctx &ctx, Kind k, MemoryBufferRef m)
- : ctx(ctx), mb(m), groupId(ctx.driver.nextGroupId), fileKind(k) {}
+ : ctx(ctx), mb(m), fileKind(k) {}
InputFile::~InputFile() {}
diff --git a/lld/ELF/InputFiles.h b/lld/ELF/InputFiles.h
index 24918a067a836..aef599102ecfc 100644
--- a/lld/ELF/InputFiles.h
+++ b/lld/ELF/InputFiles.h
@@ -130,7 +130,7 @@ class InputFile {
// checking feature. All files within the same --{start,end}-group or
// --{start,end}-lib get the same group ID. Otherwise, each file gets a new
// group ID. For more info, see checkDependency() in SymbolTable.cpp.
- uint32_t groupId;
+ uint32_t groupId = 0;
// If this is an architecture-specific file, the following members
// have ELF type (i.e. ELF{32,64}{LE,BE}) and target machine type.
>From 5e6b890bab3918c714e2646e5d5931cdc42ff2da Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Fri, 17 Apr 2026 23:46:37 -0700
Subject: [PATCH 2/2] comments
---
lld/ELF/Driver.cpp | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index fb21d324769dd..10141c201083b 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -241,10 +241,10 @@ void LinkerDriver::addFile(StringRef path, bool withLOption) {
loadJobs.push_back({mbref,
path,
LoadJob::Binary,
- false,
- false,
- false,
- false,
+ /*inWholeArchive=*/false,
+ /*lazy=*/false,
+ /*asNeeded=*/false,
+ /*withLOption=*/false,
nextGroupId,
{},
{}});
@@ -2154,8 +2154,10 @@ void LinkerDriver::loadFiles() {
break;
case LoadJob::Archive: {
// Scan all archive members rather than using the archive symbol
- // index. Most archives see high utilization rates and this is a net
- // win. All members within one archive share the same group ID.
+ // index. We assume the archive symbol table order matches the order
+ // of symbols in the member symbol tables. All files within the
+ // archive share the same group ID to allow mutual references for
+ // --warn-backrefs.
auto members = getArchiveMembers(ctx, job);
job.out.reserve(members.size());
bool lazy = !job.inWholeArchive;
@@ -2172,6 +2174,9 @@ void LinkerDriver::loadFiles() {
break;
}
case LoadJob::Shared: {
+ // Shared objects are identified by soname. soname is (if specified)
+ // DT_SONAME and falls back to filename. If a file was specified by
+ // -lfoo, the directory part is ignored.
StringRef bufPath = job.mbref.getBufferIdentifier();
auto f = std::make_unique<SharedFile>(
ctx, job.mbref,
More information about the llvm-commits
mailing list