Top 20 largest symbols

April 9, 2026 · View on GitHub

FieldValue
TitleEmbedded Performance Optimization
Typetheory
Statusapproved
Version1.0.0
Componentfoc
Date2026-04-07

Overview

Real-time motor control firmware must meet strict cycle-count deadlines at every control-loop iteration. On an ARM Cortex-M4F running at 120 MHz with a 20 kHz FOC loop, the complete ISR computation budget is 6 000 cycles — fewer than 400 of which should be consumed by the FOC core (Clarke, Park, two PI controllers, inverse Park, SVM). Violating this budget causes PWM update skips, current ripple, and control instability.

This document describes the compiler and code-level techniques used to keep the FOC path within budget, how to measure and verify cycle usage, and the common patterns that silently erode performance.


Prerequisites

SymbolMeaningUnit
fcpuf_{cpu}CPU clock frequencyHz
fswf_{sw}Switching (PWM) frequencyHz
NcycN_{cyc}Cycles available per control periodcycles
CPICycles Per Instruction (pipeline)
FMAFused Multiply-Accumulate
LTOLink-Time Optimisation
Ncyc=fcpufsw=120MHz20kHz=6000 cyclesN_{cyc} = \frac{f_{cpu}}{f_{sw}} = \frac{120\,\text{MHz}}{20\,\text{kHz}} = 6000\ \text{cycles}

Mathematical Foundation

ARM Cortex-M4 Instruction Pipeline

The Cortex-M4 uses a 3-stage pipeline (fetch, decode, execute) with a dual-issue capability for certain instruction pairs. The FPU (FPv4-SP) adds a separate pipelined floating-point execution unit. Key throughput properties:

InstructionDescriptionLatency (cycles)Throughput (cycles/inst)
vfma.f32Fused multiply-add11
vmul.f32Float multiply11
vadd.f32Float add11
vdiv.f32Float divide1414
vsqrt.f32Float sqrt1414
vcmpe.f32Float compare11
blx rNIndirect call (virtual)3+3+
bl <addr>Direct call1+N1+N
push/pop (4 regs)Stack save/restore22

Implication for FOC: All hot-path arithmetic should use FMA-capable patterns. Division and sqrt must be avoided in the ISR where possible; use reciprocals or LUT-based approximations instead.

Compiler Optimisation Theory

