HomeAbout UsContact Us

Link-Time Optimization (LTO) for Embedded Firmware Size Reduction

By Jithin Tom
Published in Embedded C/C++
August 08, 2026
2 min read
Link-Time Optimization (LTO) for Embedded Firmware Size Reduction

Table Of Contents

01
How LTO Changes the Optimization Model
02
GCC LTO: Practical Configuration
03
Clang/LLVM LTO: ThinLTO for Embedded
04
Measured Impact: Cortex-M Case Study
05
Common Pitfalls and Fixes
06
Incremental Build Strategy
07
Verification Checklist Before Shipping
08
Summary
09
Related Reading
10
References
11
Frequently Asked Questions

Link-Time Optimization (LTO) is one of the most effective levers for reducing firmware footprint on flash-constrained microcontrollers — yet many embedded teams leave it disabled due to build complexity concerns. This article examines how LTO works, quantifies its impact on Cortex-M firmware, and provides a practical migration path for GCC and Clang toolchains.


How LTO Changes the Optimization Model

Traditional compilation optimizes each translation unit (TU) in isolation. The compiler sees only the source file and its headers — cross-TU calls become opaque CALL instructions, inline candidates across files are missed, and dead code in other TUs remains linked.

+--------------------------+ +--------------------------+
| Without LTO | | With LTO |
| (Per-TU Optimization) | | (Whole-Program Opt) |
+--------------------------+ +--------------------------+
| file_a.c -> file_a.o | | file_a.c -> file_a.o |
| file_b.c -> file_b.o | | file_b.c -> file_b.o |
| file_c.c -> file_c.o | | file_c.c -> file_c.o |
| | | | | |
| v | | v |
| Link: opaque CALLs | | Link: GIMPLE/IR merge |
| No cross-TU inlining | | Global inlining |
| No cross-TU const prop | | Global const prop |
| Dead code per TU only | | Whole-program DCE |
+--------------------------+ +--------------------------+

LTO defers optimization to the link stage by embedding compiler intermediate representation (IR) — GIMPLE for GCC, LLVM IR for Clang — into object files. The linker invokes the compiler backend on the merged IR, enabling global analysis:

  1. Cross-TU inlining — Functions become inline candidates across file boundaries, including those with external linkage
  2. Interprocedural constant propagation — Constants flow through call chains spanning multiple TUs
  3. Whole-program dead code elimination (DCE) — Unreachable functions are removed globally, not just per-TU
  4. Function cloning and specialization — Hot paths cloned with constants baked in
  5. Identical code folding (ICF) — Duplicate functions merged via GCC’s -fipa-icf pass (also available as --icf=safe in the gold/lld linkers)

GCC LTO: Practical Configuration

Enabling LTO in CMake

# Enable LTO globally
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
# Or per-target (preferred for mixed projects)
target_link_options(firmware.elf PRIVATE -flto)
# Partitioning for large projects (GCC 10+)
target_link_options(firmware.elf PRIVATE -flto=auto)
# Parallel LTO with 4 jobs
target_link_options(firmware.elf PRIVATE -flto=4)
# Fat LTO (no partitioning, maximum optimization but slowest link)
target_link_options(firmware.elf PRIVATE -flto -flto-partition=none)

Critical Compiler Flags

# Compile with LTO-compatible optimization
-flto -O2 -fno-fat-lto-objects
# WHOPR partitioning (GCC 10+)
-flto=auto -fno-fat-lto-objects
# For maximum size reduction (trade compile time)
-flto -Os -fno-fat-lto-objects
# Note: -fno-fat-lto-objects saves compile time and disk space by emitting only IR.
# It requires linker plugin support, which is standard in modern ARM GCC toolchains.

Linker Plugin Integration

Ensure the linker plugin is active (automatic with gold/lld and GCC 8+):

# Verify plugin loads
arm-none-eabi-gcc -flto -v -o test.elf test.c 2>&1 | grep -i lto
# Should show: liblto_plugin.so loaded

Clang/LLVM LTO: ThinLTO for Embedded

Clang’s ThinLTO is purpose-built for large codebases and incremental builds:

# ThinLTO (recommended for embedded)
-flto=thin -O2
# ThinLTO with explicit caching for CI
-flto=thin -O2 -Wl,--thinlto-cache-dir=/path/to/cache
# Full LTO — maximum optimization, slowest link
-flto -O2

