Embedded & FirmwareInternubiquitous

C const and restrict for Optimization Hints

Use C const and restrict correctly in embedded firmware to cut RAM use, shrink flash size, and speed up hot loops on Cortex-M targets.

8 min readAhmet Zahid ArıcanUpdated 23 Sept 2026
Contents & prerequisites

Why Optimization Hints Matter in Embedded Systems

Memory and performance constraints on MCUs

A typical Cortex-M0 target ships with 16-64 KB of flash and 4-8 KB of RAM. Every byte the compiler cannot prove is read-only gets a spot in RAM and a copy routine in the startup code. Every pointer the compiler cannot prove is unaliased forces it to reload values from memory on every iteration of a loop, because it has to assume any write might have changed data reachable through another pointer. On a part with a single-cycle flash and no data cache, that reload isn't free — it's a memory-bus transaction that can stall the pipeline for multiple cycles depending on wait states. The details of how that stall propagates through fetch, decode, and execute vary by core; see ARM Cortex-M Pipeline Stages (M0 to M55) for how pipeline depth changes the cost of a missed optimization.

How compiler directives reduce code size and latency

const and restrict are not runtime features. They add zero instructions by themselves. What they do is remove the compiler's uncertainty about your data, which lets it delete redundant loads, fold constant expressions, place data in flash instead of RAM, and reorder or vectorize memory operations. The net effect shows up as fewer instructions in the final binary and fewer bus cycles at runtime — exactly the two things embedded engineers are usually trying to buy back.

Trade-offs between readability and optimization

Aggressive use of restrict and const can make signatures harder to read and, if misapplied, introduces undefined behavior that is invisible until an optimization level change puts it into the wrong instruction. This isn't a knob to blindly max out — it's a promise to the compiler, and broken promises produce bugs that only appear at -O2 and above.

The const Qualifier: Intent and Compiler Benefits

Declaring read-only data and compile-time constants

const tells the compiler a value will never be written after initialization. For scalars and small aggregates, this enables the compiler to substitute the value directly at use sites instead of loading it from memory:

const uint32_t sample_rate_hz = 48000;

If sample_rate_hz is used only in arithmetic, the compiler may fold it into immediates entirely and never emit storage for it. Contrast this with a plain global, which the compiler must assume could change between two reads unless it can prove no other code writes it.

Placing const data in ROM/Flash memory sections

For arrays and tables — lookup tables, coefficient sets, string literals — const is the difference between the linker placing data in .rodata (flash) versus .data (RAM, with a flash-to-RAM copy at startup). On a device with 8 KB of RAM, a 2 KB lookup table declared without const doubles your static RAM budget for no reason and adds a copy loop to Reset_Handler.

const int16_t sine_table[256] = { /* ... */ };

Check your linker map file after adding const to confirm the symbol actually migrated to a flash region — some toolchains still place const structs containing pointers into RAM because the pointers themselves need runtime relocation.

Function parameters and return values as const

Marking pointer parameters const doesn't just document intent — it lets the compiler eliminate redundant reloads across calls, because it knows the callee cannot mutate the pointee. It also lets the compiler put the argument in read-only storage when the caller passes a literal, avoiding a stack copy in some cases.

void log_message(const char *msg);

How const enables dead-code elimination and folding

When a const variable's value is known at compile time, entire branches guarded by that value can be eliminated:

const int debug_level = 0;
if (debug_level > 0) {
    dump_diagnostics();
}

With debug_level as const and visible at the call site (same translation unit, or LTO across units), the compiler proves the branch is dead and removes both the check and the call to dump_diagnostics, shrinking flash usage. Without const, the compiler must assume the value could differ at runtime and keep the code.

The restrict Qualifier: Alias Analysis and Memory Safety

Declaring exclusive pointer ownership

restrict (C99, available as __restrict in some C++ toolchains) is a promise attached to a pointer: for the lifetime of that pointer, the object it points to will only be accessed through that pointer or expressions derived from it — never through some other, unrelated pointer in the same scope. It says nothing about the data itself being constant; it's purely about aliasing.

Enabling aggressive loop unrolling and vectorization

Without restrict, the compiler must assume any two pointer parameters might overlap, which blocks reordering, unrolling, and SIMD-style transformations even on cores without a vector unit, because the memory-access ordering still matters for scalar reloads.

void vector_add(int *restrict dst, const int *restrict a,
                 const int *restrict b, size_t n)
{
    for (size_t i = 0; i < n; i++) {
        dst[i] = a[i] + b[i];
    }
}

Without restrict on dst, a, and b, the compiler must assume a write to dst[i] could alter a[i+1] or b[i+1] on the next iteration, and it will reload those values from memory every pass. With restrict, it can keep values in registers, unroll the loop, and on cores with DSP extensions, pack operations more densely.

