Introduction
Rust is known for its speed and safety, however even in this language you can write non-optimal code. Optimizing the performance of Rust applications is an art that combines understanding low-level language mechanisms, memory management, and efficient use of concurrency. In this article, we will break down key techniques that will help you get the most out of your code. You will learn how to profile applications, use zero-cost abstractions, manage memory without a garbage collector, and properly apply multithreading.
1. Profiling and Benchmarking
Before optimizing, you need to understand what exactly is slowing things down. Rust provides excellent tools for measuring performance.
1.1. Built-in Benchmarks (cargo bench)
Use cargo bench to write micro-benchmarks. To do this, add to Cargo.toml:
[dev-dependencies]criterion = { version = "0.5", features = ["html_reports"] }Example benchmark:
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 bench_fib(c: &mut Criterion) { c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));}
criterion_group!(benches, bench_fib);criterion_main!(benches);1.2. Profiling with perf and flamegraph
For hot code paths, use perf (Linux) or flamegraph. Install:
cargo install flamegraphRun profiling:
cargo flamegraph --bin my_appThis will create an SVG file with a flamegraph, where the most resource-intensive functions are immediately visible.
2. Memory Optimization
Rust does not have a garbage collector, but that doesn't mean memory can be managed carelessly. Key points:
2.1. Avoid Unnecessary Allocations
Every call to Box::new() or Vec::push() can be expensive. Use Vec::with_capacity() for pre-allocating memory:
// Badlet mut v = Vec::new();for i in 0..1000 { v.push(i);}
// Goodlet mut v = Vec::with_capacity(1000);for i in 0..1000 { v.push(i);}2.2. Use the Stack Instead of the Heap
If the data size is known at compile time, store it on the stack. For example, use arrays instead of Vec:
// On the stacklet arr: [i32; 5] = [1, 2, 3, 4, 5];
// On the heap (unnecessary allocation)let vec = vec![1, 2, 3, 4, 5];2.3. Cloning vs Borrowing
Avoid expensive clone(). Use references and borrowing:
fn process(data: &[u8]) { /* ... */ }
// Bad: copying datalet data = vec![1, 2, 3];process(&data.clone());
// Good: passing a referenceprocess(&data);3. Zero-cost Abstractions and Iterators
Rust is famous for high-level constructs compiling into efficient machine code. However, it's important to know how to use them correctly.
3.1. Iterators are Fast
Iterator chains often work faster than manual loops because the compiler can optimize them (loop unrolling, inlining):
// Manual looplet mut sum = 0;for i in 0..1000 { sum += i * 2;}
// Iterator (often faster)let sum: i32 = (0..1000).map(|x| x * 2).sum();3.2. Avoid Dynamic Dispatch
Using dyn Trait adds overhead from virtual calls. When possible, use static dispatch through generics (impl Trait):
// Dynamic dispatch (slower)fn process(d: &dyn SomeTrait) { d.do_something(