ThinLTO Cache in CI

# GitHub Actions example
- name: Setup ThinLTO cache
uses: actions/cache@v4
with:
path: ~/.thinlto_cache
key: thinlto-${{ hashFiles('**/*.c', '**/*.h', 'CMakeLists.txt') }}
restore-keys: thinlto-
- name: Build with ThinLTO cache
run: |
cmake -DCMAKE_C_FLAGS="-flto=thin -O2 -Wl,--thinlto-cache-dir=${HOME}/.thinlto_cache" ..
make -j$(nproc)

Measured Impact: Cortex-M Case Study

Test setup: STM32F407 (1 MB flash), FreeRTOS + application (~85 KB baseline), ARM GCC 12.2.

ConfigurationFlash UsedReductionLink TimeNotes
-O2 (no LTO)85.2 KB3.2 sBaseline
-O2 -flto74.1 KB13%18.4 sFat LTO
-O2 -flto=auto75.8 KB11%9.1 sPartitioned
-Os -flto69.8 KB18%19.2 sSize-optimized

Key observations:

  • 5-20% reduction is typical for firmware with modular structure
  • Partitioned LTO (-flto=auto) recovers ~50% of link time with minimal size penalty
  • -Os (size-optimized) + LTO compounds — but verify runtime performance
  • Link time scales with IR size; large C++ projects benefit most from ThinLTO

Common Pitfalls and Fixes

1. LTO Breaks weak Symbols

// Without LTO: weak symbol overridden by strong definition in another TU
// With LTO: both definitions visible, linker may pick wrong one
__attribute__((weak)) void Default_Handler(void) { while(1); }
// Fix: Avoid weak for critical symbols (use strong definitions), or enforce retention
void __attribute__((used)) Default_Handler(void) { while(1); }

2. Section Attributes Misplaced

// LTO may merge/collapse sections unexpectedly
__attribute__((section(".my_section"))) const uint8_t firmware_key[32] = {0};
// Fix: Force retention
__attribute__((section(".my_section"), used, retain)) // retain requires GCC 11+ / Binutils 2.36+
const uint8_t firmware_key[32] = {0};
// Linker script: KEEP(*(.my_section))

3. Debugging Optimized-Out Variables

# GCC: Preserve debug info for specific functions
-fno-lto -fno-inline-functions # Per-file opt-out
# Or in source:
__attribute__((optimize("O0"))) void critical_timing_function(void) { ... }

4. Startup Code and Interrupt Vectors

LTO can eliminate “unused” interrupt handlers referenced only in the vector table:

// Vector table references these — LTO may not see the reference
void HardFault_Handler(void);
// Fix: Mark as used to prevent LTO from removing it
void __attribute__((used)) HardFault_Handler(void);

5. Newlib-nano and LTO

# Specify nano.specs at link time WITH LTO
-specs=nano.specs -flto -Wl,--gc-sections
# Ensure libc LTO objects are available (arm-none-eabi-gcc 10+ includes them)

Incremental Build Strategy

LTO’s monolithic optimization defeats traditional incremental builds. Mitigations:

ApproachToolchainTrade-off
-flto=auto (WHOPR)GCC 10+Partitions IR, parallel link, significant parallel speedup
ThinLTO + cacheClang 9+Best incremental, near-fat quality, requires cache dir
-fno-lto on changed files onlyBothHybrid: LTO for stable files, fast rebuild for active dev
Unity build + LTOBothSingle TU, maximum optimization, no incrementality

Recommended workflow for active development:

# Debug builds: no LTO, fast iteration
set(CMAKE_C_FLAGS_DEBUG "-Og -g3 -fno-lto")
# Release builds: LTO enabled
set(CMAKE_C_FLAGS_RELEASE "-Os -flto=auto -fno-fat-lto-objects")

Verification Checklist Before Shipping

# 1. Size comparison
arm-none-eabi-size -A release/firmware.elf | grep -E "(text|data|bss)"
# 2. Symbol audit — verify no unexpected removals
arm-none-eabi-nm -S --size-sort release/firmware.elf | grep " T " | head -20
# 3. Call graph validation (requires LTO IR)
arm-none-eabi-gcc -flto -fdump-ipa-cgraph -o firmware.elf ...
# 4. Full test suite on target hardware
# - Functional tests
# - Timing-critical path verification
# - Stack usage analysis (LTO may increase stack in some paths)
# 5. Binary diff against non-LTO build
cmp -l release/firmware.elf debug/firmware.elf | wc -l

