Profiling Rust Code: Finding Bottlenecks and Speeding Up Programs

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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 report


In 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 flamegraph


Running:



cargo flamegraph --bin my_program


The 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_program


Opening 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).



Advanced Techniques: Al

Blogs

Book Recommendations