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 bycargo build, prioritizes fast compilation and includes debug information.release, used bycargo 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.
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:
- Cargo selects the
releaseprofile because of--release. - The compiler applies release-oriented optimization, including the profile's normal optimization level.
- Cargo reads
debug = truefrom[profile.release]. - The compiler emits debug metadata alongside the optimized machine code.
- The output is placed under
target/release/. - A profiler can use that metadata to identify functions such as
expensive_steprather 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.
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
| Approach | Optimized? | Debug information? | Best use |
|---|---|---|---|
cargo build | Usually no | Yes by default | Fast local development and ordinary debugging |
cargo build --release | Yes | No by default | Small, fast distribution builds |
Release with debug = 1 | Yes | Line tables | Profiling with lower symbol overhead |
Release with debug = true | Yes | Full debug information | Detailed profiling and debugger use |
Custom profiling profile inheriting release | Yes | Configurable | Repeatable 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.
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:
--releaseselects thereleaseprofile.- 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.
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.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.