Summary

Link-Time Optimization delivers 5-20% flash reduction on typical Cortex-M firmware by enabling whole-program analysis across translation units. The cost is increased link time and debugging complexity — both manageable with modern tooling:

  • GCC: Use -flto=auto (partitioned WHOPR) for balanced link time, -fno-fat-lto-objects to save build disk space
  • Clang: Use -flto=thin with a persistent cache directory for CI-friendly incremental builds
  • Always validate with full hardware-in-the-loop testing — LTO can change timing, stack usage, and code layout
  • Mark critical symbols with __attribute__((used, retain)) (GCC 11+) to prevent aggressive DCE

For flash-constrained projects (STM32G0/G4 with 128-256 KB, Nordic nRF52 with 512 KB, RP2040 with 2 MB external), LTO is often the difference between fitting and requiring a larger MCU.


  • Reducing Embedded Firmware Size with Linker Garbage Collection--gc-sections and section layout strategies
  • Compiler Attributes and Pragmas in Embedded Cused, retain, section attributes for LTO safety
  • CMake for Embedded C: Cross-Compilation & Dependency Management — Toolchain integration patterns

References

  1. GCC Wiki, Link Time Optimization, https://gcc.gnu.org/wiki/LinkTimeOptimization
  2. LLVM Documentation, ThinLTO: Scalable and Incremental LTO, https://clang.llvm.org/docs/ThinLTO.html
  3. ARM, Compiler Reference Guide: Link-time optimization (-flto), https://developer.arm.com/documentation/100067/0618
  4. Khem Raj, Optimizing Embedded Linux Size with LTO, Embedded Linux Conference (2021).
  5. GCC Manual, Options for Code Generation Conventions: -fno-fat-lto-objects, https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html
  6. FreeRTOS, FreeRTOS FAQ: Memory Usage, Boot Times & Context Switch Times, https://www.freertos.org/Why-FreeRTOS/FAQs/Memory-usage-boot-times-context/

Frequently Asked Questions

What is Link-Time Optimization (LTO) and how does it differ from regular compiler optimization?

LTO enables whole-program optimization across translation unit boundaries at link time, allowing the compiler to inline functions, propagate constants, and eliminate dead code globally — unlike per-file optimization (-O2/-O3) which is limited to individual compilation units.

How much flash size reduction can LTO typically achieve in embedded firmware?

Typical reductions range from 5-20% for code size, often saving 4-24 KB of flash on Cortex-M projects. Results depend on codebase structure — projects with many unused abstractions, small functions, and cross-file calls benefit most.

What are the main risks when enabling LTO in a production embedded project?

Key risks include increased link time (often 2-10x), higher memory usage during linking, potential debugging difficulties (optimized-out variables, merged functions), and rare linker bugs that can produce incorrect code. Always validate with full test suites.

Does LTO work with incremental builds and CI pipelines?

LTO complicates incremental builds because the entire program is re-optimized at link time. ThinLTO (Clang) mitigates this natively with incremental caching, while GCC's WHOPR (-flto=auto) enables parallel linking to reduce the time penalty. For CI, cache the ThinLTO directory to speed up rebuilds.

What is the difference between fat LTO and ThinLTO?

Fat LTO (GCC default, Clang -flto) merges all IR into a single monolithic module — maximum optimization but slow, memory-intensive linking. ThinLTO (Clang -flto=thin) partitions IR for parallel linking. GCC provides a similar mechanism using WHOPR (-flto=auto) which partitions the call graph, enabling parallel link-time optimization.

Tags

embedded-cltolink-time-optimizationgccclangfirmware-sizeflash-optimization

Share


Previous Article
Optimizing Cortex-M Flash Wait States for Performance and Safety
Jithin Tom

Jithin Tom

A Closer Look at C/C++, RTOS, and Embedded Systems

Related Posts

Reducing Embedded Firmware Size with Linker Garbage Collection
Reducing Embedded Firmware Size with Linker Garbage Collection
July 26, 2026
3 min
© 2026, All Rights Reserved.
Powered By Netlyft

Quick Links

Advertise with usAbout UsContact Us

Social Media