Skip to content

/flags/c/gcc-o2-optimization-levels

gcc -O2 vs -O0, -O3, -Os, -Og: C optimization levels explained

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash)updated
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:

bash
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

LevelWhat it doesUse it for
-O0No optimization; every variable lives in memory, fastest compileDefault; debugging with gdb or lldb
-OgOptimizations 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 costQuick builds that are not too slow
-O2Nearly all optimizations that do not trade size for speedRelease builds (common default)
-O3Adds more inlining, loop vectorization and unrollingHot numeric code, after benchmarking
-OsLike -O2 but avoids optimizations that grow the codeEmbedded, small binaries
-OzOptimize for size even more aggressively (Clang; GCC 12+)Smallest code
-Ofast-O3 plus -ffast-math and other non-conforming optionsOnly 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 -O0 can crash or loop forever at -O2 because 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 -O1 or higher; keep -Wall -Wextra on in optimized builds.
  • -O3 is not always faster. Larger code can be slower because of instruction cache pressure; measure.
  • -Ofast changes results. It allows reordering floating-point operations and assumes no NaN or infinity, so numerical results can differ from -O2.
  • NDEBUG is separate. Optimization does not disable assert(); 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.

Sources