Skip to content

DTLTO Cache optimization - rebased#203971

Open
romanova-ekaterina wants to merge 11 commits into
llvm:mainfrom
romanova-ekaterina:dtlto-cache-rename-optimization-rebased
Open

DTLTO Cache optimization - rebased#203971
romanova-ekaterina wants to merge 11 commits into
llvm:mainfrom
romanova-ekaterina:dtlto-cache-rename-optimization-rebased

Conversation

@romanova-ekaterina

@romanova-ekaterina romanova-ekaterina commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

I created this new PR for DTLTO cache optimization and I will abandoning/closing old PR for that (#193281). After a major rewrite of DTLTO functionality that happened recently (#192629), it was easier to start from scratch than to merge.

Here is what had changed:
Currently, DTLTO cache works this way: when native object file is generated and sent back by the remote compilation, it's being saved on the disk, then the content of this file is read and written into a buffer and subsequently it's read from the buffer and written into the cache file. This is obviously non-efficient.

With this optimization, after the native object file is saved on the disk, it's being renamed into a cache file.

@llvmorg-github-actions llvmorg-github-actions Bot added lld lld:ELF lld:COFF platform:windows LTO Link time optimization (regular/full LTO or ThinLTO) llvm:support labels Jun 15, 2026
@llvmorg-github-actions

llvmorg-github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

@llvm/pr-subscribers-platform-windows
@llvm/pr-subscribers-llvm-support
@llvm/pr-subscribers-lto
@llvm/pr-subscribers-lld-coff
@llvm/pr-subscribers-lld

@llvm/pr-subscribers-lld-elf

Author: Katya Romanova (romanova-ekaterina)

Changes

I created this new PR for DTLTO cache optimization and I will abandoning/closing old PR for that (#193281). After a major rewrite of DTLTO functionality, it was easier to start from scratch than to merge.

Here is what had changed:
Currently, DTLTO cache works this way: when native object file is generated and sent back by the remote compilation, it's being saved on the disk, then the content of this file is read and written into a buffer and subsequently it's read from the buffer and written into the cache file. This is obviously non-efficient.

With this optimization, after the native object file is saved on the disk, it's being renamed into a cache file.


Full diff: https://github.com/llvm/llvm-project/pull/203971.diff

7 Files Affected:

  • (modified) lld/COFF/LTO.cpp (+2-1)
  • (modified) lld/ELF/LTO.cpp (+2-1)
  • (modified) llvm/include/llvm/Support/Caching.h (+8-7)
  • (modified) llvm/lib/DTLTO/DTLTO.cpp (+11-2)
  • (modified) llvm/lib/Support/Caching.cpp (+39-1)
  • (added) llvm/test/ThinLTO/X86/dtlto/dtlto-cache-rename-optimization.ll (+73)
  • (modified) llvm/tools/llvm-lto2/llvm-lto2.cpp (+2-1)
diff --git a/lld/COFF/LTO.cpp b/lld/COFF/LTO.cpp
index 0329f6c2e9cea..4bfd62e7f7e2f 100644
--- a/lld/COFF/LTO.cpp
+++ b/lld/COFF/LTO.cpp
@@ -206,7 +206,8 @@ std::vector<InputFile *> BitcodeCompiler::compile() {
   FileCache cache;
   if (!ctx.config.ltoCache.empty())
     cache = check(localCache("ThinLTO", "Thin", ctx.config.ltoCache,
-                             createAddBufferFn(files, file_names)));
+                             createAddBufferFn(files, file_names),
+                             !ctx.config.dtltoDistributor.empty()));
 
   checkError(ltoObj->run(
       [&](size_t task, const Twine &moduleName) {
diff --git a/lld/ELF/LTO.cpp b/lld/ELF/LTO.cpp
index e40575bffec62..4ec301890c7c8 100644
--- a/lld/ELF/LTO.cpp
+++ b/lld/ELF/LTO.cpp
@@ -336,7 +336,8 @@ SmallVector<std::unique_ptr<InputFile>, 0> BitcodeCompiler::compile() {
   FileCache cache;
   if (!ctx.arg.thinLTOCacheDir.empty())
     cache = check(localCache("ThinLTO", "Thin", ctx.arg.thinLTOCacheDir,
-                             createAddBufferFn(files, filenames)));
+                             createAddBufferFn(files, filenames),
+                             !ctx.arg.dtltoDistributor.empty()));
 
   if (!ctx.bitcodeFiles.empty())
     checkError(ctx.e, ltoObj->run(
diff --git a/llvm/include/llvm/Support/Caching.h b/llvm/include/llvm/Support/Caching.h
index cebf071b0188f..4f3852bd310f7 100644
--- a/llvm/include/llvm/Support/Caching.h
+++ b/llvm/include/llvm/Support/Caching.h
@@ -20,6 +20,11 @@
 #include "llvm/Support/MemoryBuffer.h"
 
 namespace llvm {
+/// This type defines the callback to add a pre-existing file (e.g. in a cache).
+///
+/// Buffer callbacks must be thread safe.
+using AddBufferFn = std::function<void(unsigned Task, const Twine &ModuleName,
+                                       std::unique_ptr<MemoryBuffer> MB)>;
 
 /// This class wraps an output stream for a file. Most clients should just be
 /// able to return an instance of this base class from the stream callback, but
@@ -50,6 +55,7 @@ class CachedFileStream {
     if (!Committed)
       report_fatal_error("CachedFileStream was not committed.\n");
   }
+  virtual AddBufferFn GetAddBuffer() { return AddBufferFn(); }
 };
 
 /// This type defines the callback to add a file that is generated on the fly.
@@ -101,12 +107,6 @@ struct FileCache {
   std::string CacheDirectoryPath;
 };
 
-/// This type defines the callback to add a pre-existing file (e.g. in a cache).
-///
-/// Buffer callbacks must be thread safe.
-using AddBufferFn = std::function<void(unsigned Task, const Twine &ModuleName,
-                                       std::unique_ptr<MemoryBuffer> MB)>;
-
 /// Create a local file system cache which uses the given cache name, temporary
 /// file prefix, cache directory and file callback.  This function does not
 /// immediately create the cache directory if it does not yet exist; this is
@@ -117,7 +117,8 @@ LLVM_ABI Expected<FileCache> localCache(
     const Twine &CacheNameRef, const Twine &TempFilePrefixRef,
     const Twine &CacheDirectoryPathRef,
     AddBufferFn AddBuffer = [](size_t Task, const Twine &ModuleName,
-                               std::unique_ptr<MemoryBuffer> MB) {});
+                               std::unique_ptr<MemoryBuffer> MB) {},
+    bool CacheFileRename = false);
 } // namespace llvm
 
 #endif
diff --git a/llvm/lib/DTLTO/DTLTO.cpp b/llvm/lib/DTLTO/DTLTO.cpp
index d1917eb8d954a..3ff28b42bc7b0 100644
--- a/llvm/lib/DTLTO/DTLTO.cpp
+++ b/llvm/lib/DTLTO/DTLTO.cpp
@@ -284,9 +284,18 @@ Error lto::DTLTO::addObjectFilesToLink() {
             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();
+
+      // Store a file path into the cache stream. This file later will be
+      // renamed into cache file.
+      *(CacheStream.OS) << Job.NativeObjectPath;
+      if (Error Err = CacheStream.commit())
+        return Err;
+
+      AddBufferFn AddBuffer = CacheStream.GetAddBuffer();
+      AddBuffer(Job.Task, Job.ModuleID, std::move(ObjFileMbOrErr.get()));
+
       if (Error Err = CacheStream.commit())
         return Err;
     } else {
diff --git a/llvm/lib/Support/Caching.cpp b/llvm/lib/Support/Caching.cpp
index 40a5c44771b65..a3eb6ecc0077c 100644
--- a/llvm/lib/Support/Caching.cpp
+++ b/llvm/lib/Support/Caching.cpp
@@ -21,6 +21,7 @@
 #if !defined(_MSC_VER) && !defined(__MINGW32__)
 #include <unistd.h>
 #else
+#include "llvm/Support/Windows/WindowsSupport.h"
 #include <io.h>
 #endif
 
@@ -29,7 +30,7 @@ using namespace llvm;
 Expected<FileCache> llvm::localCache(const Twine &CacheNameRef,
                                      const Twine &TempFilePrefixRef,
                                      const Twine &CacheDirectoryPathRef,
-                                     AddBufferFn AddBuffer) {
+                                     AddBufferFn AddBuffer, bool CacheRename) {
 
   // Create local copies which are safely captured-by-copy in lambdas
   SmallString<64> CacheName, TempFilePrefix, CacheDirectoryPath;
@@ -142,6 +143,37 @@ Expected<FileCache> llvm::localCache(const Twine &CacheNameRef,
         AddBuffer(Task, ModuleName, std::move(*MBOrErr));
         return Error::success();
       }
+      virtual AddBufferFn GetAddBuffer() override { return AddBuffer; }
+    };
+
+    // This class is responsible for renaming/moving existing file into a
+    // cache directory. The path for an input file is passed through a string
+    // stream.
+    struct MoveFileToCache : CachedFileStream {
+      AddBufferFn AddBuffer;
+      size_t Task;
+      SmallString<0> FilePath;
+      MoveFileToCache(AddBufferFn AddBuffer, std::string EntryPath, size_t Task)
+          : CachedFileStream(std::make_unique<raw_svector_ostream>(FilePath),
+                             std::move(EntryPath)),
+            AddBuffer(std::move(AddBuffer)), Task(Task) {}
+      ~MoveFileToCache() {
+        // Rename/move native object file into cache directory, if they are
+        // located the same device/logical drive, otherwise we use a copy.
+        std::error_code EC = sys::fs::rename(FilePath, ObjectPathName);
+#ifdef _WIN32
+        if (EC ==
+            std::error_code(ERROR_NOT_SAME_DEVICE, std::system_category()))
+#else
+        if (EC == std::make_error_code(std::errc::cross_device_link))
+#endif
+          EC = sys::fs::copy_file(FilePath, ObjectPathName);
+        if (EC)
+          report_fatal_error(Twine("Failed to rename or copy file ") +
+                             FilePath + " to " + ObjectPathName + ": " +
+                             EC.message() + "\n");
+      }
+      virtual AddBufferFn GetAddBuffer() override { return AddBuffer; }
     };
 
     return [=](size_t Task, const Twine &ModuleName)
@@ -153,6 +185,12 @@ Expected<FileCache> llvm::localCache(const Twine &CacheNameRef,
         return createStringError(EC, Twine("can't create cache directory ") +
                                          CacheDirectoryPath + ": " +
                                          EC.message());
+      // MoveFileToChache class will rename/move the file into the cache on
+      // destruction.
+      if (CacheRename) {
+        return std::make_unique<MoveFileToCache>(
+            AddBuffer, std::string(EntryPath.str()), Task);
+      }
 
       // Write to a temporary to avoid race condition
       SmallString<64> TempFilenameModel;
diff --git a/llvm/test/ThinLTO/X86/dtlto/dtlto-cache-rename-optimization.ll b/llvm/test/ThinLTO/X86/dtlto/dtlto-cache-rename-optimization.ll
new file mode 100644
index 0000000000000..932770a5a0909
--- /dev/null
+++ b/llvm/test/ThinLTO/X86/dtlto/dtlto-cache-rename-optimization.ll
@@ -0,0 +1,73 @@
+; DTLTO with llvm-lto2 and -cache-dir, using --save-temps
+; to leave distributor artifacts saved on the disk (except native object files).
+; native object files shouldn't be saved, because they are renamed into
+; cache entries. 
+; The test also checks that the cache is populated
+RUN: rm -rf %t && split-file %s %t && cd %t
+
+; Generate bitcode files with summary.
+RUN: opt -thinlto-bc t1.ll -o t1.bc
+RUN: opt -thinlto-bc t2.ll -o t2.bc
+
+; Generate fake object files for mock.py to return.
+RUN: touch t1.o t2.o
+
+; Create an empty subdirectory to avoid having to account for the input files.
+RUN: mkdir %t/out && cd %t/out
+
+DEFINE: %{command} = llvm-lto2 run ../t1.bc ../t2.bc -o t.o -cache-dir cache-dir \
+DEFINE:   --save-temps \
+DEFINE:   -dtlto-distributor=%python \
+DEFINE:   -dtlto-distributor-arg=%llvm_src_root/utils/dtlto/mock.py,../t1.o,../t2.o \
+DEFINE:   -r=../t1.bc,t1,px \
+DEFINE:   -r=../t2.bc,t2,px
+
+RUN: %{command}
+
+; 12 save-temps outputs plus the cache directory.
+RUN: ls | count 11
+RUN: ls | FileCheck %s --check-prefixes=THINLTO,SAVETEMPS,NOT-NATIVE
+RUN: ls cache-dir/* | count 2
+RUN: ls cache-dir/llvmcache-* | count 2
+
+; llvm-lto2 ThinLTO output files.
+THINLTO-DAG: {{^}}t.o.1{{$}}
+THINLTO-DAG: {{^}}t.o.2{{$}}
+
+; Common -save-temps files from llvm-lto2.
+SAVETEMPS-DAG: {{^}}t.o.resolution.txt{{$}}
+SAVETEMPS-DAG: {{^}}t.o.index.bc{{$}}
+SAVETEMPS-DAG: {{^}}t.o.index.dot{{$}}
+
+; -save-temps incremental files.
+SAVETEMPS-DAG: {{^}}t.o.0.0.preopt.bc{{$}}
+SAVETEMPS-DAG: {{^}}t.o.0.2.internalize.bc{{$}}
+
+; A jobs description JSON.
+SAVETEMPS-DAG: {{^}}t.[[#]].dist-file.json{{$}}
+
+; Summary shards emitted for DTLTO.
+SAVETEMPS-DAG: {{^}}t1.1.[[#]].native.o.thinlto.bc{{$}}
+SAVETEMPS-DAG: {{^}}t2.2.[[#]].native.o.thinlto.bc{{$}}
+
+; DTLTO native output files (the results of the external backend compilations)
+; should not be saved, because these files are renamed into cache entries
+NOT-NATIVE-NOT: native.o{{$}}
+
+;--- t1.ll
+
+target triple = "x86_64-unknown-linux-gnu"
+target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
+
+define void @t1() {
+  ret void
+}
+
+;--- t2.ll
+
+target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
+target triple = "x86_64-unknown-linux-gnu"
+
+define void @t2() {
+  ret void
+}
\ No newline at end of file
diff --git a/llvm/tools/llvm-lto2/llvm-lto2.cpp b/llvm/tools/llvm-lto2/llvm-lto2.cpp
index 4bcf04df89ac9..848ba0a5c3289 100644
--- a/llvm/tools/llvm-lto2/llvm-lto2.cpp
+++ b/llvm/tools/llvm-lto2/llvm-lto2.cpp
@@ -537,7 +537,8 @@ static int run(int argc, char **argv) {
 
   FileCache Cache;
   if (!CacheDir.empty())
-    Cache = check(localCache("ThinLTO", "Thin", CacheDir, AddBuffer),
+    Cache = check(localCache("ThinLTO", "Thin", CacheDir, AddBuffer,
+                             !DTLTODistributor.empty()),
                   "failed to create cache");
 
   check(Lto->run(AddStream, Cache), "LTO::run failed");

@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown

🪟 Windows x64 Test Results

  • 138881 tests passed
  • 3578 tests skipped

✅ The build succeeded and all tests passed.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 200780 tests passed
  • 5478 tests skipped

✅ The build succeeded and all tests passed.

@teresajohnson

Copy link
Copy Markdown
Contributor

I see there are a few DTLTO cache test failures - can you take a look and let me know when this is ready for review?

@romanova-ekaterina

romanova-ekaterina commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

@teresajohnson: All tests had passed.

@@ -117,7 +117,8 @@ LLVM_ABI Expected<FileCache> localCache(
const Twine &CacheNameRef, const Twine &TempFilePrefixRef,
const Twine &CacheDirectoryPathRef,
AddBufferFn AddBuffer = [](size_t Task, const Twine &ModuleName,
std::unique_ptr<MemoryBuffer> MB) {});
std::unique_ptr<MemoryBuffer> MB) {},
bool CacheFileRename = false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document usage in the comment

Comment thread llvm/lib/DTLTO/DTLTO.cpp Outdated

// Store a file path into the cache stream. This file later will be
// renamed into cache file.
*(CacheStream.OS) << Job.NativeObjectPath;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handling here and below with the direct AddBuffer callback is non-intuitive, and feels like it is contorting the existing interface in unnecessary ways.

Rather than adding the accessor to get AddBuffer, and using the OS to write the path, I think it would be cleaner and clearer to just make a dedicated method in your new MoveFileToCache class that takes the NativeObjectPath and ObjFileMbOrErr (and anything else that can't be saved in the new class via the constructor), and do the rename and call to AddBuffer there. You probably don't need to wait until commit to do the rename? Then you could simplify it (or just remove the override and fall back to the base class commit).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @teresajohnson. For this optimisation, I think it would be useful to add functionality to the local file cache (llvm/lib/Support/Caching.cpp) to consume an existing on-disk file directly into the cache, instead of always reopening it and streaming its contents through a cache writer. This would involve modifying the Support library, but I think that is appropriate here because the functionality is generally useful, even if DTLTO would be the only user initially.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the approach that Ben proposed is simpler, the only problem is that it won't fit the existing pattern/workflow of cache usage for other scenarios; because of that I prefer to implement what Teresa proposed. However, I don't have a strong opinion about it. I will be happy to implement what Ben suggested if Teresa (or other developers) think it's better.

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@teresajohnson teresajohnson left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm but 2 suggestions below

Comment thread llvm/include/llvm/Support/Caching.h Outdated
}

virtual Error commit(std::unique_ptr<MemoryBuffer> MemBuf) {
return Error::success();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably have the same error if Committed is already set? You could just invoke CachedFileStream::commit() to avoid code duplication.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping on this suggestion.

Comment thread llvm/lib/Support/Caching.cpp Outdated

virtual Error commit() override {
virtual Error commit(std::unique_ptr<MemoryBuffer> MemBuf) override {
if (Committed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like CacheStream::commit() you could do the checking and setting of Committed via a call to CachedFileStream::commit().

@romanova-ekaterina

romanova-ekaterina commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

A brand-new test has been added 4 days ago with the commit fe5f49f, and it failed on my PR but only on Linux and AArch64 (but strangely enough not on Windows).

I will set up Linux environment today and have a look why this test is failing.

I will also make changes that Teresa suggested. Thank you for reviewing!

@romanova-ekaterina
romanova-ekaterina force-pushed the dtlto-cache-rename-optimization-rebased branch from 8d3732b to 470c1f7 Compare July 5, 2026 21:27
@romanova-ekaterina

Copy link
Copy Markdown
Contributor Author

Teresa, with the latest commit, I have only updated the failing Linux test that has been added shortly after I posted a PR.

I will be away on vacation in the remote wilderness with zero internet access, but I will address your comment as soon as I return.

@romanova-ekaterina
romanova-ekaterina force-pushed the dtlto-cache-rename-optimization-rebased branch from 470c1f7 to bb07d97 Compare July 17, 2026 07:37
@romanova-ekaterina

romanova-ekaterina commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

@teresajohnson, is it OK to commit? I have already received a LGTM from you with a request to correct one minor thing (which I did), but after rebasing, I had to correct some of the newly written tests as well, since there were some failures.

I would feel safer, if the latest commits will get reviewed too. Thank you!

Comment thread llvm/include/llvm/Support/Caching.h Outdated
if (!Committed)
report_fatal_error("CachedFileStream was not committed.\n");
}
virtual AddBufferFn GetAddBuffer() { return AddBufferFn(); }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function and its override and AddBufferFn are no longer used (leftover from the prior approach?) and can be removed.

Comment thread llvm/include/llvm/Support/Caching.h Outdated
}

virtual Error commit(std::unique_ptr<MemoryBuffer> MemBuf) {
return Error::success();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping on this suggestion.

@bd1976bris

Copy link
Copy Markdown
Contributor

@romanova-ekaterina and @teresajohnson I think this is a candidate for cherry-picking to the release branch.

@romanova-ekaterina
romanova-ekaterina force-pushed the dtlto-cache-rename-optimization-rebased branch from f2e7438 to c362bdb Compare July 23, 2026 01:57
@Endilll
Endilll removed their request for review July 23, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lld:COFF lld:ELF lld llvm:support LTO Link time optimization (regular/full LTO or ThinLTO) platform:windows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants