Skip to content
Devtoolry
Programming Languages

-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

Bar chart: the same matrix multiplication built with GCC at -O0, -O1, -O2, -O3, -Os and -Ofast, from 3.4 seconds down to 0.2 seconds

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:

LevelEnabled optimizations (GCC 11.4)In one line
-O058The default. Every variable lives in memory, every line maps to code. For debugging.
-Og85Optimizes only what doesn't hurt debugging.
-O197Cheap, safe optimizations: registers, constant folding, simple dead-code removal.
-O2140Almost everything that doesn't trade size for speed: inlining, scheduling, loop optimizations.
-Os135-O2 minus the optimizations that make code larger.
-O3155-O2 plus aggressive inlining, loop unrolling and (in GCC 11) auto-vectorisation.
-Ofast158-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

C
#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 double arithmetic and sqrt.
  • 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.

ProgramCompiler-O0-O1-O2-O3-Os-Ofast-O3 -march=native
matmulGCC 113.410.790.790.211.130.210.19
matmulClang 142.740.620.280.280.280.270.21
fibGCC 111.501.030.500.490.830.500.49
fibClang 141.241.040.620.630.600.530.63
sieveGCC 110.860.660.640.630.650.640.63
sieveClang 140.880.640.650.650.650.650.63
nbodyGCC 112.791.040.951.501.010.841.41
nbodyClang 142.591.000.840.840.840.840.66
crcGCC 111.640.880.860.860.860.860.86
crcClang 141.640.880.680.680.880.680.68

What the numbers say

  • -O2 does the heavy lifting. Going from -O0 to -O2 cut 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.
  • -O3 mattered 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 -O2 and 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, -O3 and -O2 were within a few percent.
  • Memory-bound code doesn't care. The sieve spends its time waiting for memory, so every level from -O1 up took 0.63–0.66 s with both compilers.
  • -Os is not "-O2 but smaller" for every program. Clang's -Os was as fast as -O2 in four programs. GCC's -Os took 43% longer on the matrix multiplication and 66% longer on recursive Fibonacci, because it refuses the inlining and unrolling those need.
  • -march=native helped 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.
  • -Ofast gained 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:

C
#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:

Output
result: -nan

The 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:

C
#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.44,076 B3,079 B3,728 B4,752 B2,641 B4,979 B
Clang 143,975 B3,321 B4,159 B5,367 B2,843 B4,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 file0.03 s0.05 s0.08 s0.08 s0.07 s0.08 s
Clang 14, C file0.05 s0.07 s0.08 s0.09 s0.07 s0.09 s
GCC 11, C++ file2.49 s2.93 s3.91 s4.21 s3.16 s4.22 s
Clang 14, C++ file1.75 s3.47 s3.95 s4.09 s3.28 s4.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 -g if -O0 is too slow to test with. At -O2 the 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: -Os for embedded targets and downloads; Clang's -Oz goes 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.