Mastering LTO in Keil5: Shrink Code Size and Boost Performance
This in‑depth guide explains how to enable and fine‑tune Link‑Time Optimization (LTO) in Keil MDK5 using ArmClang, showing real‑world examples, benchmark data and step‑by‑step instructions that dramatically reduce flash footprint and improve execution speed for embedded systems.
Why LTO matters for modern embedded projects
Smart‑home devices are becoming increasingly complex, and maintaining stable wireless connections on the MT7697 chip requires more than just good hardware. The hidden performance booster is the compiler’s Link‑Time Optimization (LTO) feature, which can dramatically reduce code size and improve execution efficiency.
From local to global: how LTO reshapes optimization
In a traditional build each source file is compiled to a .o file, limiting optimizations to the file scope. For example, an inline function defined in file1.c cannot be inlined into file2.c because the linker sees no intermediate representation (IR) to connect them:
// file1.c
static inline int compute(int a) { return a * 2 + 1; }
// file2.c
int api_call(int x) { return compute(x); } // expected inline failsLTO preserves the LLVM IR (usually in a .llvmbc section) inside each object file. During the link stage the linker loads all IR, analyses the whole program and can perform cross‑file inlining, dead‑code elimination and other global optimisations.
Real‑world impact on flash and speed
Measurements on a Cortex‑M4 project show that enabling LTO reduces the .text section by 15‑30 % on average and up to 48 % in extreme cases. The smaller code also improves instruction‑cache hit rates, yielding faster overall execution.
Step‑by‑step guide to activate LTO in Keil MDK5
1. Use the correct compiler : ARM Compiler 5 (ARMCC) does not support LTO. Switch to ARM Compiler 6 (ArmClang) via
Project → Options for Target → Target → Compiler Version 6. Verify with armclang --version (e.g., clang version 6.18).
2. Choose the optimisation level : In the C/C++ tab set “Compile Time” to “Optimize for Size” or “Optimize for Time” and select Optimization Level 3 ( -O3).
3. Enable the LTO switch : Tick “Enable Link‑Time Optimization”. Keil adds -flto -fno-use-linker-plugin to the compile command.
4. Configure the linker : In the Linker tab enable “Use Memory Layout from Target Dialog” and add --lto to the Misc Controls field. Also enable “Generate Cross Reference List” and “Create Object Browser Information” for better analysis.
The resulting compile command looks like:
armclang --target=arm-arm-none-eabi -mcpu=cortex-m4 -O3 -ffunction-sections -fdata-sections -flto -fno-use-linker-plugin …The full build flow is:
[Source Files]
↓
[armclang -flto -O3]
↓
[.o with .llvmbc]
↓
[armlink --lto]
↓
[Final .axf with Global Optimisation]Common pitfalls and how to fix them
Using ARMCC instead of ArmClang – switch compiler version.
Stale object files – delete the Objects/ and Listings/ folders and rebuild.
Missing LTO flag – ensure -flto appears in the build log.
Typical linker error
Error: L6242E: Cannot link object file xxx.o as it contains debug information in an unsupported format.indicates that a library was built with ARMCC and lacks Bitcode. Solutions:
Re‑compile the library with ArmClang -flto (ideal but costly).
Link the non‑LTO library separately using --allow-shrink-warnings --ignore-lto-warnings.
Exclude the problematic file from LTO with -fno-lto.
Detecting LTO support in object files
Run fromelf -c your_object.o | grep llvmbc. If there is output, the object contains Bitcode and can participate in LTO.
Empirical data: flash savings
Section | Without LTO (Bytes) | With LTO (Bytes) | Reduction | Compression
--------------------------------------------------------------------------
.text | 183488 | 94208 | -89280 | 48.7%
.constdata| 12544 | 6144 | -6400 | 51.0%
.data | 4096 | 4096 | 0 | 0%
.bss | 16384 | 15872 | -512 | 3.1%
--------------------------------------------------------------------------
Total | 216512 | 120320 | -96192 | 44.4%The .data section cannot be optimised because it holds initialized globals, while .bss shrinks slightly as unused static buffers are removed.
C++ template deduplication
When multiple instantiations of the same template generate identical code, LTO merges them. Example:
template<int N> class VectorProcessor {
public:
float process(const float* input) {
float sum = 0.0f;
for (int i = 0; i < N; ++i) sum += input[i] * 0.5f;
return sum / N;
}
};
VectorProcessor<3> vp3;
VectorProcessor<4> vp4;
VectorProcessor<3> another_vp3; // same specializationWithout LTO each VectorProcessor<3>::process is emitted twice. With LTO the linker keeps only one copy, saving about 76 bytes.
Performance boost beyond size reduction
Using the Cortex‑M DWT cycle counter, a low‑pass filter benchmark shows a drop from 142 cycles per call (no LTO) to 98 cycles (LTO), a 30.9 % speed‑up. The gains come from constant folding of the filter coefficient, reordered floating‑point operations, register‑resident state variables and loop unrolling.
Interrupt latency also improves: a four‑level ISR chain that costs ~36 cycles without LTO shrinks to ~18 cycles after full inlining, a 50 % reduction. Real measurements on a 168 MHz core confirm average latency dropping from 2.34 µs to 1.52 µs (35 % faster).
Trade‑offs: build time and debugging
Full LTO can increase compile‑link time dramatically (e.g., from 12.4 s to 47.8 s, a 285 % rise). Mitigation strategies include enabling LTO only for Release builds, using distributed build caches (IceCC), or pre‑building stable modules as LTO libraries.
Debugging becomes harder because many functions disappear or are renamed. Recommended practices:
Develop with -O2 and no LTO; enable LTO only for final releases.
Keep --debug and --split_sections in Release builds.
Mark critical debug functions with __attribute__((used)) to prevent removal.
Stack‑size analysis also suffers because LTO changes call graphs. Use runtime stack‑checking (MPU guard pages), insert explicit __current_sp() checks, or rely on profiling tools to capture actual stack usage.
Advanced LTO policies for large projects
Module Type | Example Components | LTO Recommendation
--------------------------------------------------------------------------
Real‑time control | PID, PWM ISR | -O3 + LTO + aggressive inlining
Peripheral drivers | UART, ADC drivers | -O2, disable cross‑file inlining
Protocol stacks | Modbus, JSON parser | Full LTO, prioritize size reduction
Third‑party libs | FreeRTOS, CMSIS‑DSP | Build as non‑LTO objectsCritical functions can be forced to stay inline with __attribute__((always_inline)), while safety‑critical code (e.g., flash write) can be compiled with a pragma that forces -O2 and disables LTO.
Future outlook: AI‑assisted LTO
ArmClang already supports ThinLTO and FullLTO. The next step may be AI‑driven optimisation hints that analyse call graphs, memory patterns and historic performance data to automatically suggest which modules should enable LTO, turning the optimisation process into a continuous, data‑driven workflow.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
IoT Full-Stack Technology
Dedicated to sharing IoT cloud services, embedded systems, and mobile client technology, with no spam ads.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
