/flags/c/gcc-o2-optimization-levels
gcc -O2 vs -O0, -O3, -Os, -Og: C optimization levels explained
- tool
- C
- command
- gcc
- flag
- -O0, -O1, -O2, -O3, -Os, -Oz, -Og, -Ofast
- default
- -O0 (no optimization)
gcc -O2 main.c compiles with the standard set of optimizations; without any -O option GCC and Clang use -O0, which compiles fast and keeps the code easy to debug, but produces much slower programs. -O3 adds more aggressive transformations, -Os/-Oz optimize for size, -Og optimizes while keeping debugging usable, and -Ofast additionally breaks strict standard compliance for floating-point math. If several -O options are given, the last one wins.
What changes, made visible
The example uses Clang 20 from the Zig 0.15.1 toolchain (installed from PyPI with uv); cc below is that compiler, and GCC accepts the same -O options:
cc() { uv run -q --no-project --python 3.13 --with ziglang==0.15.1 python -m ziglang cc -fno-color-diagnostics "$@"; }
cat > sum.c <<'EOF'
int sum_to_1000(void) {
int s = 0;
for (int i = 0; i < 1000; i++) s += i;
return s;
}
EOF
echo "== -O0: the loop is still there (jump instructions)"
cc -target x86_64-linux-gnu -O0 -S sum.c -o sum-O0.s 2>/dev/null
grep -E '^\s+j[a-z]+\s' sum-O0.s
echo "== -O2: the result is computed at compile time"
cc -target x86_64-linux-gnu -O2 -S sum.c -o sum-O2.s 2>/dev/null
grep -E '^\s+(movl|retq|j[a-z]+)\s' sum-O2.s
echo "== predefined macros"
for o in -O0 -O2 -Os; do printf '%s: ' "$o"; echo | cc $o -dM -E - | grep -E '__OPTIMIZE(_SIZE)?__|__NO_INLINE__' | awk '{print $2}' | sort | tr '\n' ' '; echo; done
echo "== unsupported level"
cc -O9 -c sum.c -o sum.o; echo "exit: $?"Output
== -O0: the loop is still there (jump instructions) jge .LBB0_8 jne .LBB0_4 jne .LBB0_7 jmp .LBB0_1 == -O2: the result is computed at compile time movl $499500, %eax # imm = 0x79F2C == predefined macros -O0: __NO_INLINE__ -O2: __OPTIMIZE__ -Os: __OPTIMIZE_SIZE__ __OPTIMIZE__ == unsupported level warning: optimization level '-O9' is not supported; using '-O3' instead 1 warning generated. exit: 0
-S writes assembly instead of an object file; -target x86_64-linux-gnu makes the listing the same on every machine. At -O0 the function still contains its loop, visible as conditional jumps. At -O2 the whole loop is gone and the function just returns the constant 499500. The macros __OPTIMIZE__ and __OPTIMIZE_SIZE__ let code detect the level; -O0 defines __NO_INLINE__ instead. Clang maps an unknown level such as -O9 to -O3 with a warning.
Levels
| Level | What it does | Use it for |
|---|---|---|
| -O0 | No optimization; every variable lives in memory, fastest compile | Default; debugging with gdb or lldb |
| -Og | Optimizations that do not hurt debugging (GCC 4.8+; Clang treats it like -O1) | Edit-compile-debug cycle |
| -O1 (-O) | Basic optimizations without large compile-time cost | Quick builds that are not too slow |
| -O2 | Nearly all optimizations that do not trade size for speed | Release builds (common default) |
| -O3 | Adds more inlining, loop vectorization and unrolling | Hot numeric code, after benchmarking |
| -Os | Like -O2 but avoids optimizations that grow the code | Embedded, small binaries |
| -Oz | Optimize for size even more aggressively (Clang; GCC 12+) | Smallest code |
| -Ofast | -O3 plus -ffast-math and other non-conforming options | Only when IEEE floating-point rules do not matter |
gcc -Q --help=optimizers -O2 lists which individual -f options a level turns on for your GCC version.
Pitfalls
- Undefined behaviour surfaces at higher levels. Code that "works" at
-O0can crash or loop forever at-O2because the optimizer relies on the C rules. A segmentation fault that appears only in release builds is a typical sign; reproduce it with-O0 -g -fsanitize=address,undefined. - More warnings with optimization. Some GCC warnings, such as
-Wmaybe-uninitialized, depend on the analysis done by the optimizer and appear only at-O1or higher; keep -Wall -Wextra on in optimized builds. -O3is not always faster. Larger code can be slower because of instruction cache pressure; measure.-Ofastchanges results. It allows reordering floating-point operations and assumes no NaN or infinity, so numerical results can differ from-O2.NDEBUGis separate. Optimization does not disableassert(); add-DNDEBUG(see gcc -D).
FAQ
Which optimization level should I use for release builds?
-O2 is the usual default for distributions and most projects: it enables nearly all optimizations that do not trade size for speed. Try -O3 only if a benchmark shows it is faster for your code, and -Os or -Oz for size-constrained targets.
Why does my program crash only with -O2?
Usually because the code has undefined behaviour, such as reading an uninitialized variable, signed integer overflow or an out-of-bounds access. The optimizer assumes it never happens, so the bug shows up differently. Build with -O0 -g -fsanitize=address,undefined to find it.
Can I debug an optimized build?
Yes, -g works with any -O level, but variables may be shown as optimized out and lines can execute out of order. -Og is designed as a compromise: it optimizes only in ways that keep debugging usable.