[llvm] [coverage] llvm-cov export parallel load (PR #193726)

via llvm-commits llvm-commits at lists.llvm.org
Wed Apr 29 02:12:52 PDT 2026


https://github.com/patskovn updated https://github.com/llvm/llvm-project/pull/193726

>From f870ad986d4fe18181ae97fd3bdefb38d42c81a2 Mon Sep 17 00:00:00 2001
From: patskovn <patskovn at devvm27524.cln0.facebook.com>
Date: Tue, 21 Apr 2026 09:42:46 -0700
Subject: [PATCH 1/4] [llvm-cov] Add early dedup check in loadFunctionRecord

Move the (filenames hash, function name hash) deduplication check
before the expensive counter evaluation, bitmap loading, and region
emission. This allows skipping all that work for functions we have
already seen in a previous object file.

The pre-computed hashes are reused at the existing dedup point later
in the function, avoiding redundant hash_combine_range/hash_value
calls on the success path.
---
 llvm/lib/ProfileData/Coverage/CoverageMapping.cpp | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
index 0d9a5a6758f06..f21dd21e12d71 100644
--- a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
+++ b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
@@ -878,6 +878,15 @@ Error CoverageMapping::loadFunctionRecord(
   else
     OrigFuncName = getFuncNameWithoutPrefix(OrigFuncName, Record.Filenames[0]);
 
+  // Early dedup: skip expensive counter evaluation for already-seen functions.
+  auto FilenamesHash = hash_combine_range(Record.Filenames);
+  auto FuncNameHash = hash_value(OrigFuncName);
+  {
+    auto It = RecordProvenance.find(FilenamesHash);
+    if (It != RecordProvenance.end() && It->second.count(FuncNameHash))
+      return Error::success();
+  }
+
   CounterMappingContext Ctx(Record.Expressions);
 
   std::vector<uint64_t> Counts;
@@ -944,8 +953,7 @@ Error CoverageMapping::loadFunctionRecord(
   }
 
   // Don't create records for (filenames, function) pairs we've already seen.
-  auto FilenamesHash = hash_combine_range(Record.Filenames);
-  if (!RecordProvenance[FilenamesHash].insert(hash_value(OrigFuncName)).second)
+  if (!RecordProvenance[FilenamesHash].insert(FuncNameHash).second)
     return Error::success();
 
   Functions.push_back(std::move(Function));

>From ceb4b003330e064b2e9fb87a1b8fbfa288462dab Mon Sep 17 00:00:00 2001
From: patskovn <patskovn at devvm27524.cln0.facebook.com>
Date: Thu, 23 Apr 2026 03:33:25 -0700
Subject: [PATCH 2/4] [llvm-cov] Add CoverageMapping::mergeFrom for combining
 shards

Add a private method to merge function records from one
CoverageMapping into another, deduplicating by (filenames hash,
function name hash) the same way loadFunctionRecord does. Also
carries over SingleByteCoverage and FuncHashMismatches.

This will be used by the parallel loading path to combine per-thread
coverage shards into the final result.
---
 .../ProfileData/Coverage/CoverageMapping.h    |  4 +++
 .../ProfileData/Coverage/CoverageMapping.cpp  | 30 +++++++++++++++++++
 2 files changed, 34 insertions(+)

diff --git a/llvm/include/llvm/ProfileData/Coverage/CoverageMapping.h b/llvm/include/llvm/ProfileData/Coverage/CoverageMapping.h
index 96fb5f337b075..0806ac29fa81d 100644
--- a/llvm/include/llvm/ProfileData/Coverage/CoverageMapping.h
+++ b/llvm/include/llvm/ProfileData/Coverage/CoverageMapping.h
@@ -1051,6 +1051,10 @@ class CoverageMapping {
       const std::optional<std::reference_wrapper<IndexedInstrProfReader>>
           &ProfileReader);
 
+  /// Merge function records from \p Other into this mapping, deduplicating
+  /// by (filenames hash, function name hash) as loadFunctionRecord does.
+  void mergeFrom(CoverageMapping &&Other);
+
   /// Look up the indices for function records which are at least partially
   /// defined in the specified file. This is guaranteed to return a superset of
   /// such records: extra records not in the file may be included if there is
diff --git a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
index f21dd21e12d71..4ca4e7c97997d 100644
--- a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
+++ b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
@@ -974,6 +974,36 @@ Error CoverageMapping::loadFunctionRecord(
   return Error::success();
 }
 
+void CoverageMapping::mergeFrom(CoverageMapping &&Other) {
+  // Shards are produced from the same profile, so this must agree.
+  assert(!SingleByteCoverage || !Other.SingleByteCoverage ||
+         *SingleByteCoverage == *Other.SingleByteCoverage);
+  if (!SingleByteCoverage)
+    SingleByteCoverage = Other.SingleByteCoverage;
+
+  for (auto &Func : Other.Functions) {
+    auto FilenamesHash = hash_combine_range(Func.Filenames);
+    if (!RecordProvenance[FilenamesHash]
+             .insert(hash_value(StringRef(Func.Name)))
+             .second)
+      continue;
+
+    unsigned RecordIndex = Functions.size();
+    Functions.push_back(std::move(Func));
+
+    for (StringRef Filename : Functions.back().Filenames) {
+      auto &RecordIndices = FilenameHash2RecordIndices[hash_value(Filename)];
+      if (RecordIndices.empty() || RecordIndices.back() != RecordIndex)
+        RecordIndices.push_back(RecordIndex);
+    }
+  }
+
+  FuncHashMismatches.insert(
+      FuncHashMismatches.end(),
+      std::make_move_iterator(Other.FuncHashMismatches.begin()),
+      std::make_move_iterator(Other.FuncHashMismatches.end()));
+}
+
 // This function is for memory optimization by shortening the lifetimes
 // of CoverageMappingReader instances.
 Error CoverageMapping::loadFromReaders(

>From 89cdda68385e218dc79eba9b8b94f1074e42df41 Mon Sep 17 00:00:00 2001
From: patskovn <patskovn at devvm27524.cln0.facebook.com>
Date: Thu, 23 Apr 2026 03:34:19 -0700
Subject: [PATCH 3/4] [llvm-cov] Parallelize object file loading in
 CoverageMapping::load

When multiple object files are provided, process them in parallel
using a DefaultThreadPool. Each thread gets its own
IndexedInstrProfReader and CoverageMapping shard, then results are
merged serially via mergeFrom.

Files are assigned in contiguous chunks (not striped) so that
within each shard, objects are processed in the same relative order
as serial, preserving first-seen dedup semantics. Parallelism is
capped at 32 threads to avoid cache thrashing.

Falls back to serial processing for single-file or single-thread
cases, keeping the existing behavior unchanged.
---
 .../ProfileData/Coverage/CoverageMapping.cpp  | 78 +++++++++++++++++--
 1 file changed, 73 insertions(+), 5 deletions(-)

diff --git a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
index 4ca4e7c97997d..4b89c3b4f2834 100644
--- a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
+++ b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
@@ -27,6 +27,8 @@
 #include "llvm/Support/Error.h"
 #include "llvm/Support/ErrorHandling.h"
 #include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/ThreadPool.h"
+#include "llvm/Support/Threading.h"
 #include "llvm/Support/VirtualFileSystem.h"
 #include "llvm/Support/raw_ostream.h"
 #include <algorithm>
@@ -1117,11 +1119,77 @@ Expected<std::unique_ptr<CoverageMapping>> CoverageMapping::load(
   };
 
   SmallVector<object::BuildID> FoundBinaryIDs;
-  for (const auto &File : llvm::enumerate(ObjectFilenames)) {
-    if (Error E = loadFromFile(File.value(), GetArch(File.index()),
-                               CompilationDir, ProfileReaderRef, *Coverage,
-                               DataFound, &FoundBinaryIDs))
-      return std::move(E);
+  unsigned NumFiles = ObjectFilenames.size();
+  constexpr unsigned MaxExportThreads = 32;
+  unsigned NumThreads = std::min(
+      {hardware_concurrency(NumFiles).compute_thread_count(),
+       NumFiles, MaxExportThreads});
+
+  if (NumThreads <= 1) {
+    for (const auto &File : llvm::enumerate(ObjectFilenames)) {
+      if (Error E = loadFromFile(File.value(), GetArch(File.index()),
+                                 CompilationDir, ProfileReaderRef, *Coverage,
+                                 DataFound, &FoundBinaryIDs))
+        return std::move(E);
+    }
+  } else {
+    std::vector<std::unique_ptr<IndexedInstrProfReader>> Readers(NumThreads);
+    if (ProfileFilename) {
+      for (unsigned T = 0; T < NumThreads; ++T) {
+        auto ReaderOrErr =
+            IndexedInstrProfReader::create(ProfileFilename.value(), FS);
+        if (Error E = ReaderOrErr.takeError())
+          return createFileError(ProfileFilename.value(), std::move(E));
+        Readers[T] = std::move(ReaderOrErr.get());
+      }
+    }
+
+    struct ShardData {
+      std::unique_ptr<CoverageMapping> Cov;
+      SmallVector<object::BuildID> BIDs;
+      bool Found = false;
+      Error Err;
+      ShardData() : Err(Error::success()) {}
+      ShardData(ShardData &&) = default;
+      ShardData &operator=(ShardData &&) = default;
+    };
+
+    std::vector<ShardData> Shards(NumThreads);
+    for (auto &S : Shards)
+      S.Cov.reset(new CoverageMapping());
+
+    {
+      DefaultThreadPool Pool(hardware_concurrency(NumFiles));
+      unsigned ChunkSize = (NumFiles + NumThreads - 1) / NumThreads;
+      for (unsigned T = 0; T < NumThreads; ++T) {
+        unsigned Begin = T * ChunkSize;
+        unsigned End = std::min(Begin + ChunkSize, NumFiles);
+        Pool.async([&, T, Begin, End] {
+          auto &S = Shards[T];
+          auto ProfileReaderRef =
+              Readers[T] ? std::optional<
+                               std::reference_wrapper<IndexedInstrProfReader>>(
+                               *Readers[T])
+                         : std::nullopt;
+          for (unsigned I = Begin; I < End; ++I) {
+            if (Error E =
+                    loadFromFile(ObjectFilenames[I], GetArch(I), CompilationDir,
+                                 ProfileReaderRef, *S.Cov, S.Found, &S.BIDs)) {
+              S.Err = std::move(E);
+              return;
+            }
+          }
+        });
+      }
+    }
+
+    for (auto &S : Shards) {
+      if (S.Err)
+        return std::move(S.Err);
+      DataFound |= S.Found;
+      FoundBinaryIDs.append(S.BIDs.begin(), S.BIDs.end());
+      Coverage->mergeFrom(std::move(*S.Cov));
+    }
   }
 
   if (BIDFetcher) {

>From 877a541a904c295c7569e3362b3fea75628309f4 Mon Sep 17 00:00:00 2001
From: patskovn <patskovn at devvm27524.cln0.facebook.com>
Date: Mon, 27 Apr 2026 02:51:48 -0700
Subject: [PATCH 4/4] [llvm-cov] Add --max-load-threads flag to configure
 parallel object loading

---
 .../ProfileData/Coverage/CoverageMapping.h    |  2 +-
 .../ProfileData/Coverage/CoverageMapping.cpp  | 21 +++++++++++--------
 llvm/tools/llvm-cov/CodeCoverage.cpp          | 13 +++++++++---
 3 files changed, 23 insertions(+), 13 deletions(-)

diff --git a/llvm/include/llvm/ProfileData/Coverage/CoverageMapping.h b/llvm/include/llvm/ProfileData/Coverage/CoverageMapping.h
index 0806ac29fa81d..4e4c91d778c03 100644
--- a/llvm/include/llvm/ProfileData/Coverage/CoverageMapping.h
+++ b/llvm/include/llvm/ProfileData/Coverage/CoverageMapping.h
@@ -1080,7 +1080,7 @@ class CoverageMapping {
        std::optional<StringRef> ProfileFilename, vfs::FileSystem &FS,
        ArrayRef<StringRef> Arches = {}, StringRef CompilationDir = "",
        const object::BuildIDFetcher *BIDFetcher = nullptr,
-       bool CheckBinaryIDs = false);
+       bool CheckBinaryIDs = false, unsigned MaxLoadThreads = 1);
 
   /// The number of functions that couldn't have their profiles mapped.
   ///
diff --git a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
index 4b89c3b4f2834..9bf99ef216777 100644
--- a/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
+++ b/llvm/lib/ProfileData/Coverage/CoverageMapping.cpp
@@ -1089,11 +1089,13 @@ Error CoverageMapping::loadFromFile(
   return Error::success();
 }
 
-Expected<std::unique_ptr<CoverageMapping>> CoverageMapping::load(
-    ArrayRef<StringRef> ObjectFilenames,
-    std::optional<StringRef> ProfileFilename, vfs::FileSystem &FS,
-    ArrayRef<StringRef> Arches, StringRef CompilationDir,
-    const object::BuildIDFetcher *BIDFetcher, bool CheckBinaryIDs) {
+Expected<std::unique_ptr<CoverageMapping>>
+CoverageMapping::load(ArrayRef<StringRef> ObjectFilenames,
+                      std::optional<StringRef> ProfileFilename,
+                      vfs::FileSystem &FS, ArrayRef<StringRef> Arches,
+                      StringRef CompilationDir,
+                      const object::BuildIDFetcher *BIDFetcher,
+                      bool CheckBinaryIDs, unsigned MaxLoadThreads) {
   std::unique_ptr<IndexedInstrProfReader> ProfileReader;
   if (ProfileFilename) {
     auto ProfileReaderOrErr =
@@ -1120,10 +1122,11 @@ Expected<std::unique_ptr<CoverageMapping>> CoverageMapping::load(
 
   SmallVector<object::BuildID> FoundBinaryIDs;
   unsigned NumFiles = ObjectFilenames.size();
-  constexpr unsigned MaxExportThreads = 32;
-  unsigned NumThreads = std::min(
-      {hardware_concurrency(NumFiles).compute_thread_count(),
-       NumFiles, MaxExportThreads});
+  unsigned NumThreads =
+      MaxLoadThreads <= 1
+          ? 1
+          : std::min({hardware_concurrency(NumFiles).compute_thread_count(),
+                      NumFiles, MaxLoadThreads});
 
   if (NumThreads <= 1) {
     for (const auto &File : llvm::enumerate(ObjectFilenames)) {
diff --git a/llvm/tools/llvm-cov/CodeCoverage.cpp b/llvm/tools/llvm-cov/CodeCoverage.cpp
index e19101ac76045..3f16a68c77da3 100644
--- a/llvm/tools/llvm-cov/CodeCoverage.cpp
+++ b/llvm/tools/llvm-cov/CodeCoverage.cpp
@@ -190,6 +190,7 @@ class CodeCoverageTool {
   std::unique_ptr<object::BuildIDFetcher> BIDFetcher;
 
   bool CheckBinaryIDs;
+  unsigned MaxLoadThreads;
 };
 }
 
@@ -462,9 +463,10 @@ std::unique_ptr<CoverageMapping> CodeCoverageTool::load() {
                 ObjectFilename);
   }
   auto FS = vfs::getRealFileSystem();
-  auto CoverageOrErr = CoverageMapping::load(
-      ObjectFilenames, PGOFilename, *FS, CoverageArches,
-      ViewOpts.CompilationDirectory, BIDFetcher.get(), CheckBinaryIDs);
+  auto CoverageOrErr =
+      CoverageMapping::load(ObjectFilenames, PGOFilename, *FS, CoverageArches,
+                            ViewOpts.CompilationDirectory, BIDFetcher.get(),
+                            CheckBinaryIDs, MaxLoadThreads);
   if (Error E = CoverageOrErr.takeError()) {
     error("failed to load coverage: " + toString(std::move(E)));
     return nullptr;
@@ -803,6 +805,10 @@ int CodeCoverageTool::run(Command Cmd, int argc, const char **argv) {
   cl::alias NumThreadsA("j", cl::desc("Alias for --num-threads"),
                         cl::aliasopt(NumThreads));
 
+  cl::opt<unsigned> MaxLoadThreads(
+      "max-load-threads", cl::init(1),
+      cl::desc("Maximum threads to use for loading object files (default: 1)"));
+
   cl::opt<std::string> CompilationDirectory(
       "compilation-dir", cl::init(""),
       cl::desc("Directory used as a base for relative coverage mapping paths"));
@@ -821,6 +827,7 @@ int CodeCoverageTool::run(Command Cmd, int argc, const char **argv) {
       BIDFetcher = std::make_unique<object::BuildIDFetcher>(DebugFileDirectory);
     }
     this->CheckBinaryIDs = CheckBinaryIDs;
+    this->MaxLoadThreads = MaxLoadThreads;
 
     if (!PGOFilename.empty() == EmptyProfile) {
       error(



More information about the llvm-commits mailing list