5.3.11 · D5Build Systems & Toolchain
Question bank — GCC - Clang flags — optimization (-O0 to -O3, -Os), warnings (-Wall, -Wextra), sanitizers
Before the bank, some anchors so no term is used un-defined:
The three figures below make the invisible machinery visible — refer back to them as you work through the bank.



True or false — justify
TF1 — -Wall turns on all available warnings.
False. It's a curated common-bug subset;
-Wextra, -Wshadow, -Wconversion and others live outside it. The name is historical, not literal.TF2 — Adding -O3 can never make a program run slower than -O2.
False. Aggressive inlining/vectorization can grow code and thrash the instruction cache (see figure s02 and CPU Caches and Instruction Cache); each optimization is a bet that can lose. Benchmark before trusting it.
TF3 — Warnings and errors are the same severity; both stop the build.
False. A warning by default lets the build succeed; only
-Werror promotes warnings to build-stopping errors.TF4 — If a program produces the correct output at -O0, its behaviour is guaranteed correct at -O2.
False.
-O0 can accidentally mask UB. The optimizer is allowed to assume UB never happens, so the same source can break at -O2. Correct-at--O0 proves nothing.TF5 — Sanitizers are a static analysis that runs at compile time.
False. Sanitizers are runtime checkers — they add instrumentation that fires only when the buggy path actually executes on real inputs. Warnings are the static tool.
TF6 — -Os produces a strictly slower binary than -O2.
Mostly-but-not-always false.
-Os disables size-bloating passes, so it's usually a bit slower — but a smaller binary can fit the i-cache better and occasionally win on cache-bound code.TF7 — Turning on -O3 changes what your correct program computes.
False (for a correct program). The as-if rule forbids changing observable results. It only changes how fast/small the machine code is. If the result changes, your program had UB.
TF8 — You only need to pass -fsanitize=address at the compile step.
False. AddressSanitizer (ASan) needs its runtime library linked in; you must pass the flag at the link step too, or you get undefined-reference errors. Put it in both
CFLAGS and LDFLAGS (see Makefiles and CFLAGS/LDFLAGS).TF9 — -Ofast is just -O3 with a nicer name.
False.
-Ofast adds -ffast-math, which breaks strict IEEE float semantics (reassociates operations, ignores NaN/inf rules). It can change numeric results — dangerous unless you've verified your maths tolerates it.TF10 — Running under ASan and running the release build have the same performance.
False. ASan costs roughly 2× time and 3× memory because every load/store consults shadow memory (figure s01). It belongs in tests/CI, never production.
Spot the error
SE1 — "My CI uses -O0 -fsanitize=undefined to catch the most bugs."
The idea is fine but the level is off.
-O0 is readable but slow; -O3 may optimize away the very UB you want to catch. The conventional sweet spot is -O1 -g -fsanitize=....SE2 — "I compile release code with -O3 -g -fsanitize=address for max safety and speed."
That combination does compile and run — the compiler allows it — but it's the wrong tool for a release: ASan makes it ~2× slower and heavy
-O3 optimization can muddy the reports. Ship -O2 -DNDEBUG; sanitize separately in CI.SE3 — "unsigned n = 5; if (n - 10 < 0) ... should print because 5−10 is negative."
n - 10 is computed in unsigned arithmetic, so 5u - 10u wraps to a huge positive number and < 0 is always false. -Wall/-Wtype-limits flags it. Fix with signed types or a cast.SE4 — "The optimizer miscompiled my code — it deleted my out-of-bounds check if (p + n < p)."
Not a miscompile. Pointer overflow is UB, so the compiler assumes
p + n never wraps and folds the check to false. The check itself was the bug; use size comparisons, not pointer wraparound.SE5 — "I added -Wall -Wextra and got zero warnings, so my code is memory-safe."
Warnings are static heuristics; they can't see runtime memory bugs like use-after-free or heap overflow. Zero warnings ≠ memory-safe. Add ASan to actually exercise the paths.
SE6 — "-Werror broke my build across compiler versions for no reason."
Expected, not a bug. Newer compilers add new warnings, so
-Werror can fail on previously-clean code. That's the tradeoff of a zero-warning gate; pin versions or scope -Werror to specific warnings in CI.SE7 — "I benchmarked at -O0 and picked the fastest algorithm."
-O0 keeps variables in memory and adds load/store traffic every iteration, so its timings don't reflect the optimized reality. Always benchmark at the level you'll ship (-O2/-O3).Why questions
WHY1 — Why is -O0 slow even for tiny loops?
At
-O0 every variable (loop counter, accumulator) lives in stack memory so a debugger can read/modify it at any breakpoint. Each iteration pays load+store traffic instead of using registers.WHY2 — Why is optimization even legal if it reorders and deletes code?
Because of the as-if rule: only observable behaviour must be preserved. Register allocation, inlining, and loop-invariant motion don't change results, so they're permitted.
WHY3 — Why do sanitizers exist at all if warnings already find bugs?
Warnings catch statically visible mistakes; sanitizers catch runtime ones — buffer overflows, use-after-free, signed overflow — that no static pass can prove in general. They're complementary, not redundant.
WHY4 — Why does ASan use shadow memory instead of just checking every pointer?
A pointer alone doesn't know its bounds. ASan maps every 8 app bytes to 1 shadow byte recording "addressable?", places poisoned redzones around allocations, and instruments each access to consult the shadow (figure s01) — an (constant-time, input-size-independent) check per memory op.
WHY5 — Why is -Wall -Wextra called "80/20 gold"?
It's the highest bug-found-per-second tool: a compile-time, zero-runtime-cost detector of the most common mistakes (uninitialized vars, sign-compare, switch fallthrough). No test needs to run.
WHY6 — Why not just always ship the smallest binary with -Os?
-Os disables passes that trade size for speed (e.g. aggressive vectorization; see SIMD and Auto-Vectorization), so it's typically slower on compute-bound work. Use it only when size is the actual constraint.WHY7 — Why does -Og exist between -O0 and -O2?
-Og optimizes for a good debugging experience: faster than -O0 but keeps line-info and variable locations sane so Debugging with GDB and -g still works. It's the dev/interactive middle ground.WHY8 — Why sanitize in Continuous Integration pipelines rather than on every developer machine only?
CI runs the full test suite on every commit under sanitizers, catching UB before merge in a shared, reproducible environment — no reliance on individuals remembering to run it.
Edge cases
EC1 — What does the -On ladder do for a program that is entirely UB (e.g. always reads uninitialized memory)?
Nothing is guaranteed. Since UB voids the as-if contract, any level may produce any behaviour — including "works" at one level and "crashes" at another. The fix is removing the UB, not tuning the flag.
EC2 — What happens if you pass no -O flag at all?
You get
-O0 — the default is "no optimization," not -O2. Many people assume the compiler optimizes by default; it does not.EC3 — Can -O3 ever change floating-point results even without -ffast-math?
Mostly no — plain
-O3 respects IEEE semantics under the as-if rule. But there are hardware corners: -ffp-contract may fuse a*b+c into a single FMA (fused multiply-add) that rounds once instead of twice, and on legacy x87 the 80-bit extended-precision registers can give results that differ from strict 64-bit doubles. So "identical bits" isn't guaranteed across targets even without -ffast-math.EC4 — Two flags conflict on the command line, e.g. -O0 -O2. What wins?
The last one wins — flags are applied left to right, so
-O0 -O2 gives -O2. This is why build systems put user overrides after defaults in CFLAGS.EC5 — You enable -fsanitize=address,undefined together — is that valid?
Yes; ASan and UBSan compose in one build. But TSan is incompatible with ASan (different memory models), so you can't combine those two — run thread-sanitized builds separately.
EC6 — A warning fires on a line you intended to write (deliberate fallthrough / unused param). What's the clean fix?
Silence it locally and explicitly —
[[fallthrough]], (void)param;, or a targeted pragma — not by dropping -Wall. Turning the whole warning off hides future real bugs.Recall
Recall One-line self-test
If a program crashes at -O2 but runs at -O0, what's your first move — and why? ::: Run -O1 -g -fsanitize=address,undefined. The most likely cause is UB that -O0 accidentally masked; sanitizers pinpoint the real bug rather than blaming the optimizer.