Modern Embedded C++ Tutorial: Common Compiler Flags Guide
In real-world embedded development, every single byte of Flash and RAM is truly saved by the developer. Although C++ carries the bias of being a "heavyweight language," by configuring compiler flags appropriately, we can precisely trim runtime overhead, achieving performance and code size that even surpass hand-written C code. (I believe you have already seen this in Chapter 0).
0 Some Basics
Language Standard Control: -std=
This is the most direct way to define the "modernity" of a project.
- Parameter Format:
-std=c++11,-std=c++14,-std=c++17,-std=c++23. - GNU Extension Version:
-std=gnu++17. Compared to the standard-std=c++17, it allows the use of some GCC-specific non-standard extensions (such as special inline assembly syntax). In low-level embedded development, we sometimes have to use thegnu++version.
Why choose C++17 or above in embedded?
- The Power of
constexpr: In C++17, a significant amount of logic can be moved to compile-time calculation, directly reducing runtime CPU load and Flash footprint. std::span(C++20): It is the perfect replacement for passing buffers in embedded development, safer than traditional raw pointers with no extra overhead.- Structured Binding: Makes parsing complex sensor data structures extremely elegant.
Preprocessor and Macros: -D and -U
In embedded development, due to hardware differences, we often need "conditional compilation."
-D: Define a macro.- Example:
-DDEBUGor-DSTM32F407xx. - Modern Practice: Try to control this via
target_compile_definitionsin CMake, rather than filling your code with#ifdef.
- Example:
-U: Undefine a defined macro.
Warning: Over-reliance on macros makes code paths difficult to test (Code Coverage cannot cover branches where macros are disabled). In modern C++, it is recommended to prioritize
if constexprcombined with constant objects.
Path Search and Library Linking: -I, -isystem, -L, -l
This is where beginners are most prone to configuration errors in CMake.
-I(Include): Specify header file search paths.-isystem: Specify "system" header file paths.- The Nuance: If a third-party library (like ST's HAL library) generates a lot of meaningless warnings, use
-isystemto include them. The compiler will automatically suppress all warnings in that directory, keeping your console clean.
- The Nuance: If a third-party library (like ST's HAL library) generates a lot of meaningless warnings, use
-L: Specify the search directory for static libraries (.afiles).-l: Link the specified library.- Note: If the library name is
libfoo.a, the parameter is-lfoo(remove thelibprefix and extension).
- Note: If the library name is
Output Management and Debug Info: -o and -g
-o: Specify the output filename. In cross-compilation, we usually generate an ELF file, and then useobjcopyto convert it to HEX or BIN.-gand-g3:-ggenerates standard debugging symbols for GDB debugging.-g3: Even includes debugging information for macro definitions. If you need to inspect the value of a certain#defineduring debugging, turn this on.- Misconception Correction: Enabling
-gdoes not increase the code size running on the board. Debugging information only exists in the ELF file on your computer and is not flashed into the MCU's Flash.
Warning Governance: The -W Series (Code Quality)
In safety-sensitive fields like embedded systems, warnings are hidden bugs.
-Wall: The standard for most developers, enabling most valuable warnings.-Werror: Treats all warnings as errors.- Recommended Practice: Force enable
-Werrorin CI/CD (Continuous Integration) environments to ensure committed code has no hidden dangers.
- Recommended Practice: Force enable
-Wshadow: Warns when a local variable name shadows a global variable name, which is extremely useful during embedded logic switching.-Wdouble-promotion: Embedded Essential! Warns when you unintentionally promote afloatto adouble. On MCUs without double-precision hardware floating-point units, this leads to a catastrophic drop in performance.
Dependency Generation: -M, -MD
Have you ever wondered how CMake knows "because you modified a header file, these 10 source files need to be recompiled"?
-MD: Generates a dependency relationship file with a.dsuffix during compilation.- Automation: Modern build systems (CMake/Ninja) handle these options automatically. Understanding this helps you troubleshoot incremental compilation issues like "Why didn't the compiler react after I changed my code?"
g++ -c main.cpp -MD -MF main.d1. Optimization Levels: Balancing Speed, Size, and Debugging
GCC and Clang provide multi-level optimization switches. Understanding their differences is a fundamental skill for embedded developers.
| Option | Name | Core Behavior | Applicable Scenarios |
|---|---|---|---|
-O0 | No Optimization | Maintains a one-to-one correspondence between code and assembly. | Only for tracking down extremely difficult logic bugs. |
-Og | Debug Optimization | Enables optimizations that do not affect debugging observation. | First choice for development phase, balancing performance with single-stepping. |
-O2 | Performance Optimization | Enables almost all optimizations that do not trade space for time. | High-performance computing, RTOS task logic. |
-Os | Size Optimization | Enables options in -O2 that do not increase code size. | Default choice for embedded release. |
-Ofast | Fast Optimization | Breaks IEEE 754 standard (does not guarantee floating-point precision). | Pure mathematical calculations where minor precision differences are acceptable. |
💡 Deep Dive: Why not use -O3 in embedded?
-O3 performs extensive loop unrolling and function inlining. While speed might increase, on MCUs with tight Flash space, it leads to code bloat. It might even degrade performance due to instruction cache (I-Cache) misses.
2. Trimming C++ Runtime: Shedding Heavy "Armor"
Modern C++ carries some features by default that come at a high cost in embedded systems. With the following two options, we can "slim down" C++ to have overhead similar to C.
2.1 -fno-exceptions (Disable Exceptions)
- Cost: C++ exceptions require massive "unwind table" support, increasing Flash footprint by about 10%~20%.
- Consequence: Cannot use
try/catchorthrow. If the program errors, it will directly callstd::terminate. - Embedded Guideline: In resource-constrained systems (like Cortex-M), strongly recommended to disable.
2.2 -fno-rtti (Disable Runtime Type Information)
- Cost: To support
dynamic_castandtypeid, the compiler generates extra metadata (information beyond the vtable) for every class with virtual functions. - Consequence: Cannot determine the real type of an object at runtime.
- Embedded Guideline: Modern embedded design favors compile-time polymorphism (templates/CRTP), so RTTI is usually redundant.
3. Garbage Collecting Unused Code
By default, the compiler compiles the entire source file into one massive binary block. Even if you only use one function from a library, the linker will stuff the entire library's code into Flash.
3.1 Compiler Side: Sectioning
-ffunction-sections: Packages each function independently into a section.-fdata-sections: Packages each global/static variable independently.
3.2 Linker Side: Garbage Collection
-Wl,--gc-sections: Tells the linker (ld) to scan all sections and thoroughly remove "dead code" that is not referenced from the final ELF file.
4. Best Practice Configuration in CMake
Translating the above theory into code. In your top-level CMakeLists.txt, we recommend managing these options like this:
Expand (27 lines)Collapse
# 1. Language Standard: Require C++17
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # Use standard C++, not GNU extensions
# 2. Optimization & Debug Symbols
# Release mode: Size optimization (-Os)
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os")
# Debug mode: Debug optimization (-Og)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Og -g3")
# 3. Warning Settings
add_compile_options(-Wall)
add_compile_options(-Wextra) # Enable extra warnings
add_compile_options(-Werror) # Treat warnings as errors (Optional for CI)
add_compile_options(-Wshadow)
add_compile_options(-Wdouble-promotion)
# 4. Embedded Runtime Trimming
add_compile_options(-fno-exceptions)
add_compile_options(-fno-rtti)
# 5. Link Time Optimization (LTO) & Dead Code Elimination
add_compile_options(-ffunction-sections -fdata-sections)
add_link_options(-Wl,--gc-sections)
# Optional: Enable LTO for further optimization
# add_link_options(-flto)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
5. Dangerous -Ofast and Floating-Point Traps
In embedded systems, -Ofast enables -ffast-math. This can lead to:
- Loss of Precision: To speed up execution, the compiler might ignore tiny floating-point errors.
- NaN/Inf Failure: It assumes your program will never produce illegal floating-point numbers.
- Reordering Operations: This can lead to unstable results in some algorithms.
Recommendation: Unless you are doing pure digital signal processing (DSP) and have full control over precision, always stick to -O2 or -Os.
Online Run
Compare the assembly code generated by the compiler under different optimization levels (-O0 / -Os / -O2) online to observe the effects of inlining and constant folding:
Compiler Explorer
Common Compiler Options
Compare assembly generated under -O0 / -Os / -O2, observe inlining and constant folding