The GCC/Clang compiler applies transformations that can drastically change cycle count:

  1. Inlining: For short functions called in tight loops, inlining eliminates the call overhead (branch, stack frame setup/teardown), and enables cross-function optimisations such as constant propagation and dead-code elimination.

  2. Loop unrolling: The compiler repeats loop body kk times to reduce branch overhead and expose more instruction-level parallelism. Controlled by -funroll-loops or #pragma GCC optimize("O3").

  3. Constant propagation / folding: Compile-time evaluation of constant expressions eliminates runtime computation. Use constexpr aggressively for lookup tables and configuration constants.

  4. Auto-vectorisation: Cortex-M4 SIMD (DSP extensions) can process two 16-bit or four 8-bit operands per cycle. Q15/Q31 fixed-point code benefits; floating-point does not (no NEON on M4).

  5. Fast-math transformations (-ffast-math, #pragma GCC optimize("fast-math")):

    • Allows re-association of floating-point expressions.
    • Enables FMA generation (avoids separate multiply + add).
    • Assumes no NaN/Inf inputs.
    • May change numerical results slightly — verify with tests.

Virtual Dispatch Cost Model

A virtual function call requires:

  1. Load vtable pointer from this (1 load, possible cache miss).
  2. Load function pointer from vtable (1 load, possible i-cache miss).
  3. Indirect branch blx rN (3+ cycles, branch predictor cannot speculate).
  4. Callee setup/teardown (push/pop, stack frame).

Total overhead: 6–20+ cycles per call depending on cache state. At 40 ns per virtual call (120 MHz), 10 virtual calls per FOC iteration consume 400 ns — approximately 6.7 % of the 20 kHz budget.

For the FOC path, prefer static dispatch (non-virtual, inline, constexpr) for all computation that occurs inside the ISR.


Optimisation Techniques

1. Avoid Virtual Functions in Hot Paths

Avoid (virtual dispatch overhead):

class ITrigonometry {
public:
    virtual float Sine(float angle) const = 0;
};
// In hot path: vtable lookup + indirect call
float sin_val = trig->Sine(angle);

Prefer (static dispatch, inlinable):

struct FastTrigonometry {
    static inline float Sine(float angle) noexcept {
        return LookupTable[index];  // Direct access, fully inlined
    }
};
float sin_val = FastTrigonometry::Sine(angle);

2. Avoid std::optional in Performance-Critical Code

// Avoid: generates has_value() checks and extra memory access
std::optional<float> setPoint;

// Prefer: simple flag
float setPointValue = 0.0f;
bool hasSetPoint = false;
if (!hasSetPoint) [[unlikely]] return;

3. Use constexpr and inline Aggressively

// Computed entirely at compile time — zero runtime cost
inline constexpr std::array<float, 512> sineLUT = []() {
    std::array<float, 512> table{};
    for (size_t i = 0; i < 512; ++i)
        table[i] = std::sin(2.0f * M_PI * i / 512.0f);
    return table;
}();

4. Use Compiler Attributes

#define ALWAYS_INLINE  __attribute__((always_inline)) inline
#define HOT_FUNCTION   __attribute__((hot))

// Combined: force inlining + speed-optimised
__attribute__((always_inline, hot)) inline
void FocStep(FocState& s) { /* ... */ }

5. Prefer Fixed-Size Integer Types

// Good: Explicit widths, portable
uint32_t counter;
int16_t  current_mA;
float    voltage_V;

// Avoid: Implementation-defined widths
int counter;
short current;

6. Minimise Stack Usage

// Avoid: 4 KB on stack inside a function
void Calculate() {
    float buffer[1024];
}

// Prefer: static member in .bss
class Calculator {
    static float buffer[1024];  // Zero-init, no stack cost
};

7. Enable FMA Patterns

With -ffast-math, the compiler emits vfma.f32 for a * b + c patterns:

// This single expression:
result = a * b + c;
// Becomes: vfma.f32 s0, s1, s2  (1 cycle, full precision)

8. Per-File Optimisation Pragmas

For performance-critical translation units (FOC implementations, SVM, transforms):

#if defined(__GNUC__) || defined(__clang__)
#pragma GCC optimize("O3", "fast-math")
#endif

Place at the top of the .cpp file, before any includes that define affected functions.

9. Per-Function Attributes

__attribute__((optimize("-O3")))
void CriticalFunction() {
    // Always compiled at -O3 regardless of TU-level flags
}

Note: function attributes do not propagate to callees. Use file-level pragmas for transitive effect.


Debug Builds

By default, Debug builds (-O0) disable all optimisations, making code 3–10× slower than Release. This breaks real-time deadlines for FOC ISRs during debugging sessions.

Solution 1 — Use -Og for debug builds (recommended):

set(CMAKE_CXX_FLAGS_DEBUG "-Og -g" CACHE STRING "Debug flags" FORCE)

-Og provides: basic inlining, dead-code elimination, register allocation — while keeping full debuggability (variable inspection, correct stack trace).

Solution 2 — File-level pragma (when specific files must remain fast even at -O0):

#pragma GCC optimize("O3", "fast-math")

Solution 3 — Function attribute (for a single bottleneck function):

__attribute__((optimize("-O3")))
void CriticalFunction() { /* ... */ }

Analysing Generated Code

Disassembly with objdump

# With C++ demangling + source interleave (requires -g)
arm-none-eabi-objdump -d -S -C firmware.elf > disassembly_with_source.txt

# Just one section
arm-none-eabi-objdump -d -j .text file.elf

# Specific function (grep)
arm-none-eabi-objdump -d -C firmware.elf | grep -A 100 "FunctionName"

Size Analysis

# Section sizes
arm-none-eabi-size firmware.elf

# Top 20 largest symbols
arm-none-eabi-nm --size-sort -C firmware.elf | tail -20

Signs of Poor Optimisation

; Virtual dispatch footprint
ldr    r3, [r0, #0]   ; Load vtable pointer
ldr    r3, [r3, #4]   ; Load function pointer
blx    r3             ; Indirect call — 3+ cycles, unpredictable branch

; Division (avoid in ISR)
vdiv.f32  s0, s1, s2  ; 14 cycles

; Repeated memory loads (register allocation failure)
ldr    r3, [r7, #4]
; ... some code ...
ldr    r3, [r7, #4]   ; Same address loaded again

Signs of Good Optimisation

; Minimal stack frame
push   {r4, r5, lr}
sub    sp, #16

; FMA instruction
vfma.f32  s0, s1, s2   ; 1 cycle

; Conditional execution — no branch
vcmpe.f32  s0, s1
it         gt
vmovgt.f32 s0, s1

Measuring Performance

Cycle Counter (DWT)

// Enable at startup (once)
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL   |= DWT_CTRL_CYCCNTENA_Msk;

// Measure
uint32_t start  = DWT->CYCCNT;
FocCoreStep();
uint32_t cycles = DWT->CYCCNT - start;

GPIO Toggle (requires oscilloscope)

GPIO_SetPin(DEBUG_PIN);
FocCoreStep();
GPIO_ClearPin(DEBUG_PIN);
// Measure pulse width on scope → cycles = width × f_cpu

Numerical Properties

Control Loop Cycle Budgets — ARM Cortex-M4 at 120 MHz

Control RateCycles/PeriodFOC Budget (< 7 %)Notes
10 kHz12 000840Low-frequency, conservative
20 kHz6 000420Target rate for this project
40 kHz3 000210High performance, tight budget
100 kHz1 20084Requires fixed-point or DSP

FOC core typical requirements:

  • Optimised (-O3, fast-math, LUT sin/cos): 200–400 cycles
  • Non-optimised (-O0, sinf/cosf calls): 800–1500 cycles

Key Performance Sensitivities

Code PatternCycle ImpactRecommendation
sinf() / cosf()50–200 cycles eachReplace with 512-entry LUT
Virtual call in ISR6–20 cycles eachUse static dispatch / inline
vdiv.f3214 cyclesUse precomputed reciprocal
vsqrt.f3214 cyclesUse fast inverse sqrt approximation
Stack frame > 64 bytes2–4 cycles extraReduce local variables
Cache miss (data)3–10 cyclesKeep hot data in registers or TCM

Common Pitfalls

ProblemSymptomFix
Heap allocation in ISRNon-deterministic latency spikeUse static/stack allocation only
printf in ISR10 000+ cycles, UART blockUse DWT counter or GPIO toggle instead
Exception handling overheadIncreased code size, slow pathsCompile with -fno-exceptions -fno-rtti
Float in integer-only codeUnnecessary FPU state saveUse integer arithmetic or explicit cast
Packed struct with uint32Unaligned access fault or stallAlign struct members on natural boundary

Quick Reference

GCC Optimisation Pragmas

#pragma GCC optimize("O3")        // Maximum speed
#pragma GCC optimize("Os")        // Minimum size
#pragma GCC optimize("fast-math") // Aggressive FP
#pragma GCC push_options          // Save current options
#pragma GCC pop_options           // Restore options

Function Attributes

__attribute__((always_inline))   // Force inline
__attribute__((noinline))        // Prevent inline
__attribute__((hot))             // Optimise for speed
__attribute__((cold))            // Optimise for size
__attribute__((pure))            // No side effects
__attribute__((const))           // Pure + no memory reads
__attribute__((flatten))         // Inline all callees

Branch Hints

if (condition) [[likely]]   { }   // C++20
if (condition) [[unlikely]] { }   // C++20

References

  1. ARM — Cortex-M4 Technical Reference Manual, ARM DDI 0439B.
  2. ARM — Cortex-M4 Devices Generic User Guide, ARM DUI 0553A.
  3. Fog, A. — Optimizing software in C++, Technical University of Denmark, 2023.
  4. GCC Project — Optimize Options, GCC Manual, https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
  5. Texas Instruments — AM335x ARM Cortex-A8 Microprocessors Technical Reference Manual, 2019.