Compiler Optimization Flags
Compiler flags like -O2 and -O3 tell the compiler how aggressively to rewrite your code for speed, trading compile time and predictability for runtime performance.
Prerequisites: C++ for Low-Latency Trading, Big-O Complexity
The C++ or Rust code you write is not what actually runs. The compiler reads it, rewrites it into something that computes the same result, and hands that to the CPU. Optimization flags tell the compiler how hard to work at the rewriting: whether to unroll a loop, inline a small function into its caller, reorder instructions, or drop a computation whose result is never used. The same source file can run five times faster or slower purely from which flag you compiled it with.
The idea: flags choose how aggressively the compiler rewrites your code
At -O0 (the default with no flag), the compiler does close to nothing — it translates your code almost line by line, which makes a debugger's step-through match your source exactly. That is why debug builds are slow: nothing has been optimized away.
Each level up trades that predictability for speed:
-O1: basic optimizations — dead code elimination, simple constant folding — with modest compile-time cost.-O2: the default for production builds almost everywhere. Inlines small functions, vectorizes some loops, reorders instructions for the pipeline, without changing floating-point semantics.-O3: everything in-O2plus more aggressive inlining and loop transformations (unrolling, more auto-vectorization). Usually faster, occasionally slower due to code bloat hurting the instruction cache — measure, don't assume.-Ofast:-O3plus relaxed floating-point rules (assumes no NaNs/infinities, allows reassociation). This can silently change numerical results — dangerous for pricing code where the exact rounding matters.
Two flags matter beyond the plain -O level:
-march=native: compile for the exact CPU you're building on, unlocking its specific instruction set (e.g., AVX-512). The binary may crash with "illegal instruction" on a different CPU model, so this is for a controlled, homogeneous deployment fleet, not a binary you ship broadly.-flto(link-time optimization): lets the compiler optimize across translation units — inlining a function defined in one.cppfile into a call site in another — at the cost of much longer link times.
Worked example 1: a loop that gets vectorized
// sum.cpp
double sum(const double* a, int n) {
double total = 0.0;
for (int i = 0; i < n; ++i) total += a[i];
return total;
}
At -O0, this compiles to a straight scalar loop: one load, one add, one store-back to total, per element — roughly iterations of real work. At -O2 or -O3 with -march=native, the compiler recognizes the loop sums independent chunks and rewrites it to add four or eight doubles at once using SIMD registers, cutting the number of loop iterations by that same factor. Same C++ source, same output value, meaningfully fewer CPU cycles — this is the single biggest reason a "slow" numerical routine turns out to just be missing a flag.
Worked example 2: measuring the actual difference
g++ -O0 sum.cpp -o sum_o0
g++ -O2 sum.cpp -o sum_o2
g++ -O3 -march=native sum.cpp -o sum_o3
# time each on a 100M-element array
./sum_o0 # e.g. 340 ms
./sum_o2 # e.g. 95 ms
./sum_o3 # e.g. 40 ms
The gap between -O0 and -O2 is almost always the largest single jump — that is where inlining, dead-store elimination, and register allocation kick in. The gap from -O2 to -O3/-march=native is smaller and much more workload-dependent: a branch-heavy, memory-bound routine may see nearly nothing, while a tight numerical loop like the one above can see several times the speedup from vectorization alone. The only way to know is to actually time both — see Profiling with Hardware Performance Counters.
Compiler flags don't make your algorithm asymptotically faster — a bad approach is still at any -O level. They shrink the constant factor by exploiting the CPU's actual capabilities: fewer instructions, wider SIMD operations, better register use. Fix the algorithm first, then reach for flags.
What this means in practice
Every low-latency trading system's build pipeline pins exact flags for its hot path, usually -O3 -march=<specific-cpu> for the deployment fleet, verified with the same tests that pass at -O0, because aggressive optimization can occasionally expose undefined behavior (like signed integer overflow) that -O0 happened to hide.
-Ofast and aggressive -ffast-math-style flags change floating-point results — reordering additions, assuming no NaN/Inf — which is exactly the kind of change a pricing or risk system cannot tolerate silently. A backtest that passes at -O2 but diverges at -Ofast is not a bug in your code; it is the flag doing what it says. Never enable relaxed floating-point flags on numerical trading code without explicitly validating the results.
Where it shows up
In interviews, this comes up as "why is my debug build so slow" or "how would you speed up this loop without changing the algorithm" — the expected answer includes flags, not just code changes. In production trading systems, build flags are part of the deployment config, reviewed and pinned like any other infrastructure setting, because an accidental flag change can shift both latency and numerical output of every strategy that ships in that binary.
Practice in interviews
Further reading
- GCC documentation, Optimize Options
- Agner Fog, Optimizing Software in C++