Introduction: Why Profile Rust Code?
Rust is famous for its speed and safety, but even the fastest code can run slowly due to non-obvious bottlenecks. Profiling is the process of measuring execution time, memory usage, and other resources, which helps find "bottlenecks". Without profiling, optimization becomes guesswork: you might "improve" code sections that don't affect performance at all.
In this article, we will cover the main Rust profiling tools: perf, flamegraph, cargo-flamegraph, and built-in benchmarking. You will learn how to find slow functions, allocations, and cache misses, as well as how to interpret the obtained data.
Rust Profiling Tools: Choosing the Right Approach
1. Built-in Benchmarking with #[bench]
The first step to profiling is writing benchmarks. Rust has built-in support for benchmarks (unstable, requires nightly). Example:
// In file benches/my_bench.rs#![feature(test)]extern crate test;
use test::Bencher;
#[bench]fn bench_my_function(b: &mut Bencher) { let data = vec![1, 2, 3, 4, 5]; b.iter(|| { // Function being tested let _ = data.iter().sum::<i32>(); });}Run: cargo bench. You will get precise time measurements, but this won't show where time is spent inside the function.
2. perf — Kernel-Level Profiler
The perf tool (Linux) collects CPU counter samples. It shows which lines of code are executed most frequently.
Installation: sudo apt install linux-tools-common linux-tools-generic.
Usage example:
# Compile with debug symbolscargo build --release
# Run profilingsudo perf record ./target/release/my_program
# View reportsudo perf reportIn the report, you will see functions sorted by percentage of execution time. These are the prime candidates for optimization.
3. flamegraph — Call Stack Visualization
Flamegraph is an SVG diagram where the width of each rectangle corresponds to the function's execution time. The wider the rectangle, the more time is spent in that function or its child calls.
Installing cargo-flamegraph:
cargo install flamegraphRunning:
cargo flamegraph --bin my_programThe result is a file flamegraph.svg. Open it in your browser. Look for wide "plateaus" — these are the bottlenecks.
Practical Example: Finding a Bottleneck in Rust Code
Let's consider a simple program that processes a large data array:
fn process(data: &[u8]) -> Vec<u8> { let mut result = Vec::with_capacity(data.len()); for &byte in data { result.push(byte.wrapping_mul(2)); } result}
fn main() { let data = vec![0u8; 10_000_000]; let _ = process(&data);}Compile and run profiling:
cargo build --releasecargo flamegraph --bin my_programOpening the flamegraph, we see that most time is spent in the for loop and the call to wrapping_mul. Note that Vec::push can cause reallocations if the capacity is not precisely calculated. In our case, with_capacity is set correctly, but the loop itself is the bottleneck.
Optimization: use iterators and map:
fn process_optimized(data: &[u8]) -> Vec<u8> { data.iter().map(|&b| b.wrapping_mul(2)).collect()}Re-measuring shows a 10-15% speedup due to vectorization (LLVM auto-vectorization).