Question

Rust Cargo Release Builds with Debug Information

rustrust-cargo

Question

When using Cargo, the following command creates a development build with debug information but without release optimizations:

cargo build

In contrast, this command creates an optimized release build without debug information:

cargo build --release

How can I produce an optimized Rust release build that also includes debugging information, so that profiling tools can report meaningful function names and source locations?

Short Answer

You will learn how Cargo profiles control optimization and debug information independently. By configuring the release profile, you can build optimized Rust binaries with debug symbols that profiling and debugging tools can use.

Concept

Cargo builds a Rust package using a profile: a named collection of compiler settings. The two commonly used profiles are:

  • dev, used by cargo build, prioritizes fast compilation and includes debug information.
  • release, used by cargo build --release, prioritizes runtime performance and normally omits debug information.

Optimization and debug information are separate settings. A binary can be highly optimized and contain debug symbols.

Debug symbols store metadata that maps compiled machine code back to Rust functions, files, and line numbers. Profilers use this metadata to turn an unhelpful address such as 0x0000000100ab1234 into useful output such as my_crate::parser::parse and, when supported, a source line.

For an optimized build with debug information, enable debug info in Cargo's release profile. Add this to your project's Cargo.toml:

[profile.release]
debug = true

Then build normally:

cargo build --release

The executable remains optimized, but it will contain or be associated with debug information depending on the target platform and toolchain configuration.

Mental Model

Think of optimization as tuning a race car for speed, while debug information is the car's detailed service manual.

  • Optimization changes how the engine is assembled so it runs faster.
  • Debug information is a map explaining which engine part corresponds to each entry in the manual.

Adding the manual does not make the car use the slow, untuned engine. It mainly makes the delivered artifact larger and lets profiling or debugging tools explain what they observe.

Take Quiz

Syntax and Examples

Configure the release profile in Cargo.toml:

[profile.release]
debug = true

Build with the release profile:

cargo build --release

Cargo writes the optimized executable to target/release/.

Debug information levels

Cargo's debug setting accepts booleans and numeric levels:

[profile.release]
debug = 1

debug = 1 requests line-table information, which is often enough for profiling tools that need function and source-line lookup. For fuller debug information, use:

[profile.release]
debug = true

A common profiling-oriented profile might look like this:

[profile.release]
opt-level = 3
debug = 1

opt-level = 3 is the usual release optimization level. It is shown here for clarity; Cargo's standard release profile already uses it by default.

Step by Step Execution

Consider this project configuration:

# Cargo.toml
[package]
name = "profile_demo"
version = "0.1.0"
edition = "2021"

[profile.release]
debug = true

And this program:

fn main() {
    let total: u64 = (1..10_000_000).map(expensive_step).sum();
    println!("{total}");
}

fn expensive_step(value: u64) -> u64 {
    value.wrapping_mul(value + 1)
}

Run:

cargo build --release

What happens:

  1. Cargo selects the release profile because of --release.
  2. The compiler applies release-oriented optimization, including the profile's normal optimization level.
  3. Cargo reads debug = true from [profile.release].
  4. The compiler emits debug metadata alongside the optimized machine code.
  5. The output is placed under target/release/.
  6. A profiler can use that metadata to identify functions such as expensive_step rather than showing only raw addresses.

Because optimized code may inline, reorder, combine, or remove operations, source lines in a profile can still look different from the original source. Debug information improves attribution; it does not make optimized execution behave like an unoptimized build.

Real World Use Cases

Optimized builds with debug information are useful when performance matters but you still need readable diagnostics:

  • CPU profiling: Find expensive functions in a service, command-line tool, game, or data-processing job.
  • Production crash investigation: Obtain symbolic stack traces from a release binary.
  • Benchmark analysis: Confirm where a benchmark spends time after compiler optimizations are applied.
  • Performance regression testing: Compare profiler results between versions while retaining function names.
  • Native libraries: Help consumers and support teams diagnose a Rust library compiled into a larger application.

For public distribution, teams often keep debug symbols in a private artifact store rather than shipping them to every user.

Take Quiz

Real Codebase Usage

In real projects, developers usually make profiling builds reproducible through Cargo configuration instead of passing ad hoc compiler flags.

Configure release symbols for a profiling branch or internal build

[profile.release]
debug = 1

This keeps the command simple:

cargo build --release

Use a dedicated custom profile

A custom profile avoids changing the settings of the normal release artifact:

