Zero-Cost Abstractions
Zero-cost means you do not pay for abstractions you do not use, and abstractions you do use can compile to code as efficient as hand-written loops.
Recipe
let total: u32 = data.iter().filter(|x| x % 2 == 0).map(|x| x * 2).sum();When to reach for this: Choosing between iterators, generics, and manual loops without sacrificing clarity.
Working Example
fn sum_even_double(slice: &[u32]) -> u32 {
slice.iter().filter(|n| *n % 2 == 0).map(|n| n * 2).sum()
}
fn sum_even_double_loop(slice: &[u32]) -> u32 {
let mut acc = 0;
for &n in slice {
if n % 2 == 0 {
acc += n * 2;
}
}
acc
}Inspect optimized assembly in --release; both often compile to identical machine code.
What this demonstrates:
- Iterator adapters fuse into one loop with LLVM
- Generics monomorphize to specialized code per type
Option/Resultmatch compiles without heap allocation- Prefer expressive code until profiling proves otherwise
Deep Dive
What Compiles Away
| Abstraction | Cost |
|---|---|
| Iterator chain | Often same as loop |
Option/Result | Stack-only, no boxing by default |
Generic fn | Monomorphized, inlined |
enum dispatch | Tagged union, no vtable unless dyn |
When There Is Cost
dyn Traitdynamic dispatchBox<dyn Error>in hot path- Closures capturing
String(alloc + indirect call if not inlined) - Debug assertions in debug builds only
Gotchas
- Assuming iterators always zero-cost - complex closures may not inline. Fix: inspect codegen or simplify captures.
collect()without type hint - extra passes or allocations. Fix: usesum(),try_fold, or explicit destination type.dynfor convenience - vtable per call. Fix: generics orenumdispatch in hot paths.- Debug iterator debugging - step through adapter chain is hard. Fix: expand to loop temporarily while debugging only.
- LTO off in release - cross-crate inlining fails. Fix: thin LTO in release profile.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Manual for loop | Proven hotspot needs control | Default choice over iterators |
rayon parallel iterators | CPU-bound data parallel | Async I/O code |
no_std | Embedded | Full Tokio/Axum stack |
FAQs
Iterator vs loop performance?
Usually equal in release; benchmark if critical.
Does Option allocate?
No for Option<T> where T is sized; it's a niche optimization on stack.
What is monomorphization cost?
Compile time and binary size grow with many generic instantiations; runtime per call is static dispatch.
Are closures zero-cost?
When inlined, yes; capturing env may allocate and prevent inline.
impl Trait return?
Static dispatch, zero-cost like concrete return type when not trait object.
async zero-cost?
State machine on stack/heap per task; not free but no per-poll heap alloc in well-written futures.
Compare godbolt?
Paste rustc LLVM IR or assembly for two versions side by side.
Vec iterator vs indexing?
Often identical; bounds checks may optimize away with iterators.
Polars lazy zero-cost?
Query plan optimizes pipelines; collect materializes - plan layer is lazy abstraction.
When use dyn intentionally?
Plugin boundaries, FFI, or rare cold paths where binary size matters more than call speed.
Related
- Performance Basics - measure first
- Avoiding Clones - ownership
- SIMD & Vectorization - explicit SIMD
- 30 Performance Rules - rule list
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.