ThinLTO: Scaling Cross‑Module Optimizations for Release Builds
Learn how LLVM's ThinLTO balances performance, memory, and build time for large codebases. A concrete example, trade‑offs, and step‑by‑step guidance help you decide whether ThinLTO is right for your release workflow.
29 Apr 2026, 21:37 UTC

Problem & Thesis
When a codebase grows to hundreds of thousands of lines, the classic "compile‑then‑link" pipeline often becomes a bottleneck. Full Link‑Time Optimization (LTO) can squeeze out extra speed and size, but it usually crashes the link step with excessive memory usage or takes minutes to finish. ThinLTO was introduced to keep the benefits of cross‑module optimization while keeping link time, memory, and disk usage in check.
In this post we will:
- Explain the thin‑LTO workflow and how it differs from full LTO.
- Show a concrete build example and compare key metrics.
- Highlight the trade‑offs and limitations you should be aware of.
- Give you a clear, actionable recipe for adding ThinLTO to your CI or release pipeline.
How ThinLTO Works
ThinLTO keeps the core idea of LTO—optimizing across translation units—but it does so in a more lightweight, distributed fashion:
- Summary Generation: During compilation each module emits a compact “summary” (a small bitcode file) that describes the functions it defines and the functions it calls.
- Import Phase: At link time the linker reads all summaries, builds a call‑graph, and decides which function bodies actually need to be pulled in. Only those bodies are imported from the original object files.
- Code Generation: The selected bodies are re‑compiled (or optimized) in the context of the whole program, enabling inlining, devirtualization, and other cross‑module optimizations.
- Link Time Optimization: The final binary is produced with the optimized code.
Because the linker only pulls in the needed bodies, the memory footprint stays roughly proportional to the size of the program—unlike full LTO, which loads the entire module graph into memory. The process also scales well with parallelism: the import and code‑generation phases can be distributed across workers, and the LLVM LTO API exposes a "distributed backend" that can be invoked with lto2 -backend=distributed.
Concrete Example: Measuring ThinLTO vs. Non‑LTO
Below is a minimal reproducible example. Replace <path/to/src> with the directory containing the source files. The example uses two small modules that call each other; the real value comes when you scale this up to a full application.
# Compile each module with ThinLTO summaries
clang++ -O2 -flto=thin -c <path/to/src>/module1.cpp -o module1.o
clang++ -O2 -flto=thin -c <path/to/src>/module2.cpp -o module2.o
# Link with ThinLTO
time -v clang++ -O2 -flto=thin module1.o module2.o -o app_thin
# Compile the same modules without LTO
clang++ -O2 -c <path/to/src>/module1.cpp -o module1_nonlto.o
clang++ -O2 -c <path/to/src>/module2.cpp -o module2_nonlto.o
# Link without LTO
time -v clang++ -O2 module1_nonlto.o module2_nonlto.o -o app_nonlto
# Compare binary sizes
ls -lh app_thin app_nonlto
Use time -v to capture real, user, and memory usage. The ls -lh command shows the final binary sizes. In a typical setup you should observe:
- Link time for
app_thinis longer than the non‑LTO build but significantly shorter than full LTO. - Peak memory usage during linking is moderate, often under a few hundred megabytes, even for larger projects.
- Binary size of
app_thinis usually similar to or slightly smaller than the non‑LTO binary, thanks to inlining and devirtualization.
Because we are not running the commands here, you should perform the measurement in your environment to validate the numbers. The important part is the relative comparison: ThinLTO should give you a noticeable size or speed win without the extreme resource cost of full LTO.
Trade‑offs & Limitations
| Aspect | ThinLTO | Full LTO |
|---|---|---|
| Memory (link step) | Moderate (scales with code size) | High (entire module graph in RAM) |
| Disk usage (object files) | Higher (each object contains a summary) | Lower (standard object files) |
| Optimization depth | Near‑full LTO, but some very global passes may be skipped | Full optimization across all modules |
| Debugging | Source locations may be less precise for inlined code | Same as ThinLTO |
| Mixed object files | Only ThinLTO objects participate in cross‑module optimizations | All objects participate if compiled with LTO |
| Incremental builds | Supported via -flto-incremental and distributed backends | Supported but can be slower due to larger state |
Key caveats to remember:
- Object files grow because each contains a summary. If you cache objects in a CI, the cache size will increase.
- Link time, while improved, is still longer than a non‑LTO build. Plan your CI timing accordingly.
- Debug information can be fuzzy for heavily optimized code; consider disabling inlining for debug builds.
- ThinLTO support and behavior vary across LLVM versions and targets. Verify the exact release you use (LLVM 15+ offers the most stable ThinLTO API).
Adopting ThinLTO in Your Workflow
- Check LLVM version: Ensure your toolchain supports ThinLTO. Run
clang++ --versionand look for a compiler built with LLVM 15 or newer. - Enable ThinLTO: Add
-flto=thinto your compile and link flags. For example:CXXFLAGS="-O2 -flto=thin" LDFLAGS="-flto=thin" - Profile‑Guided Optimization (PGO) (optional): ThinLTO plays nicely with PGO. Generate profiles first:
clang++ -O2 -flto=thin -fprofile-generate <src> -o app_profile ./app_profile # run workload to collect profiles clang++ -O2 -flto=thin -fprofile-use <src> -o app_pgo - Incremental & Distributed Builds: For large projects, use
-flto-incrementaland the distributed backend:clang++ -flto=thin -flto-incremental -flto-backend=distributed <src> - Measure & Iterate: Run the example above in your CI, capture link times and memory. If link time is still a bottleneck, consider moving to a multi‑stage pipeline: compile with ThinLTO, then run a separate optimization pass with
opt -passes="thinlto"before final linking. - Watch for Legacy Pass Manager Deprecation: Recent LLVM releases default to the new pass manager. ThinLTO is fully supported there; if you see warnings about the legacy pass manager, upgrade your toolchain or add
-mllvm -passes="thinlto"to enforce the new manager.
Closing Thoughts
ThinLTO gives you most of the cross‑module optimization power of full LTO while keeping resource usage manageable. It is especially valuable for large codebases or CI pipelines where link time and memory are real constraints. By following the steps above—checking your LLVM version, enabling ThinLTO, optionally combining with PGO, and measuring the impact—you can make an informed decision and bring tangible performance gains to your release builds.
Next time you hit a slow link or a memory‑heavy build, consider ThinLTO as a middle ground. It’s a practical, production‑ready feature of LLVM that can help you ship faster, leaner binaries without the heavy engineering cost of full LTO.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.