Cross-Module Inlining with GCC -flto: When Whole-Program Optimization Pays Off
GCC link-time optimization lets the compiler inline functions across separate C files and remove unused code, but it trades link time and memory for smaller binaries.
01 Aug 2026, 18:56 UTC

The problem: optimization stops at the file boundary
You compile main.c and helpers.c separately with -O2. A small helper function in helpers.c is called frequently from main.c, but GCC cannot inline it because each translation unit—a single preprocessed source file compiled into one object file—is optimized in isolation. The call and return remain, the binary keeps code you never call, and you end up with a larger executable than the program logically requires.
The useful takeaway is that GCC’s link-time optimization (LTO) defers code generation until link time, so the compiler can see multiple translation units together. That enables cross-module inlining and whole-program dead-code elimination. The cost is longer linking, higher memory use during the link step, and slightly more complicated debugging.
What -flto actually changes in the build
Without LTO, GCC turns each source file into an object file containing machine code specific to that file. The linker then stitches those object files together without asking GCC to re-optimize across them.
With -flto, GCC instead stores an intermediate representation—historically called GIMPLE—inside each object file. At link time, a linker plugin reads that representation from all object files, runs optimization passes over the combined program graph, and emits final machine code. Because the compiler sees calls that cross file boundaries, it can inline a function defined in helpers.c directly into main.c, then remove the now-unneeded standalone copy if nothing else calls it.
Worked example: build and inspect an LTO binary
Run these commands in your project directory as an ordinary user. You need read access to the source files and write access to the output directory; no root privileges are required.
# Compile each translation unit with LTO enabled
gcc -c -flto -O2 main.c
gcc -c -flto -O2 helpers.c
# Link with LTO so GCC can optimize across both object files
gcc -flto -o myapp main.o helpers.o
Placeholders: replace myapp with your output binary name, and main.c / helpers.c with your actual source files.
To check the result, compare the binary size against a non-LTO build:
ls -lh myapp
objdump -d myapp | grep -A15 '<main>'
Expected checks: the LTO binary should be no larger than the non-LTO equivalent, and the disassembly of main may show helper code appearing directly inside main instead of a separate call instruction. Do not expect a specific byte savings; results depend on the code. If you see no difference, the program may simply not have cross-module optimization opportunities.
Risks: linking with LTO can consume substantially more RAM and take longer than a normal link. If one object file was compiled without -flto while another was compiled with it, the linker may warn or produce less optimization for that module; keep the flag consistent across all objects in a single link.
Trade-offs and limitations
| Factor | Without -flto | With -flto |
|---|---|---|
| Cross-module inlining | Not performed | Possible across object files |
| Dead-code elimination | Per translation unit | Whole program |
| Link time | Faster | Slower |
| Link memory use | Lower | Higher |
| Debuggability | Straightforward with -g | More complex; line information may be altered |
Debug information deserves care. Combining -g with -flto is supported, but the optimizer may reorder, inline, or remove code, so the debugger’s view of line numbers and variables can differ from a non-LTO build. If you need to debug a shipping binary, build a separate non-LTO copy with -g -O0 for investigation, or test that your debugger handles the LTO build before relying on it in production.
Binary compatibility is another practical concern. If different teams or CI jobs compile object files with mismatched LTO flags, link behavior can vary. Treat -flto as a project-wide build decision, not a per-file tweak.
Practical closing: decide per release, not per file
Enable -flto when binary size matters, when you have small hot functions spread across files, or when you want the compiler to remove code that no translation unit can see is unused. Skip it for fast iterative debug builds, for memory-constrained build machines, or when you need the most predictable debugging experience.
Verify the decision empirically: build the same program with and without -flto, compare ls -lh output, inspect objdump -d for inlined call sites, and measure runtime on your actual workload. If the binary is smaller and the disassembly shows inlined cross-module calls, LTO is earning its cost. If not, drop the flag and rebuild—the change is only in your build commands, so reverting is as simple as recompiling without it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.