-O0 vs -O2 vs -O3: GCC and Clang Optimization Levels Benchmarked
What -O0, -O1, -O2, -O3, -Os, -Ofast and -march=native really do, measured with GCC and Clang on five C programs: run time, code size and compile time.
By Avinash KoriPublished 10 min read
On this page
Every C and C++ compiler has a dial marked -O. -O0 compiles quickly and keeps the code close to what you wrote; -O2 is what most release builds use; -O3, -Os, -Ofast and -march=native promise more speed or smaller files. Advice about them tends to repeat itself ("-O3 can be slower", "-Ofast is dangerous") without numbers.
So we measured. Five small C programs, built with GCC 11.4 and Clang 14 at seven settings each, timed on the same laptop, along with the size of the generated code and the time it takes to compile. The short version: -O2 is where almost all of the speed comes from, -O3 helps a few loops a lot and some code not at all (or negatively), and -Ofast changes what your program computes.
What each level turns on
The levels are bundles of individual optimization flags. GCC can list them: gcc -Q -O2 --help=optimizers prints every option and whether that level enables it. Counting the enabled ones in GCC 11.4:
| Level | Enabled optimizations (GCC 11.4) | In one line |
|---|---|---|
-O0 | 58 | The default. Every variable lives in memory, every line maps to code. For debugging. |
-Og | 85 | Optimizes only what doesn't hurt debugging. |
-O1 | 97 | Cheap, safe optimizations: registers, constant folding, simple dead-code removal. |
-O2 | 140 | Almost everything that doesn't trade size for speed: inlining, scheduling, loop optimizations. |
-Os | 135 | -O2 minus the optimizations that make code larger. |
-O3 | 155 | -O2 plus aggressive inlining, loop unrolling and (in GCC 11) auto-vectorisation. |
-Ofast | 158 | -O3 plus -ffast-math: floating point that may break IEEE rules. |
(The 58 "enabled at -O0" are mostly settings that are always on, such as how to handle exceptions, not transformations.) Clang uses the same names with its own bundles; it has no equivalent listing, but its levels map closely.
-march=native is a different kind of flag: it lets the compiler use every instruction your CPU has (AVX2 on this laptop), instead of only those every x86-64 CPU has. The program may then crash on an older machine with "illegal instruction".
What -O2 does to a simple loop
#include <stdio.h>
int main(void) {
int x = 0;
for (int i = 0; i < 100; i++) x += i;
printf("%d\n", x);
return 0;
}At -O0, GCC emits the loop as written: a counter in memory, a compare, a jump, about 20 instructions in main. At -O2, the loop is gone. Running gcc -O2 -S shows main loading the constant 4950 and calling printf with it, because the compiler computed the sum while compiling. (On Ubuntu the call is __printf_chk, because the distribution builds with _FORTIFY_SOURCE.)
Loops the compiler can't fully compute still shrink. Summing the numbers from 0 to 3 billion took 9.1 s at -O0 with both compilers, 1.1 s with gcc -O1, and 0.00 s with gcc -O2 and clang -O1, which recognise the arithmetic series and replace the loop with its formula.
The benchmark
Five programs, each printing a checksum so the work can't be optimized away, all checked to print exactly the same result in all 14 builds:
- matmul: 1024 × 1024 matrix multiplication in
float; a loop that vectorises well. - fib: naive recursive Fibonacci of 41; nothing but function calls.
- sieve: Sieve of Eratosthenes up to 50 million; memory-bound.
- nbody: a five-body planet simulation, 10 million steps of
doublearithmetic andsqrt. - crc: bitwise CRC-32 over 64 MB; integer and bit operations with a data dependency.
Machine: Intel Core i5-7200U (2 cores, 4 threads, AVX2), Ubuntu 22.04, GCC 11.4 and Clang 14.0, -std=c17. The laptop was in normal use, so instead of one long run per binary, all 70 binaries ran in turn, 12 rounds, pinned to one CPU core, and the table shows each binary's fastest run (the least disturbed by anything else). Times are in seconds; lower is better.
| Program | Compiler | -O0 | -O1 | -O2 | -O3 | -Os | -Ofast | -O3 -march=native |
|---|---|---|---|---|---|---|---|---|
| matmul | GCC 11 | 3.41 | 0.79 | 0.79 | 0.21 | 1.13 | 0.21 | 0.19 |
| matmul | Clang 14 | 2.74 | 0.62 | 0.28 | 0.28 | 0.28 | 0.27 | 0.21 |
| fib | GCC 11 | 1.50 | 1.03 | 0.50 | 0.49 | 0.83 | 0.50 | 0.49 |
| fib | Clang 14 | 1.24 | 1.04 | 0.62 | 0.63 | 0.60 | 0.53 | 0.63 |
| sieve | GCC 11 | 0.86 | 0.66 | 0.64 | 0.63 | 0.65 | 0.64 | 0.63 |
| sieve | Clang 14 | 0.88 | 0.64 | 0.65 | 0.65 | 0.65 | 0.65 | 0.63 |
| nbody | GCC 11 | 2.79 | 1.04 | 0.95 | 1.50 | 1.01 | 0.84 | 1.41 |
| nbody | Clang 14 | 2.59 | 1.00 | 0.84 | 0.84 | 0.84 | 0.84 | 0.66 |
| crc | GCC 11 | 1.64 | 0.88 | 0.86 | 0.86 | 0.86 | 0.86 | 0.86 |
| crc | Clang 14 | 1.64 | 0.88 | 0.68 | 0.68 | 0.88 | 0.68 | 0.68 |
What the numbers say
-O2does the heavy lifting. Going from-O0to-O2cut the time by 1.3–3.1× for most programs and by 4.3× (GCC) and 9.8× (Clang) for the matrix multiplication. Much of that already happens at-O1: keeping variables in registers instead of memory is the single biggest win.-O3mattered for one program per compiler, in opposite directions. GCC 11 vectorises loops only at-O3, so its matrix multiplication went from 0.79 s to 0.21 s, 3.8× faster. Clang vectorises at-O2and gained nothing more. But GCC's n-body simulation got 58% slower at-O3(0.95 s → 1.50 s), and stayed slow with-march=native.-O3's extra passes are heuristics; for this loop GCC's guess was wrong. Everywhere else,-O3and-O2were within a few percent.- Memory-bound code doesn't care. The sieve spends its time waiting for memory, so every level from
-O1up took 0.63–0.66 s with both compilers. -Osis not "-O2but smaller" for every program. Clang's-Oswas as fast as-O2in four programs. GCC's-Ostook 43% longer on the matrix multiplication and 66% longer on recursive Fibonacci, because it refuses the inlining and unrolling those need.-march=nativehelped floating point. With AVX2 and FMA available, Clang's n-body took 21% less time and its matrix multiplication 25% less than at-O2. Integer code (sieve, CRC, Fibonacci) didn't change.-Ofastgained up to 12% (GCC's n-body, 0.95 s → 0.84 s) and printed the same results here, to nine decimal places. The next section shows why that isn't guaranteed.- The compilers differ more than the levels. At
-O2, Clang took 21% less time than GCC on the CRC loop and 65% less on the matrix multiplication, while GCC took 19% less on Fibonacci. Our GCC vs Clang benchmark found the same pattern: no compiler wins everywhere.
-Ofast changes the answers
-Ofast turns on -ffast-math, which tells the compiler it may assume there are no NaNs or infinities, and that floating-point addition is associative. Both assumptions can change results:
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
double x = argc > 1 ? atof(argv[1]) : -1.0;
double r = sqrt(x); /* sqrt(-1) is NaN */
if (isnan(r)) printf("not a number\n");
else printf("result: %f\n", r);
return 0;
}Built with GCC or Clang at -O2, it prints not a number. At -Ofast, both print:
result: -nanThe compiler deleted the isnan check, because under fast-math a NaN "can't happen". Code that validates input with isnan or isinf, or relies on exact rounding (financial sums, compensated summation, some geometry), must not be built with -Ofast. If you want the vectorisation -Ofast enables, use -O3, and add only the narrower flags you have checked, such as -fno-math-errno.
Undefined behaviour shows up at -O2
Optimizers assume the program has no undefined behaviour. Signed integer overflow is undefined in C, so this "overflow check" may be removed:
#include <limits.h>
#include <stdio.h>
int will_overflow(int x) {
return x + 1 < x; /* meant as an overflow check */
}
int main(void) {
printf("%d\n", will_overflow(INT_MAX));
return 0;
}clang -O0 prints 1: the addition wraps around, as the author expected. clang -O2 prints 0, because "x + 1 < x" is never true for a number that can't overflow, so the function becomes return 0. GCC 11 prints 0 even at -O0, since it simplifies the expression while parsing. A bug that appears only in optimized builds is almost always undefined behaviour like this. The portable check is x == INT_MAX, or __builtin_add_overflow, and -fsanitize=undefined finds the overflow at run time. The GCC vs Clang guide has more examples of undefined behaviour that the two compilers treat differently.
Code size
Total size of the machine code (.text section) of all five programs, from size -A. It includes the C runtime's start-up code, the same few hundred bytes at every level:
-O0 | -O1 | -O2 | -O3 | -Os | -Ofast | |
|---|---|---|---|---|---|---|
| GCC 11.4 | 4,076 B | 3,079 B | 3,728 B | 4,752 B | 2,641 B | 4,979 B |
| Clang 14 | 3,975 B | 3,321 B | 4,159 B | 5,367 B | 2,843 B | 4,921 B |
-Os produced the smallest code with both compilers, about 30% smaller than -O2; -O3 grew it by 27–29%, mostly from unrolled and vectorised loops with their fallback paths. Unoptimized code is larger than -O1, because it keeps every variable in memory. For a microcontroller or a WebAssembly download, those bytes matter; for a desktop program, cache misses from bloated hot loops are the only real cost.
Compile time
Time to compile one file, fastest of 7 runs. The C file is the n-body program (about 40 lines); the C++ file is a short program that includes <regex>, <map>, <unordered_map>, <sstream> and four other standard headers.
-O0 | -O1 | -O2 | -O3 | -Os | -Ofast | |
|---|---|---|---|---|---|---|
| GCC 11, C file | 0.03 s | 0.05 s | 0.08 s | 0.08 s | 0.07 s | 0.08 s |
| Clang 14, C file | 0.05 s | 0.07 s | 0.08 s | 0.09 s | 0.07 s | 0.09 s |
| GCC 11, C++ file | 2.49 s | 2.93 s | 3.91 s | 4.21 s | 3.16 s | 4.22 s |
| Clang 14, C++ file | 1.75 s | 3.47 s | 3.95 s | 4.09 s | 3.28 s | 4.12 s |
For a small C file, every level compiles in well under a tenth of a second. For C++ with heavy templates, -O2 took 57% longer than -O0 with GCC and more than twice as long with Clang, because optimization has to work through all the template code the headers instantiate. That is why debug builds use -O0: in a large C++ project, the difference is minutes per rebuild. -O3 added only 3–8% on top of -O2.
Which -O level should you use?
- Debugging:
-O0 -g, or-Og -gif-O0is too slow to test with. At-O2the debugger shows variables as "optimized out" and steps through lines out of order. - Release builds:
-O2. It got most of the possible speed in every program we tried, it is what distributions and most projects ship, and it is the setting compiler developers test most. - Hot numeric loops: try
-O3(and, for a binary that runs only on your machines,-march=native), then measure. Keep it only where the benchmark says it helps. - Size first:
-Osfor embedded targets and downloads; Clang's-Ozgoes further. -Ofast: only when you have checked that the program doesn't depend on NaN, infinity or exact rounding.
And whatever the level, build with -Wall -Wextra and test with -fsanitize=address,undefined at -O1 or -O2, where undefined behaviour actually shows up.
Common questions
Is -O3 faster than -O2?
Sometimes. In our tests -O3 was much faster than -O2 for GCC on the matrix multiplication (GCC 11 vectorises only at -O3), the same for most other programs, and clearly slower for GCC on the n-body simulation. GCC 12 and later vectorise cheap loops at -O2 too, which narrows the gap. Measure your own program.
Is -Ofast safe?
It is safe for code that never sees NaN or infinity and doesn't need bit-exact floating-point results. It is unsafe for anything that checks isnan, relies on IEEE rounding, or compares results across machines. Clang 19 and later warn that -Ofast is deprecated and suggest spelling out -O3 -ffast-math instead.
What is the default optimization level?
-O0 for both GCC and Clang when you pass no -O flag. Build systems usually add their own: CMake's Release uses -O3, RelWithDebInfo uses -O2 -g.
Does the optimization level change my program's output?
For a correct program, no, except under -Ofast. If output changes between -O0 and -O2, the program almost certainly has undefined behaviour: an uninitialized variable, signed overflow, or an out-of-bounds access.
Which level does the online compiler use?
The online C Compiler and the C++ Compiler build with Clang at -O2 with -Wall, so programs behave like a typical release build. To compare two outputs, paste them into the Text Diff tool.
Summary
-O2 turned programs that took 0.9–3.4 s at -O0 into ones that took 0.3–1 s, and it is the right default. -O3 and -march=native pay off for vectorisable loops and can cost speed elsewhere, -Os saves about 30% of code for a small loss of speed, and -Ofast is only for code that tolerates fast-math. The best level for a hot loop is the one your own benchmark picks, built with the compiler version you ship.

Written by
Avinash Kori
Founder of Devtoolry
Avinash is a full-stack web developer from Surat, India, with eight years of experience in PHP, Laravel and Angular. He built Devtoolry so developers can solve everyday problems quickly with free tools that run entirely in the browser, and writes practical guides on the formats, encodings and techniques behind them.
Try it yourself
The free tools used in this article. They run in your browser, so nothing you paste is uploaded.
C Compiler
Compile and run C code online with Clang 21, a GCC-compatible compiler: pick C89 (ANSI C), C99, C11, C17 or C23 and add stdin input. Your code stays private.
C++ Compiler
Run C++ code online for free: compile C++20 with Clang 21 and libc++, type stdin input and see the output. It runs in your browser, so your code stays private.
Text & Code Diff
Compare two texts or code files online side by side. Additions, deletions and modified lines are highlighted with inline word changes. Runs in your browser.