The Arm Compiler optimizes your code for small code size and high performance. The trade-off between binary footprint and execution speed is one of the most consequential settings on a Cortex-M project, and the right answer depends on which build you’re producing.
Arm Compiler 6 optimization levels at a glance
Arm Compiler 6 (armclang) is the LLVM-based toolchain that ships with Keil MDK and Arm Development Studio. It exposes six optimization levels, each tuned for a different point on the size/speed/debuggability curve. Pick the level that matches the job of the build, not a vague preference for “faster code.”
| Level | When to use | Trade-off |
|---|---|---|
-O0 | Debug builds, single-step trace, breakpoint-heavy bring-up | Largest binary, slowest runtime, full source-line fidelity |
-O1 | Light optimization with most debug info preserved | Some local variables become unavailable in the debugger |
-O2 | Default for release | Best general-purpose balance of speed and size |
-O3 | Performance-critical loops, vectorizable DSP kernels | Larger binary; aggressive inlining can hurt I-cache hit rates on Cortex-M7 |
-Os | Code-size optimized while keeping reasonable speed | Slightly slower than -O2 on hot paths |
-Oz | Smallest possible binary, e.g. Cortex-M0+ with 32 KB flash | Most aggressive size optimization; can disable inlining that -Os keeps |
Source: Arm Compiler armclang Reference Guide, -O options and the Arm Compiler 6 User Guide.
Function-level optimization with __attribute__
A single -O flag on the project applies to every translation unit. That is rarely the right answer for firmware, you usually want one or two hot functions tuned aggressively while the rest of the image stays size-optimized. armclang accepts the GCC-compatible __attribute__((optimize("O3"))) on individual functions, and #pragma clang optimize on/off for bracketed regions:
__attribute__((optimize("O3")))
void fir_filter(const int16_t *in, int16_t *out, size_t n) {
/* hot DSP loop, compile at O3 even if the project is -Oz */
}
This pattern keeps the global build at -Os or -Oz for footprint while letting compute kernels run at -O3.
When -Oz pays off and when it hurts
-Oz is the right default for memory-constrained MCUs, Cortex-M0+ parts with 32–64 KB flash, BLE peripherals, sensor nodes. On a Cortex-M7 running an FFT or motor-control loop, -Oz will suppress the inlining and loop unrolling those kernels depend on, and you will measure the difference on a scope. Use -Oz globally with per-function __attribute__((optimize("O3"))) overrides on the kernels that matter, rather than flipping the whole project to -O3.
Linker dead-code elimination pairs with the compiler choice
Compiler optimization only goes as far as the translation unit. To strip unused functions and data at link time, compile with -ffunction-sections -fdata-sections so each symbol lands in its own section, then link with armlink --remove (or armclang -Wl,--gc-sections when invoking the linker via the compiler driver). The Arm linker’s unused section elimination walks the call graph from the entry point and discards anything unreachable. This pairing, fine-grained sections at compile time, garbage collection at link time, typically recovers 5–15 % of image size on a real firmware build before you change a single line of C.
For end-to-end toolchain details and the latest armclang release notes, see the Arm Compiler 6 product page and the Keil MDK page on developer.arm.com.
Also appears in:
Interested in Arm tools?
Talk to our application engineers for personalized tool recommendations.
More from Arm
View all →