Worked examples — GCC - Clang flags — optimization (-O0 to -O3, -Os), warnings (-Wall, -Wextra), sanitizers
This page walks through every kind of situation the optimization / warning / sanitizer flags can throw at you. We first lay out a matrix of case-classes, then solve one concrete example per cell. Whenever a "same source, different flag" behaviour appears, we predict it, run it in our head, and verify with the real integer arithmetic C uses.
Parent: GCC/Clang Flags (topic note). If a term slips past undefined, it is built from zero right here.
The scenario matrix
Every cell below is a class of situation this topic can produce. The examples that follow are tagged with the cell they cover.
| Cell | Case class | What makes it tricky | Example |
|---|---|---|---|
| A | Optimization changes speed but NOT result | must prove result identical | Ex 1 |
| B | Signed-vs-unsigned sign boundary (the "zero crossing") | wrap-around at | Ex 2 |
| C | Undefined behaviour: works at -O0, breaks at -O2 |
UB contract voided | Ex 3 |
| D | Degenerate / zero input (empty loop, n = 0) |
limiting case, no iterations | Ex 4 |
| E | Heap out-of-bounds — the redzone catch (ASan) | one byte past the edge | Ex 5 |
| F | Signed integer overflow at the max value (UBSan) | arithmetic limit | Ex 6 |
| G | Real-world word problem: choosing a flag set | translate goal → flags | Ex 7 |
| H | Exam twist: -Os vs -O3 size tradeoff |
size vs speed, both directions | Ex 8 |
| I | Link-step degenerate failure (sanitizer forgotten at link) | flag in one place only | Ex 9 |
We need three tiny pieces of vocabulary before any example. Each is defined before first use.
See also Undefined Behaviour in C/C++ and CPU Caches and Instruction Cache for the deeper stories.
Ex 1 — Cell A · Optimization changes speed, not result
Look at figure s01: the top track is -O0 (load → add → store, every lap touches memory), the bottom track is -O3 (the value lives in a register the whole time, or the loop vanishes into a constant).

- Compute the result by hand. . Why this step? The as-if rule only lets the optimizer change code that leaves the observable result untouched — so we must first pin down that result. .
-O0version.sandilive in memory; each lap does loads, add, stores. Five laps → 5 stores. Why this step? At-O0the debugger must be able to readsat any breakpoint, so it stays in memory. This is slow but faithful.-O3version. The compiler seesnis the constant5and every added value is known, so it may constant-fold the entire loop intos = 10;. Why this step? Since nothing abouts's final value depends on runtime input, the whole loop is a constant. Deleting it does not change the observable result — legal.
Verify: . Both binaries print
10. Same answer, different instructions — exactly the invariant the topic note calls "same observable behaviour, different machine code."
Ex 2 — Cell B · The signed/unsigned zero boundary
- Find the type of
n - 10.nisunsigned;10is promoted tounsigned, so the subtraction is unsigned. Why this step? The comparison< 0behaves completely differently for signed vs unsigned operands — the type decides everything. - Do unsigned arithmetic. Unsigned math wraps modulo . So . Why this step? Unsigned values can never be negative; instead of going below zero they wrap around to a huge positive number. That is the "zero boundary" of this cell.
- Evaluate the test. is false.
"neg"never prints. Why this step? An unsigned number is never< 0, so the branch is dead — a silent logic bug. - The warning.
-Wall(via-Wtype-limits/-Wsign-compare) flags "comparison is always false due to limited range." Why this step? A static detector caught it with zero runtime — the 80/20 win the topic note advertises.
Verify: negative, so the test is
False. Fix:if ((int)n - 10 < 0)printsnegbecause now the subtraction is signed and equals .
Ex 3 — Cell C · Works at -O0, crashes at -O2 (UB)
- Spot the uninitialized read. When
p == NULL, the branchx = *pis skipped, soxis never assigned. Reading it is undefined behaviour. Why this step? UB is the hinge of this whole cell; identify it first. -O0accident. At-O0,xoccupies a stack slot that happens to hold some garbage;x + 1returns garbage-plus-one — but does not crash. "Works" by luck. Why this step? No optimization means the naive garbage is just used as-is — the bug hides.-O2assumption. The optimizer is allowed to assume UB never happens, so it may conclude "xis always read, thereforepwas non-null, therefore delete theif (p)check." Nowf(NULL)dereferences null → crash. Why this step? This is the deep contract: legal optimization + your UB = "miscompile." The bug is in the code, not the compiler.- Detect it.
-O1 -g -fsanitize=address,undefinedreports the uninitialized/null use with a stack trace.
Verify: the correct value if
ppoints at41is . Withp = NULLthere is no defined answer at all — that is precisely the point. See Undefined Behaviour in C/C++.
Ex 4 — Cell D · Degenerate zero input (empty loop)
- Trip the loop. With
n = 0, the testi < 0is false on the first check → the body never runs. Why this step? The degenerate/limiting case: zero iterations. Nothing is summed. - Result.
skeeps its initial value . Why this step? No addition happened, so the identity element stands. - Optimizer. Since the loop body is unreachable when
n == 0, and ifnis a known constant0, the optimizer deletes the loop and the array reads entirely — zero loads ofa. Why this step? Readinga[i]for an empty range would be redundant work; removing it preserves the result.
Verify: empty sum . The array pointer is never dereferenced, so even a null
ais safe whenn = 0— the degenerate case is well-defined precisely because the count guards the access.
Ex 5 — Cell E · Heap out-of-bounds, the redzone catch
Figure s02 shows the 16 valid bytes (blue), the poisoned redzone (red) glued to the end, and the shadow-memory map ASan checks on every store.

