Use GCC link-time optimization to trim release binaries
GCC's -flto lets the linker optimize across translation units, often shrinking release binaries at the cost of longer link times.
24 Aug 2026, 16:01 UTC

The problem: optimization stops at the file boundary
You compile with -O2, rebuild, and the binary is still larger than expected. Functions in helper.c that nothing calls remain in the final image, and a small utility in main.c never gets inlined across the translation-unit boundary. The compiler optimized each .c file in isolation, then the linker stitched object files together without seeing the whole program.
GCC's link-time optimization (LTO) changes that boundary. With -flto, GCC writes an intermediate representation called GIMPLE into object files instead of only machine code. At link time, the linker reads that representation and optimizes across the entire program: inlining functions defined in other files, eliminating code that turns out to be unreachable, and propagating constants through module boundaries.
Takeaway: use -flto for release builds when binary size or cross-module performance matters, but expect slower links and higher memory use. Keep debug builds without it.
How LTO changes the build
Without LTO, gcc -O2 -c main.c emits machine code specific to that file. The compiler cannot inline a function defined in helper.c into main.c because it does not see helper.c's body at compile time.
With -flto, the object file still contains a symbol table so the linker can resolve references, but it also carries GIMPLE, GCC's middle-end intermediate representation. At link time, GCC merges the GIMPLE streams from all objects and runs optimization passes over the combined program before emitting final machine code. This is whole-program optimization: calls between files that were previously opaque become visible to the optimizer.
Worked example: compare two builds
Run these commands in a terminal from a directory you own. No root permissions are required.
cat > main.c <<'EOF'
#include <stdio.h>
static int compute(int x) { return x * 2 + 1; }
int main(void) { printf("%d\n", compute(21)); return 0; }
EOF
cat > helper.c <<'EOF'
int unused_helper(void) { return 999; }
EOF
Build without LTO:
gcc -O2 -o app_no_lto main.c helper.c
Build with LTO:
gcc -O2 -flto -o app_lto main.c helper.c
Compare the results:
| Check | Command | What to confirm |
|---|---|---|
| Binary size | size app_no_lto app_lto | LTO build is not larger; often smaller |
| Section headers | objdump -h app_lto | .text section is present and not unexpectedly grown |
| Runtime behavior | ./app_no_lto and ./app_lto | Both print the same output |
Do not copy expected output from another build; your results depend on the source, GCC version, and system. Treat any size change as a signal to measure, not a guaranteed improvement.
Trade-offs and debugging
LTO increases link time and memory consumption because the linker processes all intermediate representation. On large projects, a serial link job can consume substantial RAM and may be terminated by an out-of-memory killer.
Avoid mixing -flto objects with objects compiled without it unless you know your toolchain supports it. Inconsistent flags can produce undefined references or subtle behavior changes. In C++, LTO can surface One Definition Rule violations that separate compilation previously tolerated, because the linker now sees duplicate definitions across translation units.
Debugging is harder with LTO. Aggressive inlining and dead-code elimination can obscure stack traces. For debuggable artifacts, compile with -g but omit -flto, or build a separate debug binary.
Practical verification and closing
After building with -flto, run your test suite against both the LTO and non-LTO artifacts. Compare size output and confirm runtime behavior matches. If the size reduction is meaningful and link times are acceptable, ship the LTO binary; otherwise, -O2 alone may be the better default for your project.
Treat -flto as a release-build flag. Add it to your release CFLAGS or CXXFLAGS, measure on your own codebase, and keep debug builds without it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.