Rust Performance Engineering
Cache Locality
Modern CPU cores execute arithmetic instructions in under 0.5 ns, whereas fetching a cache line from main DRAM takes ~60–100 ns—a 150x speed gap. Cache locality designs data structures and traversal orders so the CPU's L1/L2/L3 caches and hardware prefetcher always have the next required bytes ready before the execution pipeline stalls.
CPU Memory Hierarchy Latency (Typical 64-Bit Core):
Registers : ~0.3 ns (Immediate)
L1 Data Cache : ~1.0 ns (32 - 128 KiB per core)
L2 Cache : ~3.5 ns (512 KiB - 4 MiB)
L3 Shared Cache : ~12.0 ns (16 - 64+ MiB)
Main DRAM : ~80.0 ns (Pointer-chasing cache miss penalty!)
Temporal vs Spatial Locality
- Spatial Locality: Accessing memory addresses that are physically adjacent (
slice[i],slice[i + 1]). Because a single cache fetch loads 64 or 128 contiguous bytes, readingslice[0]automatically bringsslice[1..16](foru32) into L1 cache for free, and linear access triggers the CPU's hardware stride prefetcher! - Temporal Locality: Reusing the same small working set of memory addresses repeatedly while it is still resident in L1 or L2 cache.
| Data Structure | Memory Layout | Cache Misses During Linear Scan |
|---|---|---|
Vec<T> / [T; N] / VecDeque<T> |
Contiguous block of T values inline |
Minimal ($1$ miss per $64 / \text{sizeof}(T)$ elements; hardware prefetcher hides latency). |
Vec<Box<T>> / LinkedList<T> |
Each element is a separate heap allocation at an arbitrary RAM address | Severe (Up to $1$ L1/L2/DRAM cache miss per element due to pointer chasing!). |
Row-Major 2D Matrix (vec![0.0; rows * cols]) |
Contiguous flat buffer indexed by r * cols + c |
Minimal when iterated row-by-row (for r { for c }); severe if iterated column-by-column! |
Why Row-Major Loop Order Outperforms Column-Major Order by 5x–15x
In Rust, a flat 2D grid stored as grid[row * width + col] (or [[f64; W]; H]) lays out elements of the same row contiguously in memory.
- Row-major traversal (
for r in 0..height { for c in 0..width }— inner loop incrementscby1) steps through memory sequentially (+8bytes per step), achieving 100% cache-line utilization and SIMD auto-vectorization. - Column-major traversal (
for c in 0..width { for r in 0..height }— inner loop jumps by+width * 8bytes per step) skips across cache lines, wasting 7 out of 8f64values on every cache line load!
pub struct Matrix {
rows: usize,
cols: usize,
data: Vec<u64>,
}
impl Matrix {
pub fn new_filled(rows: usize, cols: usize, val: u64) -> Self {
Self {
rows,
cols,
data: vec![val; rows * cols],
}
}
/// Cache-friendly row-major accumulation: reads contiguous memory sequentially.
pub fn sum_row_major(&self) -> u64 {
let mut total = 0u64;
for row in self.data.chunks_exact(self.cols) {
for &val in row {
total = total.wrapping_add(val);
}
}
total
}
/// Cache-friendly matrix transpose using cache-oblivious block tiling (`TILE = 16`).
pub fn transpose(&self) -> Self {
const TILE: usize = 16;
let mut out = vec![0u64; self.rows * self.cols];
for r_block in (0..self.rows).step_by(TILE) {
for c_block in (0..self.cols).step_by(TILE) {
let r_end = (r_block + TILE).min(self.rows);
let c_end = (c_block + TILE).min(self.cols);
for r in r_block..r_end {
for c in c_block..c_end {
out[c * self.rows + r] = self.data[r * self.cols + c];
}
}
}
}
Self {
rows: self.cols,
cols: self.rows,
data: out,
}
}
}
fn main() {
let mat = Matrix::new_filled(64, 128, 3);
let transposed = mat.transpose();
assert_eq!(mat.sum_row_major(), 64 * 128 * 3);
assert_eq!(transposed.rows, 128);
assert_eq!(transposed.cols, 64);
println!("Row-major sum: {}", mat.sum_row_major());
}
Row-major sum: 24576