Debug, release and profiles

Cargo builds with a profile, and the two that matter are dev and release. They differ in optimisation, in debug information, in whether arithmetic is checked, and in whether a panic unwinds. One source file can behave differently under the two, and knowing which difference is which is the point of this lesson.

The program stays the same while the profile changes: an addition that overflows a u8, a debug assertion, and a panic. Each output below is real, and the release build is the one with optimisation, which is why one number appears where the debug build stops.

Debug against release

The dev profile is the default for cargo build and cargo run: no optimisation, debug information and overflow checks. The release profile is chosen with --release: optimisation, no debug information and no overflow checks, so an overflow wraps.

The program adds one to the largest u8. The debug build stops with a panic that names the operation, and the release build prints the wrapped zero.

fn main() { let mut value: u8 = 255; value += 1; println!("{value}"); }

cargo run
cargo run --release

   Compiling overflow v0.1.0 (/tmp/rw7/L084/overflow)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/overflow`

thread 'main' (4493) panicked at src/main.rs:3:5:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
   Compiling overflow v0.1.0 (/tmp/rw7/L084/overflow)
    Finished `release` profile [optimized] target(s) in 5.59s
     Running `target/release/overflow`
0

The two binaries live side by side in target/debug and target/release, and neither replaces the other. The wrapping is defined behaviour and not a licence to ignore it: a program that relies on it writes wrapping_add and gets the same result in both profiles. The lesson on numbers has the whole family of checked, wrapping and saturating calls.

debug_assert! in each build

debug_assert! is compiled only when debug assertions are on, which the dev profile has and the release profile does not. It is the place for a check that is too expensive for production, and it disappears with its cost.

The program asserts something false and prints a line. In the debug build the assertion stops the program; in the release build the line is printed and the check is not in the binary at all.

fn main() { debug_assert!(1 + 1 == 3, "the check runs in this build"); println!("after the check"); }

cargo run
cargo run --release

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.01s
     Running `target/debug/overflow`

thread 'main' (4546) panicked at src/main.rs:2:5:
the check runs in this build
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
   Compiling overflow v0.1.0 (/tmp/rw7/L084/overflow)
    Finished `release` profile [optimized] target(s) in 0.78s
     Running `target/release/overflow`
after the check

The message is the one written in the macro, which is why the second argument is worth a sentence: it is the only text a reader sees when the check fails in a test run. The lesson on assertions compares it with assert!, which stays in every build.

A profile setting

A profile is a table in the manifest, and any of its settings can be changed. Turning overflow-checks back on for release is the common one for a program that must not wrap silently, and it costs one comparison per arithmetic operation.

The manifest below adds the setting, and the same addition now panics in the release build as well.

[profile.release]
overflow-checks = true

cargo run --release

   Compiling overflow v0.1.0 (/tmp/rw7/L084/overflow)
    Finished `release` profile [optimized] target(s) in 0.39s
     Running `target/release/overflow`

thread 'main' (4605) panicked at src/main.rs:3:5:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The other settings of the table are opt-level from zero to three, lto for link-time optimisation, codegen-units, strip and panic. A profile can also be defined for a purpose of its own, such as [profile.profiling] with optimisation and debug information together, and selected with --profile profiling.

panic = "abort"

The default panic strategy unwinds the stack and runs the destructors on the way out. With panic = "abort" a panic ends the process at once, which makes the binary smaller and the failure faster, and gives a different exit status: 134 from the abort signal instead of 101 from the runtime.

The setting goes in the profile, and the same overflow now aborts. The shell reports the signal after the panic message.

[profile.release]
overflow-checks = true
panic = "abort"

cargo run --release
echo "exit=$?"

   Compiling overflow v0.1.0 (/tmp/rw7/L084/overflow)
    Finished `release` profile [optimized] target(s) in 0.31s
     Running `target/release/overflow`

thread 'main' (4623) panicked at src/main.rs:3:5:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Aborted (core dumped)
exit=134

The message still appears because the panic handler runs before the abort, and RUST_BACKTRACE still works. What is lost is the unwinding, so a destructor does not run on the panic path and catch_unwind cannot catch anything. The lesson on panics shows the unwinding behaviour that this setting removes.

Exercises

  1. Build the same program with -C opt-level=0 and -C opt-level=3 under rustc and compare the sizes of the two binaries.
  2. Add a [profile.dev] entry that turns overflow checks off and predict the output before you run it.
  3. Put a debug_assert! in a loop and measure the release build with and without it.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Which profile does cargo run use by default?

    Answer

    dev, with no optimisation and overflow checks on

    The dev profile is the default, and the release profile needs the flag.

  2. What does an overflowing u8 addition do in a release build without a profile setting?

    Answer

    It wraps around

    Overflow checks are off in release by default, so the arithmetic wraps.

  3. When is debug_assert! compiled in?

    Answer

    When debug assertions are on, as in the dev profile

    The macro follows the debug-assertions setting of the profile.

  4. What is the exit status of a program that aborts on a panic?

    Answer

    134, from the abort signal

    The process dies from SIGABRT, which the shell reports as 128 plus 6.

  5. What does this program print? fn main() { let value: u8 = 250; println!("{}", value.wrapping_add(10)); }

    Answer

    4

    The wrapping method is defined to wrap in every profile, and 260 minus 256 is four.

  6. What does this program print? fn main() { let value: u8 = 250; println!("{:?}", value.checked_add(10)); }

    Answer

    None

    The checked method returns None when the result does not fit.

  7. What does this program print? fn main() { let value: u8 = 250; println!("{}", value.saturating_add(10)); }

    Answer

    255

    The saturating method stops at the largest value of the type.

  8. What does this program print? fn main() { let value: u8 = 10; let doubled = value * 20; println!("{}", doubled); }

    Answer

    200

    Two hundred fits in a byte, so the product is printed in either profile.

  9. What is the first line of the compiler output for this program? fn main() { let value: u8 = 255; value += 1; println!("{value}"); }

    Answer

    error[E0384]: cannot assign twice to immutable variable value

    The binding is not mutable, and the overflow is a run-time event, not a compile error.

  10. What is the first line of the compiler output for this program? fn main() { let value: u8 = 250; let other: u16 = value + 10; println!("{other}"); }

    Answer

    error[E0308]: mismatched types

    The addition happens in the type of the left side, and a u8 result does not fit a u16 binding.

The code on this page was built with rustc 1.96.1 and cargo 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev

Banner image