Course contentsShow
← Embedded Systems & Computer Architecture
Lesson 436 of 9366. Embedded Software Construction and ToolchainsFree lesson

Optimizing Code Size for Resource Constrained Systems

You will apply compiler flags to balance execution speed against code footprint to ensure firmware fits within limited flash memory.

In this lesson, you will learn how to strategically toggle compiler optimization flags to shrink your firmware’s footprint when you are running out of flash memory.

When developing for resource-constrained systems, the compiler’s default behavior is often focused on maximizing execution speed, which typically involves techniques like function inlining and loop unrolling. While these make your code run faster, they also balloon the size of your binary. To reclaim space, we must guide the compiler toward size-conscious decision-making.

Balancing Size and Speed

Most modern toolchains (like GCC or Clang) provide specific flags to prioritize code size. The primary flag for this is -Os. Unlike -O2 or -O3, which prioritize performance, -Os enables all optimizations that do not typically increase the code size, while specifically targeting code bloat.

For example, consider a function that is called in multiple places:

inline int calculate_checksum(int data) {
    // A large block of complex logic
    return result;
}

With -O2, the compiler will likely "inline" the entire block into every call site, replacing the function call with the actual logic. This speeds up execution by removing branching overhead but consumes significantly more flash memory. By switching to -Os, the compiler will favor standard function calls to keep the total instruction count low.

Fine-Tuning with Linker Flags

Beyond the main optimization level, you can use additional flags to prune the final binary. The -ffunction-sections and -fdata-sections flags tell the compiler to place each function and data variable into its own unique section. When combined with the linker flag --gc-sections, the linker can identify and discard unused code that wasn't stripped during compilation.

If your application has specific sections of code that are performance-critical (like a tight interrupt handler) while the rest is size-constrained, you can use attributes in your C code to override the global setting:

__attribute__((optimize("-O3"))) void critical_isr(void) {
    // This specific function will prioritize speed
}

By mixing global -Os settings with targeted overrides, you can ensure your critical paths remain performant while the bulk of your application remains small enough to fit within your device's memory limits.

Key Takeaway: Use the -Os flag to prioritize binary size over speed, and pair it with --gc-sections to eliminate unused code, ensuring your application fits comfortably within restricted flash memory.