Why Compiler Optimization Matters
The difference between -O0 and -O2 is typically 5-10x in execution speed. Between -O0 and -O3 with auto-vectorization, it can be 20-40x for numerical code. The important skill is not memorizing flags. It is learning to ask: what rewrite did the compiler prove safe, what tradeoff did it make, and how do I verify the result?
Think of optimization as a lab workflow:
- choose a starting level such as
-O2 - inspect the generated assembly or compiler remarks
- fix source-level blockers like aliasing or dependencies
- add LTO or PGO only when measurement justifies it
- treat unsafe flags as explicit correctness decisions
Optimization levels
Each -O level enables progressively more aggressive optimizations. The difference isn’t just “faster” — the compiler generates fundamentally different assembly at each level:
Optimization Level Comparator
Compare the same C++ source at five optimization levels. Each selection shows recorded assembly from one compiler and target.
-O2: Standard
int sum_array(int* arr, int n) {
int sum = 0;
for (int i = 0; i < n; i++)
sum += arr[i];
return sum;
}_Z9sum_arrayPii:
endbr64
test esi, esi
jle .L4
lea eax, -1[rsi]
lea rdx, 4[rdi+rax*4]
xor eax, eax
.L3:
add eax, DWORD PTR [rdi]
add rdi, 4
cmp rdi, rdx
jne .L3
ret
.L4:
xor eax, eax
ret-O2: The recorded function advances a pointer and compares it with the end address.
- Emitted function instructions
- 13
- Function code size
- 43 bytes
- Runtime performance
- Not measured
-O3 Is Not Always Faster
-O3 enables aggressive inlining and loop unrolling that increase binary
size. On code with large working sets, the extra I-cache pressure can make
-O3 slower than -O2. Always measure. The right answer is often -O2 -march=native.
Transformation passes
Tail-call elimination is another control-flow transformation: when a caller has no work left after a call, the compiler may transfer directly to the callee. The linked concept shows stack behavior and CPython 3.14’s use of tail calls between opcode handlers.
Compiler passes are the individual rewrites behind the optimization level. One pass folds constants, another removes dead code, another hoists loop-invariant work, and another rewrites expensive indexing into cheaper increments.
Use this lab to connect the human-readable source change to the mechanical compiler transformation:
Compiler Optimization Passes
Compare a source fragment with its transformation, then step through why each expression changes.
Optimization Level
Illustrated active passes: Constant Folding, Dead Code Elimination, Inline Expansion, Common Subexpression Elimination, Loop-Invariant Code Motion, Strength Reduction
Constant Folding
Evaluate constant expressions at compile time
Included in the illustrated -O2 pass set.
Current transformation: Fold 2 * 3 = 6.
int calculate() {
int a = 2 * 3;
int b = 10 / 2;
int c = a + b;
return c * 4;
}int calculate() {
return 44;
// 2*3=6, 10/2=5
// 6+5=11, 11*4=44
}Transformation Steps
1. Fold 2 * 3 = 6 (current)
Original int a = 2 * 3;Transformed int a = 6;2. Fold 10 / 2 = 5 (upcoming)
Original int b = 10 / 2;Transformed int b = 5;3. Fold 6 + 5 = 11 (upcoming)
Original int c = a + b;Transformed int c = 11;4. Fold 11 * 4 = 44 (upcoming)
Original return c * 4;Transformed return 44;
- Runtime tradeoff
- Removes work
- Code size
- Often similar
- Compile cost
- Low
- Best suited to
- Constant math
Vectorization
Auto-vectorization is where source shape matters most. At -O3 or with explicit vectorization flags, the compiler tries to convert scalar loops into SIMD instructions. It succeeds only when it can prove that iterations are independent and memory accesses are safe.
Auto-Vectorization Demo
Compare independent iterations, a loop-carried dependency and possible pointer aliasing. Inspect the source and the condition that affects SIMD.
void add_arrays(float* a, float* b,
float* c, int n) {
for (int i = 0; i < n; i++)
c[i] = a[i] + b[i];
}Auto-Vectorizes
Illustrated Lane Capacity
Scalar1 lane
Illustrated SIMD path8 lanes
8 illustrated lanes.
Why this case differs
The expression is elementwise with contiguous access. The compiler may need runtime checks when the pointer ranges could overlap.
Illustrated Compiler Diagnosis
g++ -O2 -march=native -ftree-vectorize -fopt-info-vec-optimized source.cpp
clang++ -O2 -march=native -Rpass=loop-vectorize -Rpass-missed=loop-vectorize source.cppWhen vectorization fails, the fix is usually structural: remove loop-carried dependencies, make memory access contiguous, or give the compiler a legal no-alias promise such as __restrict__.
Compiler flags and verification
Once local transformations look good, production builds usually add two more layers:
- LTO: lets the optimizer see across
.cppfile boundaries during linking. This enables cross-file inlining and whole-program dead function elimination. - PGO: uses representative runtime profiles to make better branch layout, inlining, and hot/cold code placement decisions.
- Reports: make the compiler tell you what it optimized, missed, or spent time on.
-ffast-math Changes Program Semantics
-ffast-math can be faster because it lets the compiler ignore parts of IEEE
754 floating-point behavior: NaN checks, infinities, signed zero, trapping
math, and strict reassociation rules. Treat it as a correctness decision, not
a normal performance flag.
Use the flag builder as the final lab step. Start with -O2, add one idea at a time, then verify with compiler reports and benchmarks instead of assuming the flag helped.
Production Flags & Verification
Build one command, then inspect its flags and the verification steps required for profile-guided optimization.
Compiler
Optimization
Performance
Profile-Guided Optimization
Sanitizers
Debug & Warnings
g++ -O2 -march=native -Wall -Wextra source.cpp -o binaryActive Flags
-march=nativeGenerate code for your CPU's specific features (AVX2, FMA, etc.). Binaries won't run on older CPUs.
-Wall -WextraEnable comprehensive warnings. Catches common bugs at compile time.
godbolt.org is still the fastest manual check: paste one function, compare -O2 and -O3, inspect whether the branch disappeared or the loop vectorized, then decide whether the real code deserves a build-system change.
Further Reading
- Godbolt Compiler Explorer - Instant assembly output for any C++ code, any compiler
- GCC Optimization Options - Complete reference for GCC optimization flags
- Clang/LLVM Optimization Remarks - How to read Clang’s optimization diagnostics
- What Every Programmer Should Know About Compiler Optimizations - Microsoft’s overview of key compiler optimizations
Related concepts
See how tail calls reuse stack frames, when compilers can eliminate them, and how CPython 3.14 applies tail calls to C opcode dispatch.
Follow a C++ function from tokens to a simplified AST, then inspect exact source ranges, declaration references and types in a keyboard-accessible explorer.
Understand the complete C++ compilation pipeline from source code to object files. Learn preprocessing, parsing, code generation, and optimization stages.
How C++ object files are linked into executables. Learn symbol resolution, static vs dynamic linking, and linker optimization.
C++ preprocessor visualized: macros, header guards, conditional compilation, and #include directives explained interactively.
Complete C++ thread safety guide — race conditions with step-through simulation, mutexes, atomics, condition variables, deadlock detection, memory ordering, and Thread Sanitizer walkthrough.
