Skip to main content

C++ Compiler Optimization

C++ compiler optimization lab notebook — compare optimization levels, inspect compiler rewrites, diagnose auto-vectorization, and build production verification commands.

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

C++ Source
int sum_array(int* arr, int n) {
    int sum = 0;
    for (int i = 0; i < n; i++)
        sum += arr[i];
    return sum;
}
Recorded Assembly (-O2)
_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
Recorded with GCC 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04.3), x86-64 System V, using g++ -x c++ -std=c++17 -O<level> -mavx2 -S -masm=intel -o - -. Compiler metadata and assembler directives are omitted; all function instructions and control-flow labels are retained. Function byte sizes come from nm -S on the corresponding object. Instruction counts describe the emitted function, not executed work or measured latency. Different versions and targets can produce different code.

-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.

Step 1 / 4

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.

Before Optimization
int calculate() {
    int a = 2 * 3;
    int b = 10 / 2;
    int c = a + b;
    return c * 4;
}
After Optimization
int calculate() {
    return 44;
    // 2*3=6, 10/2=5
    // 6+5=11, 11*4=44
}

Transformation Steps

  1. 1. Fold 2 * 3 = 6 (current)

    Original
    int a = 2 * 3;
    Transformed
    int a = 6;
  2. 2. Fold 10 / 2 = 5 (upcoming)

    Original
    int b = 10 / 2;
    Transformed
    int b = 5;
  3. 3. Fold 6 + 5 = 11 (upcoming)

    Original
    int c = a + b;
    Transformed
    int c = 11;
  4. 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
Authored transformations and qualitative tradeoffs, not measured compiler output or speedups. The level-to-pass mapping is simplified; enabled passes depend on the compiler version, target and program.

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.

C++ Source
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

vectorization illustration
illustration: vectorized loop (vectorization width: 8)
vectorization checks
g++ -O2 -march=native -ftree-vectorize -fopt-info-vec-optimized source.cpp
clang++ -O2 -march=native -Rpass=loop-vectorize -Rpass-missed=loop-vectorize source.cpp
Authored vectorization scenarios and diagnostic illustrations, not recorded compiler output. The bars compare one scalar lane with an illustrated eight-lane AVX float vector; lane width is not measured speedup or elements per CPU cycle. Compiler decisions depend on target, version, cost model and alias checks.

When 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 .cpp file 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

Generated Command
g++ -O2 -march=native -Wall -Wextra source.cpp -o binary

Active Flags

-march=native

Generate code for your CPU's specific features (AVX2, FMA, etc.). Binaries won't run on older CPUs.

-Wall -Wextra

Enable comprehensive warnings. Catches common bugs at compile time.

Commands are assembled from your selections; they are not executed here. Profile data must match the source and build. Compiler support, sanitizer coverage and optimization results vary by toolchain and target.

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

If you found this explanation helpful, consider sharing it with others.

Mastodon