[profile.profiling]
inherits = "release"
debug = 1

Build it with:

cargo build --profile profiling

The result is written to target/profiling/.

This pattern is useful in CI: a pipeline can build the usual small release artifact for distribution and separately build a symbol-rich profiling artifact for internal analysis.

Preserve symbols during packaging

Build configuration is only one part of the process. A later packaging, deployment, or binary-processing step can remove symbols. When a profiler cannot resolve names, verify that it has access to the exact executable and any separate debug-symbol files produced for the target platform.

Common Mistakes

Assuming --release means debug data is impossible

--release chooses a profile; it does not permanently force debug information off. Configure the profile instead:

[profile.release]
debug = true

Editing the wrong profile

This config affects development builds, not release builds:

[profile.dev]
debug = true

Use [profile.release] when building with cargo build --release.

Using an unoptimized build for performance conclusions

A development build can have very different performance characteristics from a release build. Profile the same optimization mode that matters in deployment.

Expecting source locations to match exactly

Optimizers can inline functions, remove unused work, and move instructions. A profile can correctly report an inlined function or attribute work to a caller in ways that are not a one-to-one copy of the source file.

Stripping symbols after compilation

A build may contain debug information, but a later stripping or packaging command can discard it. Ensure that the profiler receives matching symbol files or an unstripped analysis artifact.

Forgetting the size trade-off

Debug information can significantly increase artifact size. Use a dedicated profiling profile or retain symbols separately when distribution size is important.

Comparisons

ApproachOptimized?Debug information?Best use
cargo buildUsually noYes by defaultFast local development and ordinary debugging
cargo build --releaseYesNo by defaultSmall, fast distribution builds
Release with debug = 1YesLine tablesProfiling with lower symbol overhead
Release with debug = trueYesFull debug informationDetailed profiling and debugger use
Custom profiling profile inheriting releaseYesConfigurableRepeatable internal performance analysis

debug = 1 is often a practical choice when a profiler mainly needs file and line mappings. debug = true requests more complete information and is generally more useful when attaching a debugger or investigating detailed stack traces.

Take Quiz

Cheat Sheet

# Cargo.toml: optimized release build with full debug information
[profile.release]
debug = true
cargo build --release
# Smaller profiling-focused debug metadata
[profile.release]
debug = 1
# Separate profile for profiling builds
[profile.profiling]
inherits = "release"
debug = 1
cargo build --profile profiling

Key points:

  • --release selects the release profile.
  • Optimization and debug information are independent settings.
  • Symbol-rich artifacts are often larger.
  • Optimized profiling data may contain inlined or reordered code.
  • Keep the exact matching executable and debug-symbol artifacts available to the profiler.

FAQ

Does debug = true disable release optimizations?

No. It enables debug information in addition to the release profile's optimization settings.

Where do I put debug = true in Cargo?

Put it in the project's Cargo.toml under [profile.release].

[profile.release]
debug = true

Is debug = 1 enough for profiling Rust code?

Often, yes. It requests line-table information, which many profilers can use for source attribution. Use debug = true when you need more complete debugging metadata.

Why does my profiler still show hexadecimal addresses?

It may not have access to the exact matching executable or debug-symbol files, or symbols may have been removed during packaging. Confirm the analyzed binary matches the build.

Will a release profile with debug information be larger?

Usually. Debug metadata can add substantial size, especially for larger applications and dependencies.

Can optimized profiles show confusing line numbers?

Yes. Compiler optimization, particularly inlining and code elimination, can make source-level attribution less direct. The profile still reflects the optimized program that actually ran.

Should I ship debug symbols to all users?

Not necessarily. Many teams ship a stripped executable and securely archive matching debug-symbol artifacts for diagnostics and profiling.

Take Quiz

Mini Project

Description

Create a small CPU-bound Rust program and configure a dedicated Cargo profile that keeps release optimizations while emitting profiling-friendly debug information. This mirrors the workflow used to investigate a slow operation without changing the normal distribution build.

Goal

Build and run an optimized profiling executable whose profiler output can resolve Rust function names and source locations.

Requirements

Create a Rust binary that performs a repeatable CPU-bound calculation. Add a custom Cargo profile that inherits the release profile. Enable debug information for the custom profiling profile. Build the project with cargo build --profile profiling. Run the executable from the target/profiling directory.

Take Quiz

Keep learning

Related questions

View more