0% found this document useful (0 votes)
5 views32 pages

Rust Optimization

The document outlines a comprehensive Rust optimization pipeline aimed at enhancing the performance of Rust programs through various techniques and tools. It details the critical steps for optimization, including algorithm selection, LLVM optimization flags, and profiling methods, while emphasizing the importance of measuring performance before and after optimizations. Additionally, it covers advanced topics like Profile-Guided Optimization (PGO) and Binary Optimization and Layout Tool (BOLT), along with memory optimization strategies and custom allocators.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views32 pages

Rust Optimization

The document outlines a comprehensive Rust optimization pipeline aimed at enhancing the performance of Rust programs through various techniques and tools. It details the critical steps for optimization, including algorithm selection, LLVM optimization flags, and profiling methods, while emphasizing the importance of measuring performance before and after optimizations. Additionally, it covers advanced topics like Profile-Guided Optimization (PGO) and Binary Optimization and Layout Tool (BOLT), along with memory optimization strategies and custom allocators.
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

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 ![Link](x) {
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]

You might also like