| name | rust-perf-tuning |
| description | Diagnose Rust performance bottlenecks from profiling data and apply targeted optimizations. Covers the full cycle: measure โ profile (see rust-profiling skill) โ diagnose โ fix โ verify. Includes pattern-matched fixes for common flamegraph hotspots, allocation elimination, data layout, compiler settings, and parallelism tuning. |
Rust Performance Tuning
End-to-end workflow: baseline benchmark โ profile โ diagnose hotspot pattern โ apply fix โ verify improvement.
Companion skills: [[rust-profiling]] for collecting CPU flamegraphs, heaptrack reports, and collapsed stacks. This skill starts where that one ends โ you have data, now fix it. [[rust-memory-optimization]] for heap/allocation-specific fixes. [[rust-parallel-processing]] for adding parallelism once single-core is tuned. [[rust-development]] for the full workflow map.
Phase 0 โ Prerequisites: Build Settings First
Before profiling or optimizing code, apply zero-effort compiler settings. These are free speedups.
[profile.release]
codegen-units = 1
lto = "thin"
opt-level = 3
debug = 1
export RUSTFLAGS="-C target-cpu=native"
Expected impact: 10โ25% end-to-end speedup with no code changes. Do this first, always.
To collect the flamegraph and collapsed stacks needed for Phase 2 diagnosis, apply the rust-profiling skill.
Phase 1 โ Establish a Baseline
cargo bench -p <crate> -- --save-baseline before
time ./target/release/<binary> <args>
perf stat -- ./target/release/<binary> <args>
IPC interpretation from perf stat:
- IPC < 1.0 โ memory-bound (cache misses dominating)
- IPC 1โ2 โ compute-bound, some stalls
- IPC > 3 โ well-vectorized, CPU is the limit
- Cache misses > 5% โ data layout problem
Phase 2 โ Diagnose from Flamegraph / Collapsed Stacks
Run rust-profiling skill to collect a flamegraph. Then pattern-match the hotspot:
Pattern Table
| What you see in flamegraph | Diagnosis | Section |
|---|
alloc::alloc::alloc / jemalloc_sys::malloc | Per-iteration allocation | ยง3 Allocations |
std::collections::hash_map | HashMap overhead | ยง4 HashMap |
clone() in hot path | Unnecessary copy | ยง3 Allocations |
fmt::Display / format!() | String formatting overhead | ยง3 Allocations |
rayon::iter overhead > work items | Parallel overhead > gain | ยง6 Rayon |
parking_lot::Mutex::lock | Lock contention | ยง6 Rayon |
memcpy / memmove dominant | Data movement | ยง5 Data Layout |
f32::sqrt / f64::sqrt / trig in loop | Math-bound โ SIMD candidate | ยง7 SIMD |
nalgebra:: slow with dynamic matrices | Dynamic allocation in linalg | ยง7 SIMD |
| Thin hot band across many frames | IPC < 1 โ memory-bound โ SoA | ยง5 Data Layout |
core::slice::index / bounds check | Redundant bounds checking | ยง8 Unsafe Elision |
dyn Trait vtable calls wide | Dynamic dispatch overhead | ยง9 Dispatch |
Quick awk on collapsed stacks (from rust-profiling)
awk '{split($0,a,";"); leaf=a[length(a)]; sub(/ [0-9]+$/,""); n=$NF; count[leaf]+=n}
END{for(f in count) print count[f],f}' cpu.collapsed | sort -rn | head -20
grep "my_crate::" cpu.collapsed | awk '{sum+=$NF} END{print sum}'
grep "" cpu.collapsed | awk '{sum+=$NF} END{print sum}'
grep "hot_function" cpu.collapsed | sed 's/;[^;]*$//' | sort | uniq -c | sort -rn | head -10
Phase 3 โ Fix: Eliminating Allocations
Symptom: alloc, clone, format!, collect in flamegraph hot path.
Buffer reuse (most common fix)
for line in lines {
let s = format!("prefix_{}", line);
process(&s);
}
let mut buf = String::with_capacity(64);
for line in lines {
buf.clear();
write!(&mut buf, "prefix_{}", line).unwrap();
process(&buf);
}
SmallVec for short-lived collections
let mut neighbors: Vec<u32> = Vec::new();
use smallvec::SmallVec;
let mut neighbors: SmallVec<[u32; 8]> = SmallVec::new();
Vec::with_capacity to prevent reallocation
let mut v: Vec<_> = Vec::new();
for x in iter { v.push(x); }
let mut v = Vec::with_capacity(iter.size_hint().0);
for x in iter { v.push(x); }
Avoid clone by restructuring ownership
fn process(cloud: PointCloud) {
let filtered = filter(cloud.clone());
let normals = estimate(cloud.clone());
}
fn process(cloud: &PointCloud) {
let filtered = filter(cloud);
let normals = estimate(cloud);
}
Cow<'_, str> for conditionally-owned strings
fn normalize(s: &str) -> String {
if s.contains(' ') { s.replace(' ', "_") } else { s.to_owned() }
}
fn normalize(s: &str) -> Cow<'_, str> {
if s.contains(' ') { Cow::Owned(s.replace(' ', "_")) } else { Cow::Borrowed(s) }
}
Phase 4 โ Fix: HashMap Replacement
Symptom: std::collections::hash_map::HashMap near top of flamegraph.
use std::collections::HashMap;
let mut map: HashMap<u32, u32> = HashMap::new();
use rustc_hash::FxHashMap;
let mut map: FxHashMap<u32, u32> = FxHashMap::default();
use ahash::AHashMap;
let mut map: AHashMap<String, u32> = AHashMap::new();
type FastMap<K, V> = rustc_hash::FxHashMap<K, V>;
Expected speedup: 4โ84% over std HashMap (rustc's own benchmarks). Use std HashMap only when keys are user-controlled (HashDoS resistance required).
Pre-size to avoid rehashing:
let mut map = FxHashMap::with_capacity_and_hasher(expected_n, Default::default());
Phase 5 โ Fix: Data Layout (SoA over AoS)
Symptom: IPC < 1.0 in perf stat; memcpy/cache-miss dominated flamegraph; loop processes only a few fields of a large struct.
struct Particle { x: f32, y: f32, z: f32, mass: f32, charge: f32, flags: u32 }
let particles: Vec<Particle> = ...;
for p in &particles { p.x += dt * p.vx; }
struct Particles { x: Vec<f32>, y: Vec<f32>, z: Vec<f32>, mass: Vec<f32>, charge: Vec<f32>, flags: Vec<u32> }
Expected speedup: 2โ4ร on vectorizable loops. The compiler auto-vectorizes SoA without any unsafe or SIMD intrinsics.
False sharing fix (parallel code)
struct Counter { value: u64 }
let counters: Vec<Counter> = (0..n_threads).map(|_| Counter { value: 0 }).collect();
use crossbeam::utils::CachePadded;
let counters: Vec<CachePadded<Counter>> = ...;
Phase 6 โ Fix: Rayon Parallelism
Symptom: Rayon overhead visible in flamegraph; work_stealing / join calls dominate; or parallelism isn't helping.
When NOT to use rayon
small_vec.par_iter().for_each(|x| cheap_op(x));
if small_vec.len() > 10_000 {
small_vec.par_iter().for_each(|x| expensive_op(x));
} else {
small_vec.iter().for_each(|x| expensive_op(x));
}
Reducing allocation inside par_iter
let results: Vec<Vec<u32>> = data.par_iter().map(|x| compute(x)).collect();
let results: Vec<u32> = data.par_iter()
.flat_map_iter(|x| compute_iter(x))
.collect();
Thread-local buffers for per-iteration temporary data
use std::cell::RefCell;
thread_local! {
static BUF: RefCell<Vec<u32>> = RefCell::new(Vec::with_capacity(1024));
}
data.par_iter().for_each(|x| {
BUF.with(|buf| {
let mut buf = buf.borrow_mut();
buf.clear();
compute_into(x, &mut buf);
});
});
Rayon pool sizing
rayon::ThreadPoolBuilder::new()
.num_threads(num_cpus::get_physical())
.build_global()
.unwrap();
Phase 7 โ Fix: SIMD and Math
Symptom: f32::sqrt, trig, or nalgebra dynamic-matrix functions in hot path.
Enable auto-vectorization first
Before writing SIMD, check if the compiler already vectorizes with RUSTFLAGS="-C target-cpu=native". Inspect assembly:
cargo rustc --release -- --emit asm
grep -A 20 "my_function:" target/release/deps/*.s | grep -i "ymm\|zmm\|xmm"
wide crate for portable SIMD (stable Rust)
use wide::f32x8;
let result: Vec<f32> = a.iter().zip(b.iter()).map(|(&x, &y)| x * y + c).collect();
let result: Vec<f32> = a.chunks_exact(8).zip(b.chunks_exact(8))
.flat_map(|(ax, bx)| {
let va = f32x8::from(ax.try_into().unwrap());
let vb = f32x8::from(bx.try_into().unwrap());
(va * vb + f32x8::splat(c)).to_array()
})
.collect();
nalgebra: prefer fixed-size over dynamic matrices
let m: nalgebra::DMatrix<f64> = DMatrix::zeros(3, 3);
let m: nalgebra::SMatrix<f64, 3, 3> = SMatrix::zeros();
Phase 8 โ Fix: Bounds Check Elision
Symptom: core::panicking::panic_bounds_check or slice::index visible in flamegraph.
for (x, y) in a.iter().zip(b.iter()) { *x += *y; }
let (left, right) = slice.split_at(mid);
let val = unsafe { *slice.get_unchecked(i) };
Note: get_unchecked is unsafe. Only use after proving the index is valid. Incorrect use causes UB.
Phase 9 โ Fix: Dispatch Overhead
Symptom: dyn Trait vtable calls spread across flamegraph; many call sites for the same trait method.
fn process(items: &[Box<dyn Processor>]) {
for item in items { item.process(); }
}
enum ProcessorKind { A(ProcessorA), B(ProcessorB) }
impl ProcessorKind {
fn process(&self) { match self { Self::A(p) => p.process(), Self::B(p) => p.process() } }
}
Expected speedup: 2โ10ร when the hot path is a tight loop over heterogeneous trait objects.
Phase 10 โ Verify the Fix
Always compare against the baseline, not against intuition:
cargo bench -p <crate> -- --baseline before
perf record -F 997 -g --call-graph=dwarf -- ./target/release/<binary> <args>
perf script | /opt/FlameGraph/stackcollapse-perf.pl > after.collapsed
/opt/FlameGraph/difffolded.pl before.collapsed after.collapsed | /opt/FlameGraph/flamegraph.pl > diff.svg
xdg-open diff.svg
perf stat -- ./target/release/<binary> <args>
Minimum bar: criterion must show statistically significant improvement (no overlap in confidence intervals). A 5% improvement that's within noise is not a confirmed fix.
Related Skills
| Skill | When to apply |
|---|
rust-profiling | Collect CPU flamegraphs, heaptrack reports, and collapsed stacks |
code-refactoring | Structural refactors after eliminating the performance bottleneck |
code-debugging | Systematic investigation when a perf fix introduces a correctness bug |
security-review | Audit unsafe blocks introduced for bounds-check elision |
Quick Reference: Fix by Symptom
| Symptom | First fix to try | Expected gain |
|---|
alloc / malloc in hot path | Buffer reuse + with_capacity | 2โ10ร |
HashMap near top | FxHashMap | 4โ84% |
| IPC < 1.0 in perf stat | SoA data layout | 2โ4ร |
| Rayon overhead > work | Raise serial threshold or chunk size | 1.5โ3ร |
dyn Trait wide in flamegraph | Enum dispatch | 2โ10ร |
f32::sqrt / math dominant | target-cpu=native first, then wide | 2โ8ร |
| Build time / binary size bloat | lto = "thin", codegen-units = 4 | N/A |
| Slow cold start, not steady-state | PGO (cargo-pgo) | 10โ20% |
| nalgebra slow | SMatrix instead of DMatrix | 2โ5ร |
| Clone in hot path | Borrow or Arc | 2โ10ร |