Avoiding false dependencies in memory operations

This matters most in DSP-style firmware: FIR filters, FFT butterflies, block memory transforms. A single missing restrict on a filter's coefficient pointer can prevent the compiler from unrolling an otherwise hot loop, and you'll never see a diagnostic telling you so — it just silently emits the conservative version.

restrict in function signatures for firmware routines

Apply restrict at the point where you know the caller cannot pass overlapping buffers — memcpy-style routines, CRC calculators over a fixed buffer, ADC-to-processing pipelines with double-buffering. Do not apply it to routines like memmove-equivalents which are explicitly meant to tolerate overlap.

Combining const and restrict in Real Firmware

const restrict for input buffer parameters

The strongest, safest pattern for a read-only input buffer is const T *restrict: the pointee cannot be modified through this pointer, and no other pointer in scope aliases it.

uint32_t crc32(const uint8_t *restrict data, size_t len);

This tells the compiler both facts at once, and typically produces the tightest loop the target's instruction set allows.

Volatile vs. const in hardware register contexts

const and volatile answer different questions and are often confused. const means "the compiler can assume this doesn't change without explicit code writing it." volatile means "the compiler must assume this can change at any time, for reasons outside the visible code, and every access must actually touch memory." A read-only hardware status register is const volatile: the firmware never writes it (const), but its value can change due to hardware events between reads (volatile), so the compiler must not cache it in a register or elide repeated reads.

#define STATUS_REG (*(const volatile uint32_t *)0x40001000UL)

Using plain const on a hardware register without volatile is a real bug: the compiler may read it once and reuse the cached value across a polling loop, hanging your firmware waiting for a bit that will never appear to change.

Common patterns in ISR and DMA callback functions

DMA completion buffers are a natural fit for const restrict on the consumer side (the buffer won't be written by the processing routine and doesn't alias other live buffers) combined with volatile on any flag the ISR sets and the main loop polls. Don't conflate the two: the buffer contents after DMA completes are stable and can be const, while the completion flag itself needs volatile.

Measuring and Verifying Optimization Results

Inspecting generated assembly with compiler explorer tools

The only reliable way to confirm a const/restrict change did something is to look at the generated assembly for your actual target and flags, not for x86. Compiler Explorer supports ARM GCC and Clang targets; compile with your project's actual -mcpu, -O level, and check whether the loop body shrank, whether reloads disappeared, or whether the vectorizer kicked in.

Code size and execution time profiling on real hardware

Compare .text and .data sizes before and after with size or arm-none-eabi-size on the built ELF. For latency, use a GPIO toggle around the hot loop and a scope, or a cycle counter (DWT->CYCCNT on Cortex-M3 and up) if your core provides one — check your specific core's Technical Reference Manual for availability, since M0/M0+ typically lack it.

Linker map files and memory layout verification

Generate the linker map (-Wl,-Map=output.map) and grep for your const arrays to confirm they landed in a flash region (.rodata or equivalent) rather than .data/.bss. This is the definitive check that a const change actually freed RAM rather than just adding a qualifier with no placement effect.

Common Pitfalls and Best Practices

Over-applying const where mutability is needed

Marking a buffer const that a driver later needs to write in-place forces an awkward cast-away-const or a redesign. Apply const where the data is genuinely immutable for the object's whole lifetime, not just at one call site.

restrict aliasing violations and undefined behavior

If you tell the compiler two pointers don't alias and they do, the compiler is free to produce code that silently computes wrong results — reordered stores, stale reloads, or corrupted output — with no warning at any optimization level. This is a real, observed failure mode when generic buffer-processing functions are reused for in-place operation without checking the restrict contract. Always audit every call site when adding restrict to an existing function signature.

Platform-specific const placement directives (e.g., __flash)

Some 8-bit and small 32-bit toolchains (notably AVR-GCC's __flash qualifier) require an explicit address-space qualifier in addition to const because their architecture has genuinely separate address spaces for flash and RAM, and standard C has no concept of that. Check your compiler's manual before assuming plain const alone relocates data on non-ARM targets.

Documentation and team consistency

Because restrict is a contract rather than a checked property, comment every restrict parameter with the aliasing assumption it depends on, and treat adding or removing it as a reviewed API change, not a drive-by optimization tweak.

Learning

Sign in to track your progress.

Evidence

Public projects engineers linked to C const and restrict for Optimization Hints.

Add evidence

No engineer has linked a project to this topic yet. Built something that proves it? Add the project and tag it with embedded-firmware-c-const-and-restrict-for-optimization-hints — it then shows here and on your public profile.