Author: Jan Svoboda
Date: 2026-02-12T08:48:13-08:00
New Revision: 827b5d9423083da6111ddc382c9d4449ff853949

URL: 
https://github.com/llvm/llvm-project/commit/827b5d9423083da6111ddc382c9d4449ff853949
DIFF: 
https://github.com/llvm/llvm-project/commit/827b5d9423083da6111ddc382c9d4449ff853949.diff

LOG: [clang][deps] Parallelize module compilations (#180047)

In a typical build, the build system schedules many TUs from the same
target to be scanned/compiled at the same time. These TUs tend to depend
on a similar set of modules, and they usually keep their imports
alphabetically sorted. The nature of implicit modules then means that
scanning these TUs reduces into a single-threaded computation, since
only one TU wins the race to compile the common dependency module, and
the same thread/process keeps being responsible for compiling all
transitive dependencies of such module.

This PR makes use of the single-module-parse-mode in a new scanning step
that runs at the start of each TU scan. In this step, the scanner
quickly discovers unconditional module dependencies of the TU without
blocking on its compile. This typically discovers plenty of work to keep
the available threads busy and compile modules in more parallel fashion.
Modules discovered here are compiled on separate threads right away in
the same two-step fashion.

The second step then performs the regular dependency scan of the TU
where each module import is a blocking operation. However, by this time,
the first scanning step most likely already compiled the majority of
modules, so there's actually little to no waiting happening here. The
compiler only deserializes previously-built modules.

This is a barebones implementation with some known quirks marked in
FIXME comments. I will continue working on performance and polish once
this lands.

Added: 
    

Modified: 
    clang/include/clang/DependencyScanning/DependencyScanningService.h
    clang/include/clang/Serialization/ASTReader.h
    clang/lib/DependencyScanning/DependencyScannerImpl.cpp
    clang/lib/DependencyScanning/DependencyScanningService.cpp
    clang/tools/clang-scan-deps/ClangScanDeps.cpp
    clang/tools/clang-scan-deps/Opts.td

Removed: 
    


################################################################################
diff  --git 
a/clang/include/clang/DependencyScanning/DependencyScanningService.h 
b/clang/include/clang/DependencyScanning/DependencyScanningService.h
index 371b862996706..9e8041836bc26 100644
--- a/clang/include/clang/DependencyScanning/DependencyScanningService.h
+++ b/clang/include/clang/DependencyScanning/DependencyScanningService.h
@@ -85,6 +85,7 @@ class DependencyScanningService {
       ScanningMode Mode, ScanningOutputFormat Format,
       ScanningOptimizations OptimizeArgs = ScanningOptimizations::Default,
       bool EagerLoadModules = false, bool TraceVFS = false,
+      bool AsyncScanModules = false,
       std::time_t BuildSessionTimestamp =
           llvm::sys::toTimeT(std::chrono::system_clock::now()));
 
@@ -98,6 +99,8 @@ class DependencyScanningService {
 
   bool shouldTraceVFS() const { return TraceVFS; }
 
+  bool shouldScanModulesAsynchronously() const { return AsyncScanModules; }
+
   DependencyScanningFilesystemSharedCache &getSharedCache() {
     return SharedCache;
   }
@@ -115,6 +118,8 @@ class DependencyScanningService {
   const bool EagerLoadModules;
   /// Whether to trace VFS accesses.
   const bool TraceVFS;
+  /// Whether to scan modules asynchronously.
+  const bool AsyncScanModules;
   /// The global file system cache.
   DependencyScanningFilesystemSharedCache SharedCache;
   /// The global module cache entries.

diff  --git a/clang/include/clang/Serialization/ASTReader.h 
b/clang/include/clang/Serialization/ASTReader.h
index d1c1b98f78f2c..1c17e91b51d7e 100644
--- a/clang/include/clang/Serialization/ASTReader.h
+++ b/clang/include/clang/Serialization/ASTReader.h
@@ -1541,7 +1541,6 @@ class ASTReader
   /// If we have any unloaded specialization for \p D
   bool haveUnloadedSpecializations(const Decl *D) const;
 
-private:
   struct ImportedModule {
     ModuleFile *Mod;
     ModuleFile *ImportedBy;
@@ -1559,6 +1558,8 @@ class ASTReader
                             off_t ExpectedSize, time_t ExpectedModTime,
                             ASTFileSignature ExpectedSignature,
                             unsigned ClientLoadCapabilities);
+
+private:
   ASTReadResult ReadControlBlock(ModuleFile &F,
                                  SmallVectorImpl<ImportedModule> &Loaded,
                                  const ModuleFile *ImportedBy,

diff  --git a/clang/lib/DependencyScanning/DependencyScannerImpl.cpp 
b/clang/lib/DependencyScanning/DependencyScannerImpl.cpp
index ec157cdd8f464..cafd3eb976312 100644
--- a/clang/lib/DependencyScanning/DependencyScannerImpl.cpp
+++ b/clang/lib/DependencyScanning/DependencyScannerImpl.cpp
@@ -9,12 +9,20 @@
 #include "clang/DependencyScanning/DependencyScannerImpl.h"
 #include "clang/Basic/DiagnosticFrontend.h"
 #include "clang/Basic/DiagnosticSerialization.h"
+#include "clang/DependencyScanning/DependencyScanningFilesystem.h"
+#include "clang/DependencyScanning/DependencyScanningService.h"
 #include "clang/DependencyScanning/DependencyScanningWorker.h"
 #include "clang/Driver/Driver.h"
 #include "clang/Frontend/FrontendActions.h"
+#include "llvm/ADT/IntrusiveRefCntPtr.h"
 #include "llvm/ADT/ScopeExit.h"
+#include "llvm/Support/AdvisoryLock.h"
+#include "llvm/Support/CrashRecoveryContext.h"
+#include "llvm/Support/VirtualFileSystem.h"
 #include "llvm/TargetParser/Host.h"
 
+#include <thread>
+
 using namespace clang;
 using namespace dependencies;
 
@@ -546,6 +554,133 @@ dependencies::initializeScanInstanceDependencyCollector(
   return MDC;
 }
 
+struct SingleModuleWithAsyncModuleCompiles : PreprocessOnlyAction {
+  DependencyScanningService &Service;
+
+  SingleModuleWithAsyncModuleCompiles(DependencyScanningService &Service)
+      : Service(Service) {}
+
+  bool BeginSourceFileAction(CompilerInstance &CI) override;
+};
+
+/// The preprocessor callback that takes care of initiating an asynchronous
+/// module compilation if needed.
+struct AsyncModuleCompile : PPCallbacks {
+  CompilerInstance &CI;
+  DependencyScanningService &Service;
+
+  AsyncModuleCompile(CompilerInstance &CI, DependencyScanningService &Service)
+      : CI(CI), Service(Service) {}
+
+  void moduleLoadSkipped(Module *M) override {
+    M = M->getTopLevelModule();
+
+    HeaderSearch &HS = CI.getPreprocessor().getHeaderSearchInfo();
+    ModuleCache &ModCache = CI.getModuleCache();
+    std::string ModuleFileName = HS.getCachedModuleFileName(M);
+
+    uint64_t Timestamp = ModCache.getModuleTimestamp(ModuleFileName);
+    // Someone else already built/validated the PCM.
+    if (Timestamp > CI.getHeaderSearchOpts().BuildSessionTimestamp)
+      return;
+
+    if (!CI.getASTReader())
+      CI.createASTReader();
+    SmallVector<ASTReader::ImportedModule, 0> Imported;
+    // Only calling ReadASTCore() to avoid the expensive eager deserialization
+    // of the clang::Module objects in ReadAST().
+    // FIXME: Consider doing this in the new thread depending on how expensive
+    // the read turns out to be.
+    switch (CI.getASTReader()->ReadASTCore(
+        ModuleFileName, serialization::MK_ImplicitModule, SourceLocation(),
+        nullptr, Imported, {}, {}, {},
+        ASTReader::ARR_OutOfDate | ASTReader::ARR_Missing |
+            ASTReader::ARR_TreatModuleWithErrorsAsOutOfDate)) {
+    case ASTReader::Success:
+      // We successfully read a valid, up-to-date PCM.
+      // FIXME: This could update the timestamp. Regular calls to
+      // ASTReader::ReadAST() would do so unless they encountered corrupted
+      // AST block, corrupted extension block, or did not read the expected
+      // top-level module.
+      return;
+    case ASTReader::OutOfDate:
+    case ASTReader::Missing:
+      // The most interesting case.
+      break;
+    default:
+      // Let the regular scan diagnose this.
+      return;
+    }
+
+    ModCache.prepareForGetLock(ModuleFileName);
+    auto Lock = ModCache.getLock(ModuleFileName);
+    bool Owned;
+    llvm::Error LockErr = Lock->tryLock().moveInto(Owned);
+    // Someone else is building the PCM right now.
+    if (!LockErr && !Owned)
+      return;
+    // We should build the PCM.
+    // FIXME: Pass the correct BaseFS to the worker FS.
+    IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS =
+        llvm::makeIntrusiveRefCnt<DependencyScanningWorkerFilesystem>(
+            Service.getSharedCache(), llvm::vfs::getRealFileSystem());
+    VFS = createVFSFromCompilerInvocation(CI.getInvocation(),
+                                          CI.getDiagnostics(), std::move(VFS));
+    auto DC = std::make_unique<DiagnosticConsumer>();
+    auto MC = makeInProcessModuleCache(Service.getModuleCacheEntries());
+    CompilerInstance::ThreadSafeCloneConfig CloneConfig(std::move(VFS), *DC,
+                                                        std::move(MC));
+    auto ModCI1 = CI.cloneForModuleCompile(SourceLocation(), M, ModuleFileName,
+                                           CloneConfig);
+    auto ModCI2 = CI.cloneForModuleCompile(SourceLocation(), M, ModuleFileName,
+                                           CloneConfig);
+
+    // FIXME: Have the service own a thread pool and use that instead.
+    // FIXME: This lock belongs to a module cache that might not outlive the
+    // thread. (This should work for now, because the in-process lock only
+    // refers to an object managed by the service, which should outlive this
+    // thread.)
+    std::thread([Lock = std::move(Lock), ModCI1 = std::move(ModCI1),
+                 ModCI2 = std::move(ModCI2), DC = std::move(DC),
+                 Service = &Service] {
+      llvm::CrashRecoveryContext CRC;
+      (void)CRC.RunSafely([&] {
+        // Quickly discovers and compiles modules for the real scan below.
+        SingleModuleWithAsyncModuleCompiles Action1(*Service);
+        (void)ModCI1->ExecuteAction(Action1);
+        // The real scan below.
+        ModCI2->getPreprocessorOpts().SingleModuleParseMode = false;
+        GenerateModuleFromModuleMapAction Action2;
+        (void)ModCI2->ExecuteAction(Action2);
+      });
+    }).detach();
+  }
+};
+
+/// Runs the preprocessor on a TU with single-module-parse-mode and compiles
+/// modules asynchronously without blocking or importing them.
+struct SingleTUWithAsyncModuleCompiles : PreprocessOnlyAction {
+  DependencyScanningService &Service;
+
+  SingleTUWithAsyncModuleCompiles(DependencyScanningService &Service)
+      : Service(Service) {}
+
+  bool BeginSourceFileAction(CompilerInstance &CI) override {
+    CI.getInvocation().getPreprocessorOpts().SingleModuleParseMode = true;
+    CI.getPreprocessor().addPPCallbacks(
+        std::make_unique<AsyncModuleCompile>(CI, Service));
+    return true;
+  }
+};
+
+bool SingleModuleWithAsyncModuleCompiles::BeginSourceFileAction(
+    CompilerInstance &CI) {
+  CI.getInvocation().getPreprocessorOpts().SingleModuleParseMode = true;
+  CI.getPreprocessor().addPPCallbacks(
+      std::make_unique<AsyncModuleCompile>(CI, Service));
+  return true;
+}
+
 bool DependencyScanningAction::runInvocation(
     std::string Executable,
     std::unique_ptr<CompilerInvocation> OriginalInvocation,
@@ -574,6 +709,34 @@ bool DependencyScanningAction::runInvocation(
   // Create a compiler instance to handle the actual work.
   auto ScanInvocation =
       createScanCompilerInvocation(*OriginalInvocation, Service);
+
+  // Quickly discovers and compiles modules for the real scan below.
+  if (Service.shouldScanModulesAsynchronously()) {
+    auto ModCache = makeInProcessModuleCache(Service.getModuleCacheEntries());
+    auto ScanInstanceStorage = std::make_unique<CompilerInstance>(
+        std::make_shared<CompilerInvocation>(*ScanInvocation), PCHContainerOps,
+        std::move(ModCache));
+    CompilerInstance &ScanInstance = *ScanInstanceStorage;
+
+    DiagnosticConsumer DiagConsumer;
+    initializeScanCompilerInstance(ScanInstance, FS, &DiagConsumer, Service,
+                                   DepFS);
+
+    // FIXME: Do this only once.
+    SmallVector<StringRef> StableDirs = getInitialStableDirs(ScanInstance);
+    auto MaybePrebuiltModulesASTMap =
+        computePrebuiltModulesASTMap(ScanInstance, StableDirs);
+    if (!MaybePrebuiltModulesASTMap)
+      return false;
+
+    // Normally this would be handled by GeneratePCHAction
+    if (ScanInstance.getFrontendOpts().ProgramAction == frontend::GeneratePCH)
+      ScanInstance.getLangOpts().CompilingPCH = true;
+
+    SingleTUWithAsyncModuleCompiles Action(Service);
+    (void)ScanInstance.ExecuteAction(Action);
+  }
+
   auto ModCache = makeInProcessModuleCache(Service.getModuleCacheEntries());
   ScanInstanceStorage.emplace(std::move(ScanInvocation),
                               std::move(PCHContainerOps), std::move(ModCache));
@@ -747,6 +910,8 @@ bool CompilerInstanceWithContext::computeDependencies(
                          FileType, PrevFID, IDLocation);
   }
 
+  // FIXME: Scan modules asynchronously here as well.
+
   SrcLocOffset++;
   SmallVector<IdentifierLoc, 2> Path;
   IdentifierInfo *ModuleID = PP.getIdentifierInfo(ModuleName);

diff  --git a/clang/lib/DependencyScanning/DependencyScanningService.cpp 
b/clang/lib/DependencyScanning/DependencyScanningService.cpp
index 72f359e56d116..9224de39b40cc 100644
--- a/clang/lib/DependencyScanning/DependencyScanningService.cpp
+++ b/clang/lib/DependencyScanning/DependencyScanningService.cpp
@@ -14,7 +14,8 @@ using namespace dependencies;
 DependencyScanningService::DependencyScanningService(
     ScanningMode Mode, ScanningOutputFormat Format,
     ScanningOptimizations OptimizeArgs, bool EagerLoadModules, bool TraceVFS,
-    std::time_t BuildSessionTimestamp)
+    bool AsyncScanModules, std::time_t BuildSessionTimestamp)
     : Mode(Mode), Format(Format), OptimizeArgs(OptimizeArgs),
       EagerLoadModules(EagerLoadModules), TraceVFS(TraceVFS),
+      AsyncScanModules(AsyncScanModules),
       BuildSessionTimestamp(BuildSessionTimestamp) {}

diff  --git a/clang/tools/clang-scan-deps/ClangScanDeps.cpp 
b/clang/tools/clang-scan-deps/ClangScanDeps.cpp
index bfca051c68299..efa9bff019a60 100644
--- a/clang/tools/clang-scan-deps/ClangScanDeps.cpp
+++ b/clang/tools/clang-scan-deps/ClangScanDeps.cpp
@@ -94,6 +94,7 @@ static std::string TranslationUnitFile;
 static bool DeprecatedDriverCommand;
 static ResourceDirRecipeKind ResourceDirRecipe;
 static bool Verbose;
+static bool AsyncScanModules;
 static bool PrintTiming;
 static bool EmitVisibleModules;
 static llvm::BumpPtrAllocator Alloc;
@@ -239,6 +240,8 @@ static void ParseArgs(int argc, char **argv) {
 
   Verbose = Args.hasArg(OPT_verbose);
 
+  AsyncScanModules = Args.hasArg(OPT_async_scan_modules);
+
   RoundTripArgs = Args.hasArg(OPT_round_trip_args);
 
   VerbatimArgs = Args.hasArg(OPT_verbatim_args);
@@ -1136,7 +1139,8 @@ int clang_scan_deps_main(int argc, char **argv, const 
llvm::ToolContext &) {
   };
 
   DependencyScanningService Service(ScanMode, Format, OptimizeArgs,
-                                    EagerLoadModules, /*TraceVFS=*/Verbose);
+                                    EagerLoadModules, /*TraceVFS=*/Verbose,
+                                    AsyncScanModules);
 
   llvm::Timer T;
   T.startTimer();

diff  --git a/clang/tools/clang-scan-deps/Opts.td 
b/clang/tools/clang-scan-deps/Opts.td
index 6ea9d824c9646..21d0de7aa07f2 100644
--- a/clang/tools/clang-scan-deps/Opts.td
+++ b/clang/tools/clang-scan-deps/Opts.td
@@ -44,6 +44,8 @@ def emit_visible_modules
 
 def verbose : F<"v", "Use verbose output">;
 
+def async_scan_modules : F<"async-scan-modules", "Scan modules 
asynchronously">;
+
 def round_trip_args : F<"round-trip-args", "verify that command-line arguments 
are canonical by parsing and re-serializing">;
 
 def verbatim_args : F<"verbatim-args", "Pass commands to the scanner verbatim 
without adjustments">;


        
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to