Rust Optimization Pipeline
A comprehensive skill for optimizing Rust programs to production-grade performance levels using
industry-standard tools and techniques.
When to Use This Skill
Trigger this skill when the user wants to:
• Make their Rust code faster
• Set up advanced compilation flags (LTO, PGO, BOLT)
• Profile and benchmark Rust programs
• Optimize hot paths and reduce allocations
• Achieve 2x-4x speedups on existing code
• Deploy production-optimized binaries
• Questions like "how do I make my Rust code faster?"
Core Optimization Philosophy
CRITICAL ORDER - Never skip or reverse these steps:
1. Algorithm/Data Structure (biggest impact)
2. LLVM Optimization Flags
3. Link-Time Optimization (LTO)
4. Profile-Guided Optimization (PGO)
5. BOLT (Binary Optimization)
6. Manual Micro-optimizations
7. Continuous Profiling
Why this order matters:
• Wrong algorithm = no amount of compiler flags will save you
• Compiler flags give 30-40% improvement for free
• PGO/BOLT optimize based on real usage patterns
• Manual optimization should target profiler hotspots, not guesses
1. LLVM Optimization Flags (Foundation)
Start with a strong compilation baseline.
Standard Release Build (Enhanced)
Add this to [Link]:
[[Link]]
opt-level = 3 # Maximum LLVM optimization
lto = "fat" # Full Link-Time Optimization
codegen-units = 1 # Better global analysis (slower compile)
strip = true # Remove debug symbols
panic = "abort" # Smaller binary, no unwinding
# For specific CPU (faster runtime, less portable)
[[Link]."*"]
opt-level = 3
[build]
rustflags = ["-C", "target-cpu=native"]
Build Command
RUSTFLAGS="-C target-cpu=native" cargo build --release
Flag Reference Table
Flag Purpose Tradeoff
`opt-level=3` Maximum optimization Slower compile, best runtime
`lto="fat"` Cross-crate inlining Much slower compile, 10-20% speedup
`lto="thin"` Faster LTO variant Better compile time, slightly less optimization
`codegen-units=1` Single compilation unit Slowest compile, best optimization
`target-cpu=native` Use all CPU features Not portable to other machines
`target-cpu=x86-64-v3` Modern x86 features Portable to most modern CPUs
Expected gain: 20-40% over default release build
2. Benchmarking (Measure Everything)
Golden Rule: Never optimize without measuring.
Install Hyperfine
cargo install hyperfine
Basic Benchmark
hyperfine './target/release/my_program'
Compare Before/After
# Save baseline
hyperfine --export-json [Link] './target/release/my_program'
# After optimization
hyperfine --export-json [Link] './target/release/my_program'
Warmup and Multiple Runs
hyperfine --warmup 3 --runs 10 './target/release/my_program [Link]'
[Link] (For Micro-benchmarks)
Add to [Link]:
[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "my_benchmark"
harness = false
Create benches/my_benchmark.rs:
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 criterion_benchmark(c: &mut Criterion) {
c.bench_function("fib 20", |b| [Link](|| fibonacci(black_box(20))));
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
Run with:
cargo bench
3. Profile-Guided Optimization (PGO) ■
Most impactful optimization technique. Teaches compiler your actual usage patterns.
Step A: Compile with Profiling
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" \
cargo build --release
Step B: Run Real Workload
Execute your program with production-like data:
./target/release/my_program typical_input.txt
./target/release/my_program edge_case.txt
./target/release/my_program large_dataset.txt
Important: Run multiple representative workloads to capture all hot paths.
This generates *.profraw files in /tmp/pgo-data/.
Step C: Merge Profile Data
llvm-profdata merge -o /tmp/pgo-data/[Link] /tmp/pgo-data/*.profraw
If llvm-profdata not found:
# Ubuntu/Debian
sudo apt install llvm
# macOS
brew install llvm
export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
Step D: Rebuild with Profile
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/[Link] -C lto=fat -C codegen-units=1" \
cargo build --release
Automated PGO Script
Create scripts/build_pgo.sh:
#!/bin/bash
set -e
echo "==> Building instrumented binary..."
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release
echo "==> Running workload to collect profiles..."
rm -rf /tmp/pgo-data/*.profraw 2>/dev/null || true
./target/release/my_program data/[Link]
./target/release/my_program data/[Link]
./target/release/my_program data/[Link]
echo "==> Merging profile data..."
llvm-profdata merge -o /tmp/pgo-data/[Link] /tmp/pgo-data/*.profraw
echo "==> Building optimized binary..."
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/[Link] -C lto=fat -C codegen-units=1" \
cargo build --release
echo "==> Done! Optimized binary at target/release/my_program"
Expected gain: 10-50% on top of LTO
4. BOLT (Binary Optimization and Layout Tool) ■
Post-link optimizer that rearranges code layout based on runtime profiling.
Prerequisites
Install LLVM with BOLT:
# Ubuntu/Debian
sudo apt install llvm lld
# macOS
brew install llvm
Step A: Build with Debug Info
BOLT needs symbols:
RUSTFLAGS="-C debuginfo=1 -C lto=fat" cargo build --release
Step B: Profile with Perf
sudo perf record -e cycles:u -j any,u -- \
./target/release/my_program typical_input.txt
This creates [Link].
Step C: Convert Perf Data
perf2bolt ./target/release/my_program \
-p [Link] \
-o my_program.fdata
Step D: Optimize with BOLT
llvm-bolt ./target/release/my_program \
-o ./target/release/my_program.bolt \
-data my_program.fdata \
-reorder-blocks=ext-tsp \
-reorder-functions=hfsort \
-split-functions \
-split-all-cold \
-dyno-stats
Use my_program.bolt as your production binary.
BOLT Automated Script
Create scripts/build_bolt.sh:
#!/bin/bash
set -e
echo "==> Building with debug info..."
RUSTFLAGS="-C debuginfo=1 -C lto=fat" cargo build --release
echo "==> Profiling with perf..."
sudo perf record -e cycles:u -j any,u -- \
./target/release/my_program data/[Link]
echo "==> Converting perf data..."
perf2bolt ./target/release/my_program -p [Link] -o my_program.fdata
echo "==> Running BOLT optimization..."
llvm-bolt ./target/release/my_program \
-o ./target/release/my_program.bolt \
-data my_program.fdata \
-reorder-blocks=ext-tsp \
-reorder-functions=hfsort \
-split-functions \
-split-all-cold \
-dyno-stats
echo "==> Done! BOLT-optimized binary at target/release/my_program.bolt"
Expected gain: 5-15% on top of PGO
5. Profiling and Performance Analysis
Before manual optimization, know WHERE to optimize.
Basic Profiling with Flamegraph
cargo install flamegraph
cargo flamegraph --release -- typical_input.txt
Opens an interactive SVG showing:
• Width = time spent
• Color-coded by library/crate
• Click to zoom into hot functions
Perf (Linux) - Basic Usage
# Record
perf record --call-graph dwarf ./target/release/my_program
# View
perf report
Perf with Hardware Counters
# Cache misses
perf stat -e cache-misses,cache-references ./target/release/my_program
# Branch mispredictions
perf stat -e branch-misses,branches ./target/release/my_program
# All hardware events
perf stat -d ./target/release/my_program
Instruments (macOS)
# Build with debug info
RUSTFLAGS="-C debuginfo=2" cargo build --release
# Run in Instruments
open -a Instruments target/release/my_program
Valgrind Callgrind
valgrind --tool=callgrind --callgrind-out-file=[Link] \
./target/release/my_program
# Visualize with kcachegrind
kcachegrind [Link]
Memory Profiling with DHAT
# Install valgrind
sudo apt install valgrind
# Run with DHAT
valgrind --tool=dhat ./target/release/my_program
DHAT shows:
• Allocation hotspots
• Memory fragmentation
• Lifetime patterns
• Heap usage over time
Heaptrack
# Install
sudo apt install heaptrack
# Profile
heaptrack ./target/release/my_program
# Visualize
heaptrack_gui heaptrack.my_program.*
Shows:
• Call graphs for allocations
• Temporary allocation patterns
• Memory leaks
CPU Cache Analysis with Cachegrind
valgrind --tool=cachegrind \
--cache-sim=yes \
./target/release/my_program
# View results
cg_annotate [Link].*
Metrics:
• L1/L2/L3 cache hit rates
• Instruction cache misses
• Data cache misses
Custom Instrumentation
use std::time::Instant;
struct Timer {
name: &'static str,
start: Instant,
}
impl Timer {
fn new(name: &'static str) -> Self {
Timer {
name,
start: Instant::now(),
}
}
}
impl Drop for Timer {
fn drop(&mut self) {
println!("{}: {:?}", [Link], [Link]());
}
}
// Usage
fn expensive_function() {
let _timer = Timer::new("expensive_function");
// Function body
}
Preventing Inline for Profiling
// Add to code
#[inline(never)]
#[no_mangle]
fn hot_function() {
// Your code
}
Then profile normally - #[inline(never)] prevents it from being inlined and disappearing.
6. Memory Optimization
A. Custom Allocators
The default allocator isn't always optimal. Custom allocators can give 10-40% speedups.
■ Level 1: Drop-in Replacements (Easiest)
Use production-ready allocators:
Allocator Best For Provider
mimalloc General speed Microsoft
jemalloc Stability, servers Facebook/Meta
tcmalloc Google workloads Google
snmalloc Security-critical Microsoft Research
Implementation:
[dependencies]
mimalloc = "0.1"
use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
fn main() {
// All allocations now use mimalloc
}
That's it! Many companies stop here.
Benchmarking Allocators
# Test with system allocator
cargo build --release
hyperfine './target/release/my_program'
# Test with mimalloc
# (add to [Link] and code)
cargo build --release
hyperfine './target/release/my_program'
■ Level 2: Custom Allocators for Your Use Case
Write specialized allocators for specific patterns.
Arena Allocator - For temporary allocations that die together:
use std::alloc::{Layout, GlobalAlloc};
use std::ptr;
struct Arena {
buffer: Vec<u8>,
offset: std::sync::atomic::AtomicUsize,
}
impl Arena {
fn new(size: usize) -> Self {
Arena {
buffer: vec![0; size],
offset: std::sync::atomic::AtomicUsize::new(0),
}
}
fn reset(&self) {
[Link](0, std::sync::atomic::Ordering::Release);
}
}
unsafe impl GlobalAlloc for Arena {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let size = [Link]();
let align = [Link]();
let offset = [Link].fetch_add(size, std::sync::atomic::Ordering::AcqRel);
if offset + size > [Link]() {
return ptr::null_mut();
}
[Link].as_ptr().add(offset) as *mut u8
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
// No-op: arena is reset all at once
}
}
Use case: Game frame allocations, request handlers, temporary computations.
Bump Allocator - Simplest custom allocator:
struct BumpAllocator {
buffer: Vec<u8>,
position: usize,
}
impl BumpAllocator {
fn new(capacity: usize) -> Self {
BumpAllocator {
buffer: vec![0; capacity],
position: 0,
}
}
fn allocate(&mut self, size: usize, align: usize) -> Option<*mut u8> {
let padding = (align - ([Link] % align)) % align;
let start = [Link] + padding;
let end = start + size;
if end > [Link]() {
return None;
}
[Link] = end;
Some(unsafe { [Link].as_mut_ptr().add(start) })
}
fn reset(&mut self) {
[Link] = 0; // Instant "free all"
}
}
Pool Allocator - For fixed-size objects:
struct Pool<T> {
objects: Vec<T>,
free_list: Vec<usize>,
}
impl<T: Default> Pool<T> {
fn with_capacity(cap: usize) -> Self {
Pool {
objects: (0..cap).map(|_| T::default()).collect(),
free_list: (0..cap).collect(),
}
}
fn acquire(&mut self) -> Option<&mut T> {
self.free_list.pop().map(|idx| &mut [Link][idx])
}
fn release(&mut self, obj: &T) {
let idx = unsafe {
(obj as *const T).offset_from([Link].as_ptr()) as usize
};
self.free_list.push(idx);
}
}
When to Use Custom Allocators:
Pattern Allocator Why
Request handlers Arena/Bump All freed at once
Game entities Pool Fixed-size, frequent alloc/free
Parsers Arena Temporary AST nodes
Network buffers Pool Reuse buffers
General speedup mimalloc/jemalloc Drop-in replacement
B. Avoid Allocations
■ Inefficient:
fn process(items: &[Item]) -> Vec<Result> {
let mut results = Vec::new(); // Allocates on each push
for item in items {
[Link](process_one(item));
}
results
}
■ Optimized:
fn process(items: &[Item]) -> Vec<Result> {
let mut results = Vec::with_capacity([Link]()); // Pre-allocate
for item in items {
[Link](process_one(item));
}
results
}
■ Even Better (Iterator):
fn process(items: &[Item]) -> Vec<Result> {
[Link]().map(process_one).collect()
}
C. String Concatenation
■ Inefficient:
let mut s = String::new();
for i in 0..1000 {
s = s + &i.to_string(); // Allocates every iteration
}
■ Optimized:
let mut s = String::with_capacity(4000);
for i in 0..1000 {
s.push_str(&i.to_string());
}
■ Best:
use std::fmt::Write;
let mut s = String::with_capacity(4000);
for i in 0..1000 {
write!(&mut s, "{}", i).unwrap(); // No intermediate allocation
}
D. Stack vs Heap Strategy
// Stack allocation (fast, limited size)
let array: [u8; 1024] = [0; 1024]; // 1KB on stack
// Heap allocation (slow, unlimited size)
let vec: Vec<u8> = vec![0; 1024 * 1024]; // 1MB on heap
// Hybrid: Small = stack, large = heap
enum Buffer {
Small([u8; 64]),
Large(Vec<u8>),
}
Rule of thumb:
• Stack: <1KB, known at compile time
• Heap: >1KB, dynamic size, long lifetime
E. Contiguous Data Structures
Cache-friendly layouts:
// Bad: Pointer chasing
struct BadNode {
value: i32,
next: Option<Box<BadNode>>, // Each node is a separate allocation
}
// Good: Contiguous memory
struct GoodList {
values: Vec<i32>, // All values in one allocation
indices: Vec<usize>, // Indices instead of pointers
}
F. Cache Locality
■ Poor Cache Usage:
struct Player {
id: u64,
name: String, // Heap allocation
position: Vec3,
inventory: Vec<Item>,
}
let players: Vec<Player> = vec![/*...*/];
■ Better Cache Locality (SoA):
// Struct of Arrays pattern
struct Players {
ids: Vec<u64>,
names: Vec<String>,
positions: Vec<Vec3>,
inventories: Vec<Vec<Item>>,
}
// Now iterating over positions is cache-friendly
for pos in &[Link] {
// Hot loop only touches position data
}
G. Cache Line Awareness
use std::sync::atomic::{AtomicU64, Ordering};
// Bad: False sharing
struct BadCounter {
counter1: AtomicU64, // Adjacent in memory
counter2: AtomicU64, // Same cache line = contention
}
// Good: Padding to separate cache lines
#[repr(align(64))] // Cache line size
struct GoodCounter {
counter1: AtomicU64,
_padding: [u8; 56], // Force different cache lines
counter2: AtomicU64,
}
Cache line size: 64 bytes on most CPUs.
H. SmallVec and SmallString
Avoid heap allocations for small collections:
[dependencies]
smallvec = "1.13"
use smallvec::{SmallVec, smallvec};
// Avoids heap allocation for ≤8 elements
let mut vec: SmallVec<[i32; 8]> = smallvec![1, 2, 3];
I. Box Large Types
// Large enum, only one variant active at a time
enum Message {
Small(u8),
Large([u8; 1024]), // Wastes 1KB per Message
}
// Better: box the large variant
enum Message {
Small(u8),
Large(Box<[u8; 1024]>), // Only 8 bytes when not Large
}
J. Reduce Struct Padding
// Bad layout (16 bytes due to padding)
struct Bad {
a: u8, // 1 byte
// 7 bytes padding
b: u64, // 8 bytes
}
// Good layout (9 bytes, 1 padding)
struct Good {
b: u64, // 8 bytes
a: u8, // 1 byte
}
Check with:
println!("Size: {}", std::mem::size_of::<MyStruct>());
7. CPU Optimization
A. Iterator Chains vs Manual Loops
Measure both approaches:
// Iterator style (usually faster due to optimizations)
let sum: i32 = [Link]()
.filter(|&&x| x > 0)
.map(|&x| x * 2)
.sum();
// Manual style (sometimes faster for complex operations)
let mut sum = 0;
for &x in &vec {
if x > 0 {
sum += x * 2;
}
}
Rule: Iterators optimize better 90% of the time. Only use manual loops if profiler shows benefit.
B. Inline Hints
Use sparingly and measure:
#[inline(always)] // Force inline (use rarely)
fn small_hot_function(x: i32) -> i32 {
x * 2 + 1
}
#[inline(never)] // Prevent inline (for profiling)
fn rarely_called_large_function() {
// ...
}
#[inline] // Suggest inline (compiler decides)
fn might_be_hot(x: i32) -> i32 {
// ...
}
When to use:
• `#[inline(always)]`: Tiny functions (<5 lines) called millions of times
• `#[inline(never)]`: Large functions, or when profiling
• `#[inline]`: Cross-crate calls that should inline
C. Bounds Check Elimination
// Compiler can't eliminate bounds check
fn sum(data: &[i32]) -> i32 {
let mut total = 0;
for i in 0..[Link]() {
total += data[i]; // Bounds check on every access
}
total
}
// No bounds checks
fn sum_fast(data: &[i32]) -> i32 {
let mut total = 0;
for &value in data { // Iterator proves bounds safety
total += value;
}
total
}
// Unsafe but fastest (use only in proven hot paths)
fn sum_unsafe(data: &[i32]) -> i32 {
let mut total = 0;
for i in 0..[Link]() {
unsafe {
total += *data.get_unchecked(i);
}
}
total
}
D. Branch Prediction
// Bad: Unpredictable branches
fn process(items: &[Item]) {
for item in items {
if item.is_special() { // Random, hard to predict
special_path(item);
} else {
normal_path(item);
}
}
}
// Good: Separate loops (better branch prediction)
fn process_optimized(items: &[Item]) {
// Process all special items
for item in [Link]().filter(|i| i.is_special()) {
special_path(item);
}
// Process all normal items
for item in [Link]().filter(|i| !i.is_special()) {
normal_path(item);
}
}
E. Loop Unrolling
// Manual unrolling for tiny hot loops
fn sum_unrolled(data: &[i32]) -> i32 {
let mut sum = 0;
let chunks = [Link]() / 4;
for i in 0..chunks {
let base = i * 4;
sum += data[base];
sum += data[base + 1];
sum += data[base + 2];
sum += data[base + 3];
}
// Handle remainder
for i in chunks * 4..[Link]() {
sum += data[i];
}
sum
}
Note: LLVM often auto-unrolls. Only do manually if profiler shows benefit.
F. Branchless Code
// With branch
fn max_branching(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
// Branchless (for hot paths)
fn max_branchless(a: i32, b: i32) -> i32 {
let diff = a - b;
let mask = diff >> 31; // -1 if a < b, 0 otherwise
b + (diff & !mask)
}
Use sparingly: Modern CPUs predict branches well. Only optimize proven hotspots.
G. Copy vs Clone
// Expensive clone
#[derive(Clone)]
struct Heavy {
data: Vec<u8>,
}
let h1 = Heavy { data: vec![0; 1000] };
let h2 = [Link](); // Allocates and copies 1000 bytes
// Cheap copy
#[derive(Copy, Clone)]
struct Light {
x: i32,
y: i32,
}
let l1 = Light { x: 1, y: 2 };
let l2 = l1; // Memcpy, no allocation
Rule: Use Copy for types ≤16 bytes with no heap data.
H. Cow (Clone-on-Write)
use std::borrow::Cow;
fn process(input: &str) -> Cow<str> {
if [Link]("old") {
Cow::Owned([Link]("old", "new")) // Allocate only if needed
} else {
Cow::Borrowed(input) // No allocation
}
}
8. SIMD Optimization
Portable SIMD (std::simd)
Requires nightly Rust:
#![feature(portable_simd)]
use std::simd::*;
fn add_arrays(a: &[f32], b: &[f32]) -> Vec<f32> {
assert_eq!([Link](), [Link]());
let mut result = vec![0.0; [Link]()];
let chunks = [Link]() / 4;
for i in 0..chunks {
let va = f32x4::from_slice(&a[i*4..]);
let vb = f32x4::from_slice(&b[i*4..]);
let vr = va + vb;
vr.copy_to_slice(&mut result[i*4..]);
}
// Handle remainder
for i in chunks*4..[Link]() {
result[i] = a[i] + b[i];
}
result
}
Auto-Vectorization
Modern LLVM auto-vectorizes simple loops:
// This often auto-vectorizes
fn scale(data: &mut [f32], factor: f32) {
for x in data {
*x *= factor;
}
}
Check with:
cargo rustc --release -- --emit=llvm-ir
cat target/release/deps/*.ll | grep vector
Libraries with SIMD
[dependencies]
packed_simd = "0.3" # SIMD library
ndarray = "0.15" # NumPy-like arrays with SIMD
9. Concurrency Optimization
Parallelism with Rayon
[dependencies]
rayon = "1.10"
use rayon::prelude::*;
// Pattern 1: Parallel map
let results: Vec<_> = input
.par_iter()
.map(|x| expensive(x))
.collect();
// Pattern 2: Parallel fold/reduce
let sum: i32 = data
.par_iter()
.map(|x| x * x)
.sum();
// Pattern 3: Parallel sort
data.par_sort();
// Pattern 4: Parallel chunks
data.par_chunks(1000)
.for_each(|chunk| process_chunk(chunk));
Lock-Free Structures
[dependencies]
crossbeam = "0.8"
use crossbeam::queue::SegQueue;
// Lock-free queue (better than Mutex<VecDeque>)
let queue: SegQueue<Task> = SegQueue::new();
// Multiple threads can push/pop without locks
[Link](task);
if let Some(task) = [Link]() {
process(task);
}
Thread Pool Tuning
use rayon::ThreadPoolBuilder;
// Custom thread pool
let pool = ThreadPoolBuilder::new()
.num_threads(8)
.stack_size(2 * 1024 * 1024) // 2MB per thread
.build()
.unwrap();
[Link](|| {
// Your parallel work here
});
False Sharing Prevention
use std::sync::atomic::{AtomicU64, Ordering};
// Bad: Adjacent atomics cause false sharing
struct Counters {
thread1: AtomicU64,
thread2: AtomicU64,
}
// Good: Pad to cache line boundaries
#[repr(C, align(64))]
struct PaddedCounter {
value: AtomicU64,
}
struct Counters {
thread1: PaddedCounter,
thread2: PaddedCounter,
}
10. I/O Optimization
Buffered I/O
use std::io::{BufReader, BufWriter, Read, Write};
use std::fs::File;
// Bad: Unbuffered (syscall per byte)
let mut file = File::open("[Link]")?;
let mut byte = [0u8; 1];
[Link](&mut byte)?; // Expensive!
// Good: Buffered
let mut reader = BufReader::with_capacity(64 * 1024, file);
let mut byte = [0u8; 1];
[Link](&mut byte)?; // Reads ahead, cached
Memory-Mapped Files
[dependencies]
memmap2 = "0.9"
use memmap2::MmapOptions;
use std::fs::File;
let file = File::open("large_file.dat")?;
let mmap = unsafe { MmapOptions::new().map(&file)? };
// Access file like a slice (no read() calls)
let data: &[u8] = &mmap[..];
Benefits:
• Zero-copy access
• OS handles paging
• Shared memory between processes
Use when: Reading large files multiple times or random access patterns.
Async I/O
[dependencies]
tokio = { version = "1", features = ["full"] }
use tokio::fs::File;
use tokio::io::AsyncReadExt;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut file = File::open("[Link]").await?;
let mut contents = vec![];
file.read_to_end(&mut contents).await?;
Ok(())
}
When to use:
• Network I/O
• Many concurrent connections
• Mixed I/O and compute
When not to use:
• CPU-bound work
• Single-threaded sequential I/O
11. Algorithm Optimization
Choose the Right Collection
Operation Use Don't Use Reason
Random access `Vec` `LinkedList` Cache locality
Queue (FIFO) `VecDeque` `Vec` No shifting
Ordered iteration `BTreeMap` `HashMap` Sorted keys
Fast lookup `HashMap` `Vec` O(1) vs O(n)
Small sets (<32) `SmallVec` `HashSet` Avoid heap
Memoization
use std::collections::HashMap;
struct Fibonacci {
cache: HashMap<u64, u64>,
}
impl Fibonacci {
fn new() -> Self {
let mut cache = HashMap::new();
[Link](0, 0);
[Link](1, 1);
Fibonacci { cache }
}
fn compute(&mut self, n: u64) -> u64 {
if let Some(&result) = [Link](&n) {
return result;
}
let result = [Link](n - 1) + [Link](n - 2);
[Link](n, result);
result
}
}
Reduce Complexity
// O(n²) - Bad
fn has_duplicates_slow(arr: &[i32]) -> bool {
for i in 0..[Link]() {
for j in i+1..[Link]() {
if arr[i] == arr[j] {
return true;
}
}
}
false
}
// O(n) - Good
fn has_duplicates_fast(arr: &[i32]) -> bool {
use std::collections::HashSet;
let mut seen = HashSet::new();
for &x in arr {
if  {
return true;
}
}
false
}
12. Compile Time Optimization
Not runtime, but improves iteration speed:
[Link]
[[Link]]
opt-level = 1 # Faster debug builds
[[Link]."*"]
opt-level = 2 # Fast dependencies, slow main code
# Use mold linker (much faster)
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
Install mold:
# Ubuntu/Debian
sudo apt install mold
# macOS
brew install mold
Sccache (Compilation Cache)
cargo install sccache
export RUSTC_WRAPPER=sccache
Caches compiled crates across projects.
13. Advanced System-Level Optimization
NUMA (Non-Uniform Memory Access)
For multi-socket servers:
# Check NUMA topology
numactl --hardware
# Bind process to specific NUMA node
numactl --cpunodebind=0 --membind=0 ./target/release/my_program
When to use: Multi-socket servers, large memory footprints, HPC workloads.
Huge Pages
Reduce TLB misses for large memory allocations:
# Enable transparent huge pages
echo always | sudo tee /sys/kernel/mm/transparent_hugepage/enabled
# Or allocate explicit huge pages
sudo sysctl -w vm.nr_hugepages=1024
Benefits:
• Fewer TLB misses
• Better memory throughput
• 5-15% speedup for large datasets
CPU Affinity
Pin threads to specific cores:
[dependencies]
core_affinity = "0.8"
use core_affinity;
fn main() {
let core_ids = core_affinity::get_core_ids().unwrap();
std::thread::spawn(move || {
// Pin this thread to core 0
core_affinity::set_for_current(core_ids[0]);
// CPU-intensive work
});
}
Use cases:
• Real-time applications
• Reduce context switching
• Predictable performance
Security Hardening vs Performance
Trade-offs between security and speed:
[[Link]]
# More secure but slower
overflow-checks = true # Runtime integer overflow checks
# Less secure but faster (default)
overflow-checks = false
ASLR Considerations
Address Space Layout Randomization affects performance slightly:
# Disable ASLR for benchmarking (not production!)
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
# Re-enable
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
Note: Never disable in production. Security > 1% performance gain.
14. Project-Specific Workflows
For CLI Tools
[[Link]]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = true
panic = "abort"
Build with PGO using common commands:
# Profile
./my_cli --help
./my_cli common_command [Link]
./my_cli another_command --option
For Web Servers
[[Link]]
opt-level = 3
lto = "thin" # Fat LTO too slow for large projects
codegen-units = 16 # Faster compile, still optimized
PGO with production traffic:
# Run instrumented server with load testing
wrk -t12 -c400 -d30s [Link]
For Number Crunching
Enable all CPU features:
[build]
rustflags = [
"-C", "target-cpu=native",
"-C", "target-feature=+avx2,+fma"
]
Consider f32 instead of f64 if precision allows (2x throughput).
For Libraries
[[Link]]
opt-level = 3
lto = true # Let downstream decide fat vs thin
codegen-units = 16
# Don't use target-cpu=native in libraries (portability)
15. Common Optimization Mistakes
■ Optimizing Without Profiling
"I think this function is slow" ≠ measured hotspot
■ Premature Optimization
Optimize AFTER code works correctly.
■ Ignoring Algorithm Complexity
O(n²) with all optimizations < O(n log n) with none
■ Using Unsafe Unnecessarily
Unsafe code is:
• Hard to review
• Easy to get wrong
• Usually not faster than safe alternatives
Only use in proven hot paths after measuring.
■ Over-inlining
Too much #[inline(always)] bloats binaries and hurts i-cache.
■ Ignoring Compilation Time
If CI builds take hours, optimization isn't worth it.
■ Needless Cloning
// Clones everywhere
fn process(data: Vec<u8>) {
worker1([Link]());
worker2([Link]());
worker3([Link]());
}
// Share with references
fn process_fast(data: &[u8]) {
worker1(data);
worker2(data);
worker3(data);
}
■ Wrong Collection Type
// LinkedList (bad for iteration)
let mut list: LinkedList<i32> = LinkedList::new();
// Vec (good for iteration)
let mut vec = Vec::with_capacity(1000);
16. Typical Performance Gains (Real-World)
Stage Typical Improvement Cumulative
Baseline debug build 1x 1x
Release build (`-O3`) 3-5x 3-5x
+ LTO +20-30% 4-6.5x
+ PGO +10-50% 4.4-9.75x
+ BOLT +5-15% 4.6-11.2x
+ Manual optimization +50-200% 6.9-33.6x
Conservative estimate: 2-4x total improvement is normal.
With effort: 5-10x is achievable.
Best case: 20x+ for specific hot paths.
17. Production Deployment Checklist
• [ ] Algorithm is optimal (right data structures)
• [ ] Benchmark baseline established
• [ ] [Link] configured with release profile
• [ ] Built with LTO
• [ ] Profiled with flamegraph/perf
• [ ] Hot paths identified
• [ ] PGO with representative workload
• [ ] BOLT optimization (if on Linux)
• [ ] Manual optimizations to hot paths only
• [ ] Benchmarked final result
• [ ] Regression tests to prevent slowdowns
• [ ] Binary stripped (`strip = true`)
• [ ] Security audit (no unsafe without review)
18. Tools Reference
Tool Purpose Install
hyperfine Benchmarking `cargo install hyperfine`
criterion Micro-benchmarks Add to [Link]
flamegraph Visual profiling `cargo install flamegraph`
perf Linux profiling `sudo apt install linux-tools-generic`
valgrind Memory/cache analysis `sudo apt install valgrind`
heaptrack Memory profiling `sudo apt install heaptrack`
llvm-profdata PGO merge `sudo apt install llvm` / `brew install llvm`
llvm-bolt Binary optimization Included in LLVM
mold Fast linker `sudo apt install mold` / `brew install mold`
sccache Compilation cache `cargo install sccache`
19. Quick Start Script
Create scripts/[Link]:
#!/bin/bash
set -e
echo "==> Stage 1: Baseline benchmark"
cargo build --release
hyperfine --export-json [Link] './target/release/my_program'
echo "==> Stage 2: PGO"
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release
rm -rf /tmp/pgo-data/*.profraw 2>/dev/null || true
./target/release/my_program data/[Link]
llvm-profdata merge -o /tmp/pgo-data/[Link] /tmp/pgo-data/*.profraw
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/[Link] -C lto=fat" cargo build --release
echo "==> Stage 3: Benchmark PGO"
hyperfine --export-json [Link] './target/release/my_program'
echo "==> Stage 4: Flamegraph"
cargo flamegraph --release -- data/[Link]
echo "==> Done! Check [Link] for hot paths"
20. When to Stop Optimizing
• Performance meets requirements ✓
• Further gains require weeks of work for <5% improvement
• Code becomes unmaintainable
• Compilation time exceeds acceptable limits
Remember: Correctness > Performance > Readability. Optimize in that order.
21. Additional Resources
• [Rust Performance Book]([Link]
• [[Link] Docs]([Link]
• [LLVM PGO Docs]([Link]
• [BOLT Paper]([Link]
s-and-beyond/)
• [Rayon Documentation]([Link]
• [The Rust Performance Book - Memory]([Link]