Performance & Profiling
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Rust code is fast by default — no garbage collector, no boxing of values, monomorphised generics — but "fast by default" is not "as fast as possible". The method is always the same: build in release mode, measure with a benchmark, profile to find where the time goes, change one thing, and measure again. The community Rust Performance Book covers the topic in depth; this page is a practical summary.
Build settings
Always measure release builds (cargo build --release, cargo bench). Debug builds can be 10-100x slower and their
profiles are misleading. Beyond the defaults, the profile settings that matter most:
[profile.release]
opt-level = 3 # default; "s"/"z" for size
lto = "fat" # whole-program optimisation across crates: slower builds, often 5-20% faster code
codegen-units = 1 # better optimisation, slower builds
panic = "abort" # smaller code, no unwinding (only if panics may end the process)
debug = "line-tables-only" # keep symbol names and lines for profilers, without full debug info
[profile.bench]
inherits = "release"
Source: The Cargo Book: Profiles
-
-C target-cpu=native(inRUSTFLAGSor.cargo/config.toml) lets LLVM use every instruction of the build machine’s CPU (AVX2, AVX-512): good for binaries that run where they are built, wrong for distributed binaries. A safer middle ground is a baseline such asx86-64-v3. -
Profile-guided optimisation (PGO): build an instrumented binary (
-Cprofile-generate), run a representative workload, merge the profiles withllvm-profdata, and rebuild with-Cprofile-use. Typical gains are 10-20% for branch-heavy code such as parsers and compilers;cargo-pgoautomates the steps, and BOLT can be layered on top. -
The global allocator can be swapped (below).
Source: The rustc Book: Codegen options
Allocation avoidance
Heap allocation is the most common avoidable cost in Rust programs: each String, Vec or Box created in a hot loop
is a call into the allocator. Techniques:
-
pre-size collections with
Vec::with_capacity/String::with_capacity, orreserve; -
reuse buffers across iterations (
buf.clear()keeps the capacity); -
borrow instead of cloning (
&strparameters, iterators over references); -
return
Cow<'_, str>from functions that usually do not modify their input; -
use small-vector types (
smallvec::SmallVec<[T; 8]>,arrayvec) for collections that are usually tiny, keeping them on the stack; -
intern repeated strings, or use
Arc<str>for shared immutable text.
use std::borrow::Cow;
use std::fmt::Write;
fn normalise(title: &str) -> Cow<'_, str> {
if title.chars().all(|c| c.is_ascii_lowercase() || c == ' ') {
Cow::Borrowed(title) // common case: no allocation
} else {
Cow::Owned(title.to_lowercase())
}
}
fn render(lines: &[(&str, u16)]) -> String {
let mut out = String::with_capacity(lines.len() * 24); // one allocation, sized up front
let mut buf = String::new(); // reused scratch buffer
for (title, year) in lines {
buf.clear();
write!(buf, "{year}: {}", normalise(title)).expect("writing to a String cannot fail");
out.push_str(&buf);
out.push('\n');
}
out
}
fn main() {
print!("{}", render(&[("dune", 1965), ("Emma", 1815)]));
}
Iterators and zero-cost abstractions
Iterator chains compile to the same machine code as hand-written loops and often better, because slice iterators let the
compiler drop bounds checks. Prefer iter().map().sum() and zip over indexing loops (for i in 0..n { a[i] * b[i] }),
which need a bounds check per access unless the compiler can prove them redundant. See
Iterators.
Inlining and cold paths
-
#[inline]makes a function’s body available for inlining across crates (generic functions already are); it is a hint, rarely needed in application code.#[inline(always)]and#[inline(never)]are stronger hints, useful mainly to steer profiles and benchmarks. -
#[cold]marks a function as unlikely to be called (error constructors, panic paths), andstd::hint::cold_path()(1.95) marks a branch as unlikely, so the optimiser lays out the hot path first.
#[cold]
#[inline(never)]
fn invalid_isbn(text: &str) -> String {
format!("invalid ISBN: {text}")
}
fn check(text: &str) -> Result<u64, String> {
match text.parse::<u64>() {
Ok(n) if text.len() == 13 => Ok(n),
_ => {
std::hint::cold_path(); // 1.95: this branch is unlikely
Err(invalid_isbn(text))
}
}
}
fn main() {
println!("{:?} {:?}", check("9780441013593"), check("12"));
}
Source: std::hint::cold_path
Alternative allocators
The default global allocator is the system’s malloc. Multi-threaded, allocation-heavy programs — web services in
particular — often run faster and fragment less with mimalloc or jemalloc (tikv-jemallocator). Swapping is two
lines, but measure: the benefit depends entirely on the workload.
// Cargo.toml: mimalloc = "0.1"
use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
fn main() {
let v: Vec<String> = (0..1000).map(|i| i.to_string()).collect();
println!("{}", v.len());
}
Source: std::alloc: The #[global_allocator attribute]
Benchmarking
Use criterion (stable) for statistically sound micro-benchmarks, with std::hint::black_box to stop the optimiser from
deleting the work; see Testing & Benchmarking. For whole
programs, hyperfine compares command-line runs, and load generators (oha, k6) measure services. Benchmark on quiet
machines, compare against a saved baseline, and look at variance, not just the mean. Mara & Holmes (Refactoring to
Rust, chapter 6) benchmark the Rust replacement against the original implementation in the same harness — the right
way to justify a rewrite.
Profiling CPU time
A sampling profiler interrupts the program many times per second and records the call stack; the hot functions are the
ones that appear most often. On Linux, perf is the standard tool; flamegraphs visualise its output with each function’s
width proportional to the time spent in it and below it.
# Keep symbols in release builds: debug = "line-tables-only" in [profile.release], or:
CARGO_PROFILE_RELEASE_DEBUG=line-tables-only cargo build --release
cargo install flamegraph
cargo flamegraph --bin shelf -- search dune # writes flamegraph.svg (uses perf on Linux, dtrace on macOS)
cargo install samply
samply record ./target/release/shelf search dune # opens the Firefox Profiler in a browser
perf record -g ./target/release/shelf search dune && perf report
Source: cargo-flamegraph
Read a flamegraph from the bottom up: wide plateaus at the top are where CPU time is actually spent. In async services, time blocked on I/O does not appear in CPU profiles; use tracing spans and tokio-console for that (see Axum: Performance Tuning). Gangappa & Rangaraj (Mastering Distributed Observability in Rust, chapter 9) also use continuous profiling, which samples production processes all the time.
Profiling memory
-
heaptrack (Linux) and DHAT (part of Valgrind, or the
dhatcrate) record every allocation with its call stack: where memory is allocated, how much, and how short-lived. -
With jemalloc as the allocator, its built-in heap profiling (
prof:trueinMALLOC_CONF, viatikv-jemallocator/jemalloc_pprof) produces pprof profiles of live memory — useful for leaks in long-running services. -
cargo bloatshows which functions and crates take space in the binary.
Compile-time performance
Long builds slow every iteration. cargo build --timings writes an HTML report showing each crate’s compile time and how
much the build runs in parallel — look for long sequential chains and single huge crates. Cargo’s build-performance guide
lists the levers: split large crates, trim dependencies and their default features, reduce generic code in hot crates,
use cargo check while editing, a faster linker (LLD by default on x86-64 Linux since 1.90; mold), sccache, and the
Cranelift codegen backend for debug builds on nightly.
References
-
The Rust Performance Book (community)
-
The rustc Book: Codegen options and Profile-guided Optimization
-
The Rust Programming Language: Comparing Performance: Loops vs. Iterators
-
std::hint (black_box, cold_path)
-
Lily Mara and Joel Holmes, Refactoring to Rust (Manning, 2025)
-
Manjunath Gangappa and Rajkumar Rangaraj, Mastering Distributed Observability in Rust (Packt, 2026)