How Does Link-Time Optimization Inline Functions?

Link-time optimization is a compiler technique that optimizes a program across multiple source files during the linking stage. In traditional compilation, each source file is compiled independently into machine code, which prevents the compiler from seeing function definitions in other files. Link-time optimization solves this by deferring final code generation until all compilation units are combined, allowing the optimizer to inline functions and remove unused code across the entire program.

The Limit of Separate Compilation

In classical computing, large programs are split into separate compilation units, such as individual C or C++ source files. During standard compilation, the compiler translates each file into an object file containing machine code and a symbol table. Because the compiler only sees one file at a time, a call to a function defined in another file appears as an opaque external reference.

Without visibility into the target function's body, the compiler cannot inline it at the call site. It also cannot prove whether a function or global variable is actually used elsewhere in the program, so it must conservatively keep exported symbols in the final binary.

How Intermediate Representation Enables LTO

When link-time optimization is enabled, the compiler changes what it stores in each object file. Instead of emitting final machine code, it writes an intermediate representation (IR), such as LLVM bitcode or GCC GIMPLE, into the object file.

During the linking phase, the linker invokes a compiler plugin or an integrated LTO driver. This driver reads the IR from all object files and merges them into a single, unified module. With the entire program's IR available in one place, the optimizer can perform whole-program analysis before generating the final machine code.

Cross-Module Function Inlining

Once the IR modules are merged, the optimizer builds a complete call graph of the program. Every function call now has a visible target body, regardless of which source file originally contained it.

The inliner evaluates call sites using cost heuristics, such as function size, call frequency, and register pressure. If inlining a function from another compilation unit is beneficial, the optimizer replaces the call instruction with a copy of the function's IR body. This eliminates call overhead and exposes new optimization opportunities, such as constant propagation and loop unrolling, across the former file boundary.

Whole-Program Dead Code Elimination

With a complete view of the program, the linker and optimizer can accurately determine which functions and variables are reachable from the program's entry point, such as main, or from exported library symbols.

Any function or global variable that is never called or referenced is marked as unreachable. Dead code elimination (DCE) then removes these unused definitions from the unified IR. Furthermore, if inlining a function removes its last remaining call site, the original function definition itself becomes dead code and is stripped from the final binary, reducing executable size and improving instruction cache locality.

Scaling LTO with ThinLTO

Full link-time optimization can be slow and memory-intensive for very large codebases because it merges all IR into a single process. To address this, modern compilers offer incremental approaches like ThinLTO.

ThinLTO stores a compact summary of each module's functions and call graph alongside its IR. During linking, the compiler performs fast, whole-program analysis using only these summaries to decide which functions to inline and which symbols are dead. It then imports only the necessary function bodies into each module in parallel, achieving most of the performance and size benefits of full LTO at a fraction of the build time.