| name | rust-performance |
| description | | Use when this capability is needed. |
Rust Performance Optimization
Comprehensive guide to profiling, measuring, and optimizing Rust code. Based on "The Rust Performance Book" and real-world optimization patterns.
Quick Navigation
- references/profiling.md - Profiling tools and techniques
- references/allocations.md - Reducing heap allocations
- references/concurrency.md - Parallel and async optimization
- references/compiler_optimizations.md - Release profiles, LTO, PGO, CPU targets
Golden Rules
- Measure first - Profile before optimizing
- Optimize hot paths - 90% of time in 10% of code
- Benchmark changes - Verify improvements
- Consider tradeoffs - Speed vs memory vs complexity
Release Mode
Always benchmark in release mode with optimizations:
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
[profile.release-with-debug]
inherits = "release"
debug = true
cargo build --release
cargo run --release
Quick Performance Wins
1. Use &str Instead of String
fn greet_slow(name: String) {
println!("Hello, {}!", name);
}
fn greet_fast(name: &str) {
println!("Hello, {}!", name);
}
2. Pre-allocate Collections
let mut v = Vec::new();
for i in 0..1000 {
v.push(i);
}
let mut v = Vec::with_capacity(1000);
for i in 0..1000 {
v.push(i);
}
let v: Vec<_> = (0..1000).collect();
3. Avoid Unnecessary Clones
fn process(items: Vec<Item>) -> Vec<Result> {
items.iter()
.map(|item| item.clone())
.map(|item| transform(item))
.collect()
}
fn process(items: Vec<Item>) -> Vec<Result> {
items.into_iter()
.map(transform)
.collect()
}
4. Use Cow<str> for Maybe-Owned Strings
use std::borrow::Cow;
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains(' ') {
Cow::Owned(input.replace(' ', "_"))
} else {
Cow::Borrowed(input)
}
}
5. Use collect() Strategically
let sum: i32 = items.iter()
.map(|x| x * 2)
.collect::<Vec<_>>()
.iter()
.sum();
let sum: i32 = items.iter()
.map(|x| x * 2)
.sum();
Benchmarking
Criterion.rs
The gold standard for Rust benchmarks:
[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "my_benchmark"
harness = false
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 {
match n {
0 | 1 => n,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("fib 20", |b| {
b.iter(|| fibonacci(black_box(20)))
});
let mut group = c.benchmark_group("String Ops");
group.bench_function("clone", |b| {
b.iter(|| String::from("hello").clone())
});
group.bench_function("to_string", |b| {
b.iter(|| "hello".to_string())
});
group.finish();
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
cargo bench
cargo bench -- "fib"
Key Points
- Use
black_box() to prevent optimization
- Multiple iterations for statistical significance
- Compare baseline vs optimized
- Watch for outliers
Profiling
CPU Profiling
perf (Linux):
perf record -g --call-graph dwarf target/release/myapp
perf report
flamegraph:
cargo install flamegraph
cargo flamegraph --bin myapp
samply (Cross-platform):
cargo install samply
samply record target/release/myapp
Memory Profiling
DHAT (Heap profiling):
[dependencies]
dhat = "0.3"
#[cfg(feature = "dhat")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
fn main() {
#[cfg(feature = "dhat")]
let _profiler = dhat::Profiler::new_heap();
}
Heaptrack (Linux):
heaptrack target/release/myapp
heaptrack --analyze heaptrack.myapp.*.zst
Finding Allocations
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
static ALLOCATED: AtomicUsize = AtomicUsize::new(0);
struct CountingAlloc;
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOCATED.fetch_add(layout.size(), Ordering::SeqCst);
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
ALLOCATED.fetch_sub(layout.size(), Ordering::SeqCst);
System.dealloc(ptr, layout)
}
}
#[global_allocator]
static A: CountingAlloc = CountingAlloc;
fn main() {
let before = ALLOCATED.load(Ordering::SeqCst);
let after = ALLOCATED.load(Ordering::SeqCst);
println!("Allocated {} bytes", after - before);
}
Common Optimizations
String Operations
let mut s = String::new();
for i in 0..100 {
s = s + &i.to_string();
}
let mut s = String::with_capacity(300);
for i in 0..100 {
use std::fmt::Write;
write!(s, "{}", i).unwrap();
}
let s: String = (0..100).map(|i| i.to_string()).collect();
Hash Maps
use std::collections::HashMap;
let mut map = HashMap::with_capacity(1000);
*map.entry(key).or_insert(0) += 1;
use rustc_hash::FxHashMap;
let mut map: FxHashMap<u64, Value> = FxHashMap::default();
Iterators vs Loops
let sum: i32 = data.iter().map(|x| x * 2).filter(|x| *x > 10).sum();
let mut sum = 0;
for x in &data {
let doubled = x * 2;
if doubled > 10 {
sum += doubled;
}
}
Avoid Bounds Checking
for i in 0..v.len() {
process(v[i]);
}
for item in &v {
process(*item);
}
unsafe {
for i in 0..v.len() {
process(*v.get_unchecked(i));
}
}
Stack vs Heap
let data = Box::new([0u8; 1024]);
let data = [0u8; 1024];
Async Performance
let data = {
let guard = mutex.lock().await;
guard.clone()
};
process(data).await;
let result = tokio::task::spawn_blocking(|| {
expensive_computation()
}).await?;
use tokio::io::BufReader;
let reader = BufReader::new(file);
Data Structure Choice
| Use Case | Data Structure | Why |
|---|
| Sequential access | Vec<T> | Cache-friendly |
| Key-value lookup | HashMap / FxHashMap | O(1) average |
| Sorted + lookup | BTreeMap | O(log n), ordered |
| Unique elements | HashSet | O(1) contains |
| FIFO queue | VecDeque | O(1) push/pop both ends |
| Small fixed set | ArrayVec / SmallVec | No heap allocation |
| Bit flags | bitflags | Memory efficient |
Compiler Hints
#![feature(core_intrinsics)]
use std::intrinsics::{likely, unlikely};
if unlikely(error_condition) {
handle_error();
}
#[inline]
#[inline(always)]
#[inline(never)]
#[cold]
fn rarely_used() { ... }
#[cfg(target_feature = "avx2")]
fn simd_process(data: &[f32]) { ... }
Arena Allocators
For batch allocations freed together (parsers, request processing):
use bumpalo::Bump;
fn parse<'a>(input: &str, arena: &'a Bump) -> Vec<&'a Node> {
tokenize(input).map(|t| arena.alloc(Node::new(t))).collect()
}
async fn handle(req: Request) -> Response {
let arena = Bump::new();
let parsed = parse(&req.body, &arena);
generate_response(parsed)
}
SmallVec — Inline Small Collections
use smallvec::SmallVec;
let mut tags: SmallVec<[&str; 4]> = SmallVec::new();
tags.push("performance");
tags.push("rust");
Write! Over format!
use std::fmt::Write;
let s = format!("key={} val={}", key, val);
let mut buf = String::with_capacity(128);
for (key, val) in &map {
buf.clear();
write!(buf, "key={key} val={val}").unwrap();
send(&buf);
}
Entry API for HashMap
if let Some(v) = map.get_mut(&k) { *v += 1; } else { map.insert(k, 1); }
*map.entry(k).or_insert(0) += 1;
Struct Size — Keep It Small
use std::mem::size_of;
const _: () = assert!(size_of::<MyEvent>() <= 64);
enum Message {
Ping,
Text(String),
HugeThing(Box<VeryLargeStruct>),
}
Advanced Compiler Optimizations
1. Compile with CPU Native Target
In release mode, use target-cpu=native to allow the compiler to generate machine instructions specifically optimized for the host CPU (enabling AVX, SSE4, etc.).
RUSTFLAGS="-C target-cpu=native" cargo build --release
2. Instruct the Compiler on Cold Paths
Mark rarely executed logic (like initialization, configuration loading, or panic routes) with the #[cold] attribute. This instructs the compiler to optimize the layout of code instructions to prioritize hot execution flows.
#[cold]
fn parse_dev_configurations() {
}
3. Explicit SIMD Vectorization
For high-performance numerical routines, consider using std::simd to process multiple elements in parallel.
#![feature(portable_simd)]
use std::simd::f32x4;
pub fn add_vectors(a: &[f32; 4], b: &[f32; 4]) -> [f32; 4] {
let sa = f32x4::from_slice(a);
let sb = f32x4::from_slice(b);
let sum = sa + sb;
let mut out = [0.0; 4];
sum.copy_to_slice(&mut out);
out
}
Quick Checklist
References
Source: adxptived/Rust-Skills — distributed by TomeVault.