Using GCC Link-Time Optimization to Shrink Binaries and Speed Up Code
Learn how GCC's Link-Time Optimization lets the compiler inline functions and eliminate dead code across translation units, and see the trade‑offs in build time, binary size, and debugging.
20 Jan 2026, 17:58 UTC

When per‑file optimization leaves performance on the table
Most developers compile each translation unit with -O2 (or -O3) and rely on the linker to stitch the resulting object files together. This model works well for incremental builds, but it prevents the compiler from seeing the whole program. Opportunities such as inlining a small helper function that lives in another file, eliminating dead code that is only referenced from a single translation unit, or propagating constants across file boundaries are missed.
How GCC’s Link-Time Optimization (LTO) bridges the gap
When -flto is added to the compile line, GCC does not emit final machine code in the object file. Instead, it stores the intermediate GIMPLE representation. During the link step, the linker (or the GCC linker plugin) reads all GIMPLE units, merges them, and runs optimization passes as if the whole program were a single translation unit. The final code generation then produces the binary.
To make the process seamless with the GNU linker, the companion flag -fuse-linker-plugin tells GCC to invoke the LTO plugin; without it, you would need to use gcc -flto -o prog *.o and rely on the linker’s built‑in support.
Worked example: enabling LTO in a simple Makefile
Consider a tiny project with two source files:
# main.c
#include "helper.h"
int main() {
return helper(42);
}
# helper.c
#include "helper.h"
int helper(int x) {
return x * 2 + 3;
}
# helper.h
#ifndef HELPER_H
#define HELPER_H
int helper(int x);
#endif
A basic Makefile without LTO:
CFLAGS = -O2
OBJS = main.o helper.o
prog: $(OBJS)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) prog
To switch on LTO, adjust the compile and link flags:
CFLAGS = -O2 -flto -fuse-linker-plugin
LDFLAGS = -flto
prog: $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^ $(CFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
Running make clean && make now produces object files that contain GIMPLE bytecode. The link step performs the optimization pass and emits the final executable.
Verifying the effect
You can inspect the generated assembly to see cross‑function inlining that would not appear without LTO:
gcc -O2 -flto -fuse-linker-plugin -S main.c helper.c -o prog.sOpen
prog.sand look for the body ofhelperbeing inserted directly intomain’smainfunction. Without-flto, you would see a call instruction tohelper.To measure the trade‑offs, time a full build and compare binary sizes:
# without LTO make clean time make size prog # with LTO (adjust Makefile as above) make clean time make size progTypical outcomes for CPU‑intensive code are a 5%‑20% reduction in runtime and a comparable shrink in binary size, while link time and peak memory usage increase noticeably.
Limitations and practical checks
- Link‑time cost. Large projects may see the link step double or triple in duration. Monitor with
time makeand consider limiting parallelism with-flto=jobserveror reducing memory pressure via-flto=thin. - ODR violations. If an inline function or template is defined differently in two translation units, LTO will expose the mismatch as a link‑time error. The fix is to ensure a single definition (e.g., move the definition to a header guarded by
inlineor use-fno-inlinefor troubleshooting). - Debugging. Even with
-g, the optimized code can be hard to step through. For debugging sessions, build a separate variant without-fltoor use-flto -g -O0to preserve a clearer mapping.
Actionable closing
Enable LTO when you have a release build that benefits from whole‑program analysis and you can tolerate the extra link‑time and memory overhead. Start by adding -flto -fuse-linker-plugin to both CFLAGS and LDFLAGS, verify the assembly for expected inlining, and compare build timing and binary size against a baseline. If the gains match your performance goals and the link‑time increase is acceptable, make LTO a permanent part of your release profile; otherwise, keep it disabled for debug or incremental builds.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.