- Size the buffer.
4 * sizeof(int)bytes. Valid byte offsets are . Why this step? We must know exactly where the legal region ends to see wherep[4]falls. - Locate
p[4]. Element index sits at byte offset — the first byte past the end. Why this step? Offset is bytes after the last valid byte: it is the very first poisoned byte. ASan's report literally says "0 bytes after." - The shadow check. ASan keeps 1 shadow byte per 8 app bytes. The redzone bytes are marked "poisoned." The instrumented store at offset 16 reads the shadow byte, sees poison, and aborts with a stack trace. Why this step? This is the mechanism the topic note calls the redzone trick, shown concretely at the boundary.
Verify: buffer size bytes,
p[4]offset , overrun bytes after the buffer — matching ASan'sheap-buffer-overflow ... 0 bytes after. Also4 \times 4 = 16shadow region begins exactly here.
Ex 6 — Cell F · Signed overflow at the arithmetic limit (UBSan)
- Identify the limit. For 32-bit
int, the maximum is . Why this step? Overflow is defined relative to this ceiling — the limiting value of the type. - Add one. , which is beyond
INT_MAX. For signed ints this is undefined behaviour (unlike unsigned, which wraps legally). Why this step? The signed/unsigned distinction from Ex 2 returns: unsigned wrap is defined, signed overflow is UB. - UBSan output.
-fsanitize=undefinedprintssigned integer overflow: 2147483647 + 1 cannot be represented in type 'int'. Why this step? A runtime check fires exactly at the arithmetic boundary — no earlier, no later.
Verify: (
INT_MAX) and , which exceeds — so the result is unrepresentable inint. Contrast: ifxwereunsigned, is perfectly legal and defined.
Ex 7 — Cell G · Real-world flag choice
- CI: correctness over speed. Use
-Og -g -Wall -Wextra -fsanitize=address,undefined. Why this step?-Ogkeeps the debugger sane while still being faster than-O0; the two sanitizers plus warnings maximize bugs-caught-per-run — see Continuous Integration pipelines and Debugging with GDB and -g. - Release: speed. Use
-O2 -DNDEBUG. Why this step?-O2is the production default (good bets, no i-cache-thrashing gambles);-DNDEBUGstripsasserts. - Firmware: size. Use
-Os -DNDEBUG. Why this step?-Osis-O2minus the passes that grow the binary — the byte budget wins over raw speed.
Verify: three goals → three sets, each mapping goal to lever. No sanitizer flag appears in the release/firmware sets because ASan costs ~2× time and ~3× memory — CI only. These are exactly the recipes in the topic note's "Putting it together." Store them in your Makefiles and CFLAGS/LDFLAGS.
Ex 8 — Cell H · Exam twist, -Os vs -O3 both directions
- Compare code size to cache budget.
-O3body bytes; i-cache budget bytes. Since , the loop cannot fit — the CPU re-fetches instructions every pass. Why this step? An optimization that grows code is only a win if the grown code still fits the cache; here it does not. -Osbody. Suppose-Oskeeps it at bytes. Since , it fits — no instruction re-fetch stalls. Why this step? Fitting the cache can beat doing "more clever" work that spills out of it.- Conclusion. For this loop
-Os(or-O2) can beat-O3. The optimization "bet" of Ex-note fame did not pay off. Why this step? Directly demolishes the "-O3is always faster" mistake — the number is a bet, not a guarantee.
Verify: (overflows i-cache) and (fits). So the size ordering that matters is
-Osfits,-O3doesn't →-Oscan win. The only honest arbiter is a benchmark: measure, don't assume.
Ex 9 — Cell I · Degenerate build: sanitizer at compile but not link
- Compile step.
-fsanitize=addressinserts calls to ASan runtime functions (like__asan_report_store4) intoa.o. Why this step? Instrumentation emits calls to a library that is not part of your code. - Link step. The final link has no
-fsanitize=address, so the ASan runtime library is not pulled in →undefined reference to __asan_.... Why this step? The linker resolves those emitted calls only when the sanitizer runtime is on the link line. Missing → degenerate failure. - Fix. Pass the flag at both steps (or add it to both
CFLAGSandLDFLAGS). Why this step? ASan is unique among these flags in needing a runtime linked in, unlike optimization flags which are compile-only.
Verify: the missing library causes an
undefined reference, not a runtime crash — a build-time symptom that uniquely fingerprints "sanitizer forgotten at link." Adding-fsanitize=addressto the link line resolves every__asan_*symbol.
Recall Self-check: predict before revealing
With n=5, does if (n-10 < 0) (n unsigned) print "neg"? ::: No — n-10 is unsigned, wraps to , which is never < 0.
p = malloc(4*sizeof(int)); p[4] overruns by how many bytes? ::: bytes after the end — offset is the first poisoned redzone byte.
INT_MAX + 1 for signed int — defined or UB? ::: UB (signed overflow); UBSan reports it. Unsigned would wrap legally.
Empty loop for(i=0;i<0;i++) s+=a[i] gives s = ? ::: ; the body never runs, so the initial value stands.
A loop grows to 520 bytes but the i-cache holds 256 — -O3 or -Os faster? ::: -Os can win; -O3's bigger code overflows the instruction cache.
-fsanitize=address at compile only → what error? ::: Link-time undefined reference to __asan_*; add the flag to the link step too.