Linkers, Loaders & Object Formats

link-time optimization

/ LTO, said by its letters /

Normally the compiler optimizes one source file at a time, then the linker just glues the finished pieces together without looking inside them. This means an optimization that spans two files — say, inlining a tiny function from a.c into a hot loop in b.c, or deleting a function nobody actually calls — simply cannot happen, because at compile time neither file can see the other. Link-time optimization removes that wall: it defers the heavy optimization until link time, when the whole program is finally visible at once.

The mechanism is a clever shift in what a .o file contains. With LTO enabled (compile with -flto), the compiler does not emit final machine code into each .o; instead it stores its internal intermediate representation — a serialized form of the program's meaning. At link time, the LTO-aware linker (or a plugin) gathers all those IR chunks, treats them as one big program, runs the optimizer across the whole thing, and only THEN generates machine code. Now cross-file inlining, whole-program dead-code elimination, and better constant propagation all become possible. There are two flavors. Full (or 'fat') LTO loads everything into memory and optimizes it as a single monolithic unit — maximum optimization, but slow and memory-hungry on large programs. Thin LTO does a cheap global analysis to decide what to import where, then optimizes each module in parallel with just the relevant imports — nearly as good in practice, far faster, and parallelizable, which is why it is preferred for big codebases.

It matters because LTO often gives a real speed and size win for free at the cost of build time, especially for code split across many small functions and files. The honest caveats: builds get slower and use more memory, debugging information and build reproducibility can suffer, and because the optimizer now sees across the whole program, latent undefined behavior that happened to be harmless within one file can suddenly bite when the optimizer exploits it across the new, wider view. LTO does not make incorrect code correct — it can make existing bugs more visible.

$ gcc -O2 -flto -c a.c b.c # .o files hold IR, not final code $ gcc -O2 -flto a.o b.o -o app # whole-program optimize at link # thin LTO (clang): -flto=thin -> parallel, fast; full: -flto=full

With -flto the .o files store intermediate representation; the optimizer runs across the whole program at link time, enabling cross-file inlining.

LTO does not fix bugs; by widening the optimizer's view it can EXPOSE latent undefined behavior that was previously masked within a single file. Thin LTO trades a little peak optimization for much faster, parallel builds and is usually the right default for large projects.

Also called
LTOwhole-program optimizationthin LTO全程式最佳化