“Zero-cost abstraction” is Rust’s most famous marketing claim. It’s mostly true, but the nuance matters when you’re chasing nanoseconds.
The claim
What you don’t use, you don’t pay for. What you do use, you couldn’t hand-code any better. — Bjarne Stroustrup (adapted for Rust)
Iterators: the canonical example
Rust iterators are lazy and monomorphised at compile time.
fn sum_squares(v: &[f64]) -> f64 { v.iter() .map(|x| x * x) .filter(|&x| x > 1.0) .sum()}Compile with --release and look at the assembly:
cargo rustc --release -- --emit asmgrep -A 20 'sum_squares' target/release/deps/*.sYou’ll see tight SIMD loops — the iterator chain evaporates. The equivalent hand-written C would look identical.
Debug builds do not apply these optimisations. Always benchmark with --release.
Closures: three flavours
Rust has three closure traits:
| Trait | Can call | Memory |
|---|---|---|
Fn | Multiple times | Immutable borrow |
FnMut | Multiple times | Mutable borrow |
FnOnce | Once | Consumes captured vars |
When a closure doesn’t escape its call site, it’s inlined and fully optimised away.
// This allocates nothing — closure is stack-onlylet multiplier = 3;let result: Vec<i32> = (0..10).map(|x| x * multiplier).collect();Where you do pay: trait objects
dyn Trait is a fat pointer (data + vtable) with a runtime dispatch cost:
// Static dispatch — zero costfn process_static<T: Processor>(p: &T) { p.run(); }
// Dynamic dispatch — vtable lookupfn process_dynamic(p: &dyn Processor) { p.run(); }The vtable indirection prevents inlining, which can inhibit LLVM’s subsequent optimisations. For tight loops, prefer generics.
Benchmarking the difference
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn bench_static(c: &mut Criterion) { let data = vec![1.0f64; 10_000]; c.bench_function("static dispatch", |b| { b.iter(|| sum_squares(black_box(&data))) });}
criterion_group!(benches, bench_static);criterion_main!(benches);On my M3 MacBook Pro, static dispatch on a 10k float slice runs at ~5µs; switching to dyn Fn adds ~40% overhead on that tight loop.
Use dyn Trait at API boundaries where flexibility matters. Avoid it in hot loops.
Takeaway
Rust’s zero-cost abstractions are real for iterators, generics, and closures. They’re not free for dynamic dispatch. The rule of thumb: if the type is known at compile time, the abstraction is free. If it isn’t, you’re paying for the flexibility.