Rust Performance Engineering
Zero-Cost Abstractions
A zero-cost abstraction is a high-level language feature that obeys two principles first articulated by Bjarne Stroustrup and central to Rust's design:
- What you don't use, you don't pay for.
- What you do use, you couldn't hand-code any better.
In Rust, generic functions, trait bounds, iterator pipelines (iter().zip().map().sum()), newtype wrappers, and zero-sized type states compile down to the exact same machine instructions as hand-written, low-level imperative loops.
High-Level Expressive Rust:
`a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()`
|
v Monomorphization + Inlining + SROA
Low-Level Machine Code:
Identical (or faster due to eliminated bounds checks!) to a raw `while` index loop!
Primary Zero-Cost Abstractions in Rust
| Abstraction | Compile-Time Mechanism | Runtime Overhead |
|---|---|---|
Generics (fn foo<T: Trait>(x: T)) |
Monomorphized into specialized functions per concrete type T. |
0 bytes / 0 cycles (direct static call, often inlined). |
Iterator Adapters (map, filter, zip, fold) |
Nested iterator structs on the stack flattened by SROA and inlining into a single loop. | 0 heap allocations; often faster than for i in 0..n because slice iterators avoid per-index bounds checks! |
Newtypes (struct UserId(u64)) |
Type-checked at compile time; erased to raw u64 in MIR/LLVM IR. |
0 bytes / 0 cycles (passed in the exact same CPU register as u64). |
Option<&T> / Option<NonNull<T>> / Option<Box<T>> |
Null-pointer niche optimization (None stored as 0x0). |
0 extra bytes (size_of::<Option<&T>>() == size_of::<&T>()). |
Lifetimes ('a) and PhantomData<T> |
Verified by the borrow checker in MIR and erased completely before codegen. | 0 bytes / 0 instructions (do not exist at runtime). |
Demonstrating Zero-Cost Newtypes, Niche Options, and Iterator Fusion
use std::mem::size_of;
use std::num::NonZeroU64;
/// A strongly-typed distance in meters that prevents mixing up meters and feet at compile time,
/// while occupying the exact same 8-byte register as `f64` at runtime.
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Meters(pub f64);
/// High-level functional iterator pipeline:
pub fn weighted_sum_iter(values: &[u64], weights: &[u64]) -> u64 {
values
.iter()
.zip(weights.iter())
.map(|(&v, &w)| v.wrapping_mul(w))
.fold(0u64, u64::wrapping_add)
}
/// Low-level imperative index loop:
pub fn weighted_sum_loop(values: &[u64], weights: &[u64]) -> u64 {
let n = values.len().min(weights.len());
let mut acc = 0u64;
let mut i = 0;
while i < n {
acc = acc.wrapping_add(values[i].wrapping_mul(weights[i]));
i += 1;
}
acc
}
fn main() {
// 1. Verify zero size overhead of Newtypes and Niche-Optimized Options:
assert_eq!(size_of::<Meters>(), size_of::<f64>());
assert_eq!(size_of::<Option<NonZeroU64>>(), size_of::<u64>());
assert_eq!(size_of::<Option<&Meters>>(), size_of::<*const Meters>());
// 2. Verify high-level iterator pipeline produces identical results to manual loop:
let vals = [10u64, 20, 30, 40];
let wgts = [1u64, 2, 3, 4];
assert_eq!(
weighted_sum_iter(&vals, &wgts),
weighted_sum_loop(&vals, &wgts)
);
println!("Weighted sum: {}", weighted_sum_iter(&vals, &wgts));
}
Weighted sum: 300
When an Abstraction Is Not Zero-Cost
Not every convenient feature in Rust is zero-cost; knowing the boundary between zero-cost static abstractions and runtime-cost abstractions prevents accidental bottlenecks:
Box<dyn Trait>/&dyn Trait: Carries a vtable pointer indirection (and heap allocation forBox), which can inhibit inlining and loop auto-vectorization inside tight inner loops.Rc<RefCell<T>>/Arc<Mutex<T>>: Performs runtime reference-count updates and borrow-flag or atomic lock checks.iter().collect::<Vec<_>>()in the middle of a pipeline: Allocates a temporary heap buffer. Keep iterator chains lazy until the final consumer!