Profiling Rust Code: Finding Bottlenecks in Practice
When you write in Rust, performance is often a key advantage. But even the fastest code can have hidden bottlenecks: slow allocations, inefficient algorithms, or caching issues. Profiling is the process of measuring program performance to identify such places. In this article, we will cover practical tools and methods for profiling Rust code, from simple benchmarks to flamegraph visualization.
Why Profile Code in Rust?
Rust guarantees memory safety, but it does not guarantee optimal performance. Bottlenecks can arise from:
- Inefficient data structures (e.g., using
Vecinstead ofHashMapfor frequent lookups). - Excessive allocations (cloning strings or vectors where references could be used).
- Caching issues (incorrect matrix traversal order).
- Lock contention in multithreaded code (contention on mutexes).
Profiling helps you find exactly which parts to optimize, rather than guessing blindly.
Tools for Profiling Rust
Let's look at three main approaches: built-in benchmarks, system profilers, and visualization.
1. Benchmarks with criterion
The criterion library is the standard for micro-benchmarks in Rust. It allows measuring the execution time of individual functions, accounting for statistical outliers.
Setup in Cargo.toml:
[dev-dependencies]criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]name = "my_benchmark"harness = falseExample benchmark (file benches/my_benchmark.rs):
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 { match n { 0 => 1, 1 => 1, n => fibonacci(n-1) + fibonacci(n-2), }}
fn benchmark_fib(c: &mut Criterion) { c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));}
criterion_group!(benches, benchmark_fib);criterion_main!(benches);Run: cargo bench. Criterion will show the average time, standard deviation, and generate an HTML report with charts.
2. System Profiler perf (Linux)
perf is a powerful Linux kernel tool for collecting CPU statistics (cycles, cache misses, instructions). It is especially useful for Rust as it shows not only time but also hardware events.
Usage example:
# Collect dataperf record -g -- ./target/release/my_program
# Interactive analysisperf report -g
# View hot functionsperf top -p $(pgrep my_program)The -g flag enables call graph recording. This allows you to see which functions are calling the slow code.
3. Visualization with flamegraph
Flamegraph is a way to visualize the call stack. The width of a rectangle shows how much time the program spent in that function and its descendants. The wider the block, the more time.
Installing the tool:
cargo install flamegraphRun:
cargo flamegraph --bin my_programAfter execution, you will get a flamegraph.svg file. Open it in a browser — you will see an interactive graph where you can click on blocks for details.
Practical Example: Finding a Bottleneck
Suppose we have a program that processes a large text file and counts word frequency. The code might look like this:
use std::collections::HashMap;use std::fs::File;use std::io::{Buf