Link-Time Optimization with GCC: Shaving Size Across Translation Units
GCC Link-Time Optimization moves optimization to the link step using GIMPLE bytecode, enabling cross-module inlining and dead-code removal. Learn how to enable -flto safely, verify it, and weigh build-time costs.
16 Sept 2025, 21:52 UTC

You have a C or C++ program that builds cleanly with -O2, yet the binary still contains unused helper functions and calls that could not be inlined across .c files. The compiler sees each translation unit in isolation, so whole-program decisions are deferred. GCC Link-Time Optimization, -flto, moves optimization to the link step so the compiler can see all units at once.
What -flto actually changes
Normally GCC compiles a source file to machine code and discards intermediate representations. With -flto, the front end lowers the code to GIMPLE, GCC's internal intermediate representation, and writes that bytecode into special sections of the object file. The linker then collects those sections and runs the optimizer again across the whole program before emitting machine code.
Key points for engineers:
- GIMPLE is stored, not machine code, in .gnu.lto_* sections. You can verify presence with readelf.
- Optimizations are performed by the linker driver, usually with plugin support. The same -O level you used at compile time applies at link time.
- Both compile and link invocations need -flto. Using it only at one stage produces mismatched objects.
Enabling LTO safely in a build
Enable LTO on release builds where link time is acceptable. Keep debug builds without it for faster iteration.
Typical compile and link commands from a terminal in the source directory, no elevated permissions required:
gcc -O2 -flto -c foo.c -o foo.o
gcc -O2 -flto foo.o bar.o -o programFor smaller object files, -fno-fat-lto-objects drops the native machine-code copy that GCC otherwise keeps for toolchains that do not understand LTO. This reduces disk use at the cost of requiring an LTO-aware linker for those objects.
Practical checks after a build:
- Confirm LTO sections exist:
readelf -S foo.o | grep gnu.lto. Presence of .gnu.lto_* sections indicates GIMPLE bytecode is stored. - Compare binary size:
size programorls -lh programagainst a non-LTO build. - Run your own benchmark harness to observe runtime behavior under your workload. Do not rely on micro-benchmarks alone.
Risks to note before adoption: LTO increases peak memory during linking because the whole program's GIMPLE is loaded. Link times grow, which can hurt incremental development. Debug information is preserved with -g, but aggressive inlining and dead-code removal can make source-level stepping less predictable.
Worked comparison
Consider two small modules: math.c defines a helper used only in main.c, and main.c references a function from util.c that is never called in a particular build configuration.
Without LTO:
gcc -O2 -c math.c util.c main.c
gcc -O2 math.o util.o main.o -o app_no_ltoWith LTO:
gcc -O2 -flto -c math.c util.c main.c
gcc -O2 -flto math.o util.o main.o -o app_ltoThe LTO build can inline across translation units and eliminate the unused util function globally. The effect is visible as a change in binary size and symbol visibility. You can inspect which symbols remain with nm -g app_lto and compare to the non-LTO binary.
Trade-offs and limitations
LTO exposes One Definition Rule violations that separate compilation hides. If the same header defines a non-inline function differently in two translation units, LTO may produce link errors or silently choose one definition. Ensure headers are consistent and avoid duplicate definitions.
Debuggability is reduced. With -g -flto, line tables are kept, but inlining decisions made at link time can make stepping jump between files. Some debuggers struggle with optimized LTO binaries.
Build systems that rely on fast incremental links will feel the slowdown. A common pattern is to use LTO only for release or nightly builds, and to keep development builds with -O2 without -flto.
Actionable next step: add -flto to your release CFLAGS and LDFLAGS, keep -O2 or -O3, verify LTO sections with readelf, and measure size and runtime on representative workloads. If link memory becomes a bottleneck, consider splitting the program into link units or using -ffat-lto-objects for compatibility.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.