rustc -C opt-level and -O: optimization levels 0-3, s and z
- 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:
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
| Value | Meaning | Typical use |
|---|---|---|
| 0 | No optimizations; debug assertions and overflow checks on by default | Default of rustc and of Cargo dev builds |
| 1 | Basic optimizations | Faster debug builds that are still debuggable |
| 2 | Some optimizations (same as -O) | Good speed with shorter compile times than 3 |
| 3 | All optimizations | Cargo release profile default |
| s | Optimize for binary size | Embedded, WebAssembly |
| z | Optimize for size and also turn off loop vectorization | Smallest 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:
[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 releasecargo 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.rsbinaries are meaningless: add-Oor-C opt-level=3. - Code that relies on wrapping overflow should say so with
wrapping_addorstd::num::Wrapping; otherwise it panics in debug builds and wraps in release builds. opt-levelalone does not strip debug info or symbols; use-C debuginfo=0and-C strip=symbols(or thedebugandstripprofile 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
- doc.rust-lang.orgdoc.rust-lang.org/rustc/codegen-options/index.html#opt-level
- doc.rust-lang.orgdoc.rust-lang.org/rustc/codegen-options/index.html#overflow-checks
- doc.rust-lang.orgdoc.rust-lang.org/rustc/command-line-arguments.html#-o-optimize
- doc.rust-lang.orgdoc.rust-lang.org/cargo/reference/profiles.html