Skip to content

/flags/rust/rustc-c-opt-level

rustc -C opt-level and -O: optimization levels 0-3, s and z

verified onenvironmentlocal · windows 10.0.26200 arm64 · GNU bash 5.3.15 (Git Bash) · rustc 1.98.1 (48a229cea 2026-09-01)updated
tool
Rust
command
rustc
flag
-C opt-level=<0|1|2|3|s|z>, -O
default
0 for plain rustc (-O means 2); Cargo: 0 in dev, 3 in release

rustc -C opt-level=3 main.rs compiles with full optimization; -O is a shortcut for -C opt-level=2. Plain rustc uses level 0, no optimization, which is why a single file compiled by hand runs much slower than a Cargo release build. The level also changes behaviour: at level 0 debug assertions and integer overflow checks are enabled, at higher levels they are off unless you turn them on with -C debug-assertions and -C overflow-checks.

Same code, different levels

The program adds 1 to a u8 that holds 255:

bash
cat > ov.rs <<'EOF'
fn main() {
    let x: u8 = std::env::args().count() as u8 + 254;
    let y = x + 1;
    println!("y = {y}, debug_assertions = {}", cfg!(debug_assertions));
}
EOF
echo "== default (opt-level 0)"
rustc ov.rs -o ov0 && ./ov0 2>&1 | sed -E "s/^thread 'main' \([0-9]+\)/thread 'main'/"; echo "exit: ${PIPESTATUS[0]}"
echo "== -C opt-level=3"
rustc -C opt-level=3 ov.rs -o ov3 && ./ov3
echo "== -O"
rustc -O ov.rs -o ovO && ./ovO
echo "== -C opt-level=3 -C overflow-checks=on"
rustc -C opt-level=3 -C overflow-checks=on ov.rs -o ovc && ./ovc 2>&1 | sed -E "s/^thread 'main' \([0-9]+\)/thread 'main'/"; echo "exit: ${PIPESTATUS[0]}"
echo "== invalid level"
rustc -C opt-level=4 ov.rs; echo "exit: $?"

Output

== default (opt-level 0)

thread 'main' panicked at ov.rs:3:13:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit: 101
== -C opt-level=3
y = 0, debug_assertions = false
== -O
y = 0, debug_assertions = false
== -C opt-level=3 -C overflow-checks=on

thread 'main' panicked at ov.rs:3:13:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit: 101
== invalid level
error: optimization level needs to be between 0-3, s or z (instead was `4`)

exit: 1

At level 0 the overflow panics with attempt to add with overflow and exit code 101. Optimized builds wrap to 0 and report debug_assertions = false; adding -C overflow-checks=on brings the panic back. (The sed only removes the thread number that recent Rust versions print in panic messages.)

Levels

ValueMeaningTypical use
0No optimizations; debug assertions and overflow checks on by defaultDefault of rustc and of Cargo dev builds
1Basic optimizationsFaster debug builds that are still debuggable
2Some optimizations (same as -O)Good speed with shorter compile times than 3
3All optimizationsCargo release profile default
sOptimize for binary sizeEmbedded, WebAssembly
zOptimize for size and also turn off loop vectorizationSmallest code

Values outside this list are rejected: optimization level needs to be between 0-3, s or z.

With Cargo

Cargo passes -C opt-level from the active profile, so you rarely type it there:

Cargo.toml
[profile.dev]
opt-level = 1            # faster debug binaries

[profile.dev.package."*"]
opt-level = 3            # optimize dependencies even in dev builds

[profile.release]
opt-level = 3            # default
overflow-checks = true   # keep overflow panics in release

cargo build --release selects the release profile (see cargo build --release). For one-off builds you can also pass flags through RUSTFLAGS="-C opt-level=2" or cargo rustc --release -- -C opt-level=s, but profiles are easier to review.

Pitfalls

  • Benchmarks of rustc file.rs binaries are meaningless: add -O or -C opt-level=3.
  • Code that relies on wrapping overflow should say so with wrapping_add or std::num::Wrapping; otherwise it panics in debug builds and wraps in release builds.
  • opt-level alone does not strip debug info or symbols; use -C debuginfo=0 and -C strip=symbols (or the debug and strip profile keys) to shrink the binary further.

FAQ

Is rustc -O the same as cargo build --release?

Not exactly. -O is -C opt-level=2, while Cargo's release profile uses opt-level 3 by default. Both turn off debug assertions and overflow checks unless you enable them.

Why does my program panic with attempt to add with overflow only in debug builds?

Overflow checks are on by default only when debug assertions are on, which is the case at opt-level 0. In optimized builds the addition wraps silently. Use checked_add, wrapping_add or saturating_add to make the behaviour explicit.

Which level gives the smallest binary?

opt-level=z optimizes for size and also turns off loop vectorization; s is a milder size optimization. Combine with -C strip=symbols, -C panic=abort and LTO for small binaries, and measure: for tiny programs the difference may be zero.

Sources