Intuition The one core idea
A compiler flag is a knob you turn to change how your source code becomes machine code — trading speed against debuggability, and buying safety by asking the compiler to add checks. Everything on the parent page is just three families of knobs (speed, warnings, runtime checks) plus the single rule that makes them legal: the machine code must behave the same as your correct program .
Before you can read the parent note fluently, you need a small pile of words and pictures. This page builds each one from nothing, in the order that each depends on the last. If a term appeared on the parent page and you weren't 100% sure what it meant — it is defined here.
Source code is the text you write — a .c or .cpp file. It is for humans. The CPU cannot run it directly.
Machine code is a long list of tiny numeric instructions the CPU actually executes ("load this number", "add these two", "jump back"). It is for the machine, not for you.
Look at the top row of the figure below: the same idea (s = a + b) exists as human text on the left and as CPU instructions on the right. The compiler is the arrow between them.
Intuition Why this matters for flags
Translation is not unique. There are many correct lists of machine instructions that all compute a + b. A flag is how you tell the compiler which of those translations you want.
See the Compilation Pipeline (preprocess-compile-assemble-link) for the full journey; here we only need "text in, instructions out".
-Osomething means
On the command line you run the compiler like gcc -O2 myfile.c. Each word starting with a dash - is a flag — a setting. Reading them:
-O2 → the letter O (for O ptimize) followed by a level number 2.
-Wall → W for W arning, then the group name all.
-fsanitize=address → -f introduces a compiler feature ; sanitize=address names it.
So -, O, W, f are not maths — they are just letters chosen as abbreviations.
Common mistake "The number after
-O is a quantity I can pick freely, like -O7."
Reality: only -O0, -O1, -O2, -O3, -Os, -Og, -Ofast exist. The digit is a label for a preset bundle , not a dial you can crank to any value.
To understand why -O0 is slow and -O2 is fast, you need one picture of where a number lives while the program runs.
A register is one of a tiny handful (about 16) of ultra-fast slots inside the CPU itself . Doing arithmetic requires the numbers to be in registers. Reaching a register is far faster than reaching memory.
The figure below draws the CPU with its few fast registers on the left and the big slow memory shelf on the right. An arrow crossing between them is a load (memory→register) or store (register→memory) — and every such crossing costs time.
Intuition Why the parent's
-O0 example is slow
At -O0 the compiler leaves your variables s and i in the slow memory boxes and reloads them every loop iteration — so each pass pays the crossing cost. At -O2 it keeps them in fast registers, and the crossings almost vanish. That single choice — box vs. slot — is most of the speed difference.
The consequences of code size (from -O3) are about the CPU Caches and Instruction Cache ; the "do 8 additions at once" trick is SIMD and Auto-Vectorization .
Warnings and sanitizers are wildly different tools, and the only thing that separates them is when they act.
Compile time is while the compiler is translating — before the program has ever run. Anything found here is found by reading your code.
Run time is while the finished program is executing on real data. Problems here appear only when the buggy line actually runs.
Intuition This split IS the warnings-vs-sanitizers split
A warning (-Wall) is a compile-time guess: "reading this line, you probably made a mistake." Free — no running needed.
A sanitizer (-fsanitize=...) is a run-time detective: it makes the running program watch itself and shout the moment it does something illegal.
This is the single most important concept the parent page leans on, so it gets its own picture.
Definition Undefined Behaviour
Undefined Behaviour (UB) is when your program does something the C/C++ language rulebook says is illegal , so the rulebook promises nothing about what happens next. Examples: reading past the end of an array, reading a variable you never set, adding two integers so large the result overflows.
Intuition The "as-if" contract and why the optimizer trusts you
The rulebook lets the compiler produce any machine code as if it ran your program correctly — this is the as-if rule . The optimizer is allowed to assume you never trigger UB . So if you do trigger it, the optimizer's assumption is false, and the "faster" code can behave completely differently from the slow code.
The figure below shows two roads from the same source. On the safe road (correct program) -O0 and -O2 reach the same destination. On the UB road, the two levels split apart — same source, different answers — which is exactly the parent's "crashes at -O2, works at -O0" mystery.
Common mistake "If two things divide, the compiler is broken."
Reality: the divergence is your UB, not a compiler bug. That is why the parent recommends sanitizers — dynamic detectives that catch UB in the act.
Go deeper on this in Undefined Behaviour in C/C++ .
The parent's ASan explanation uses two words worth a picture.
Definition Shadow memory and redzone
A redzone is a strip of "forbidden" bytes ASan places right around each of your arrays. Stepping into it means you went out of bounds.
Shadow memory is a separate bookkeeping table: for every 8 of your bytes it stores 1 note saying "OK to touch?" or "poisoned — forbidden". Every load/store is rewritten to first peek at this note.
Picture a valid array in green with red "do not enter" strips glued on both ends; the shadow table is a little ledger marking each region OK or POISON. A write into red is caught instantly.
-g and -DNDEBUG
-g tells the compiler to bundle debug info (a map from machine instructions back to your source lines) so a debugger — see Debugging with GDB and -g — can show you variable names and line numbers.
-DNDEBUG D efines the name NDEBUG, which switches off assert() checks in release builds.
CFLAGS / LDFLAGS
These are just lists of flags stored in a build file. CFLAGS are handed to the compiler; LDFLAGS are handed to the linker (the step that glues object files and libraries together). Sanitizers must appear in both — see Makefiles and CFLAGS/LDFLAGS . Enforced automatically in Continuous Integration pipelines .
optimization levels O0 to O3
5.3.11 GCC and Clang flags
Each foundation on the left is a word you now own; together they let you read every callout on the parent topic .
Cover the right side and test yourself.
What does a compiler do, in five words? Turns source text into machine code.
Where does arithmetic actually happen — memory or registers? Registers (the tiny fast slots inside the CPU).
Why is a load/store between memory and register costly? Registers are inside the CPU and fast; memory is off to the side and slow, so crossing takes time.
Compile time vs run time — one-line difference? Compile time is while translating (no run yet); run time is while the finished program executes.
Warnings are static or dynamic? Static — found by reading the code at compile time, no run needed.
Sanitizers are static or dynamic? Dynamic — the running program watches itself at run time.
What is Undefined Behaviour? An illegal action the language rulebook leaves unspecified, so anything may happen.
Why can UB make -O2 differ from -O0? The optimizer is allowed to assume UB never happens; if it does, that assumption breaks and the code can diverge.
What is a redzone in ASan? A forbidden strip of poisoned bytes around each array; touching it means out-of-bounds.
What is shadow memory? A side table storing, per 8 bytes, whether that memory is OK or poisoned to touch.
What does -g add and who uses it? Debug info mapping instructions back to source lines; used by the debugger.
Why must -fsanitize go in both CFLAGS and LDFLAGS? The compiler instruments the code and the linker must attach the sanitizer runtime library.