ThinLTO: Turning Separate Compilation into Whole‑Program Optimisation
When a hot helper lives in another source file, traditional separate compilation keeps it as a call. LLVM’s ThinLTO lets you inline it at link time without the compile‑time cost of headers. This blog walks through the problem, the LTO mechanism, a concrete test case, and the practical trade‑offs.
08 Feb 2026, 02:45 UTC

The Missing Inlining Problem
In a typical C project you write main.c and mathutil.c. The compiler sees main.c as a separate translation unit (TU). If mathutil.c defines a small helper like clamp(), the compiler has no knowledge of it when optimising main.c. As a result, the hot loop that calls clamp() remains a real function call in the final binary, even though the helper is tiny and could be inlined for a performance win.
Traditional workarounds include putting the helper in a header and inlining it manually, or duplicating the code. Both add compile‑time overhead or risk code bloat. LLVM offers a cleaner solution: move the optimisation to link time with Link‑Time Optimization (LTO).
Link‑Time Optimisation in LLVM
When you compile with -flto, Clang does not emit machine code. Instead, each object file contains LLVM bitcode – an intermediate representation that is still optimisable. A special linker plugin (provided by lld or the LLVM plugin for GNU ld/gold) collects all the bitcode, re‑enters the LLVM optimiser, and performs whole‑program analyses such as cross‑module inlining, dead‑code elimination, and more.
Full LTO merges the entire bitcode graph before optimisation. For very large codebases this can use a lot of memory and take a long time. ThinLTO addresses this with a summary‑and‑import model: each TU emits a compact summary that lists its functions and types. At link time the optimiser only pulls in the bitcode for functions that are actually needed for cross‑module optimisation, keeping the memory footprint low and allowing parallel work.
ThinLTO: The Scalable Variant
- Compile‑time cost: almost identical to normal compilation – the compiler just emits a small summary file (
.tbcor.thinlto.bc). - Link‑time cost: higher than a normal link, but parallelisable and limited to the functions that matter.
- Result: cross‑module inlining, function‑level dead code elimination, and other whole‑program optimisations that would otherwise be impossible.
- Compatibility: works with
lldor GNU ld/gold + LLVM plugin. CMake exposes it viaCMAKE_INTERPROCEDURAL_OPTIMIZATION.
Hands‑On Example
Let’s demonstrate the difference with a two‑file project. Create the following files:
/* mathutil.c */
static inline int clamp(int x, int lo, int hi) {
if (x < lo) return lo;
if (x > hi) return hi;
return x;
}
/* main.c */
#include <stdio.h>
int main(void) {
int sum = 0;
for (int i = 0; i < 1000000; ++i) {
sum += clamp(i, 0, 500000);
}
printf("%d\n", sum);
return 0;
}
Compile without LTO:
clang -O2 main.c mathutil.c -o no_lto
Compile with ThinLTO:
clang -O2 -flto=thin main.c mathutil.c -o thin_lto
Both builds require an LTO‑aware linker. On most modern systems lld is the default, but you can enforce it with -fuse-ld=lld.
Verify the optimisation by inspecting the disassembly. Run:
llvm-objdump -d no_lto | grep -n "clamp"
llvm-objdump -d thin_lto | grep -n "clamp"
In the no_lto binary you should see a call instruction to clamp. In the thin_lto binary the helper is inlined – the call disappears and the loop body contains the clamping logic directly. The exact assembly will vary with your target architecture and compiler version, but the presence or absence of the call is the key check.
Trade‑Offs and Practical Considerations
- Build time: ThinLTO adds a few seconds to the link step and can use up to 1–2 GB of RAM on large projects. Measure with
time -v clang. - Toolchain version: Bitcode is not a stable interchange format. Mixing objects compiled with different clang/LLVM releases under LTO can fail. Keep your CI pipeline pinned to a single toolchain.
- Static libraries: If you ship
.aarchives, build them with-ffat-lto-objects(or-ffat-lto-objects=thin) so they work both with and without LTO. - Debugging: LTO can make debugging harder because the binary no longer matches the source line layout. Use
-galong with-flto=thinif you need debugging. - Workload dependence: The benefit appears when hot code crosses TU boundaries. For small projects or when the hot path stays in one file, the gain may be negligible.
Actionable Takeaway
If you have a moderately large codebase and want to squeeze out a few percent of performance, try ThinLTO. Add the flag to your build system:
# CMake
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
# or
add_compile_options(-flto=thin)
Ensure the linker is LTO‑aware; on Linux lld is usually the default, but you can enforce it with -fuse-ld=lld. Re‑run the example above to confirm that the helper is inlined. If you notice a significant increase in link time or memory, consider tuning -flto=thin options or disabling LTO for modules that do not benefit.
In short: separate compilation keeps build times low but hides cross‑module optimisation opportunities. ThinLTO gives you the best of both worlds – a small compile‑time cost and scalable link‑time optimisation that can inline helpers across files and remove dead code. Try it, measure it, and adjust your build accordingly.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.