Iterator Adapters and Performance
Iterator adapter chains in Rust are a prime example of a zero-cost abstraction: because each adapter is a generic struct parameterized over the concrete types of the inner iterator and closure, the compiler monomorphizes and inlines the entire chain into a single loop with no virtual calls or intermediate heap allocations.
How Adapter Chains Compile to a Single Loop
When you chain .iter().filter(p).map(f).sum(), the nested type is:
Map<Filter<std::slice::Iter<'_, i32>, ClosureP>, ClosureF>
Because every next() call in that nested struct is statically known, LLVM inlines Sum::sum -> Map::next -> Filter::next -> Iter::next and fuses them into a single machine loop equivalent to—or faster than—a hand-written for loop.
Bounds-Check Elimination and Auto-Vectorization (SIMD)
Iterating via slice iterators (slice.iter(), slice.chunks_exact(4), or std::iter::zip) often optimizes better than manual index loops (for i in 0..slice.len() { slice[i] }):
| Aspect | Manual Indexing (slice[i]) |
Slice Iterator (slice.iter()) |
|---|---|---|
| Bounds checking | Checked on slice[i] unless the optimizer proves i < slice.len() |
Pointer advances directly from ptr to end_ptr; no index bounds checks |
| Aliasing & SIMD | Harder for LLVM to prove across multiple indexed slices | chunks_exact and zip expose exact trip counts, enabling SIMD auto-vectorization |
pub fn dot_product_iter(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
}
fn main() {
let x = [1.0_f32, 2.0, 3.0, 4.0];
let y = [2.0_f32, 3.0, 4.0, 5.0];
println!("Dot product: {}", dot_product_iter(&x, &y));
}
Dot product: 40
Internal Iteration (fold / for_each) vs. External Iteration (next)
For chained or nested iterators such as Chain<A, B> or FlatMap, calling .next() repeatedly must check which inner state is active on every single element. By contrast, consumer methods built on internal iteration—.fold(), .try_fold(), .for_each(), and .sum()—run separate tight loops over A and then B without branching on state per element:
fn main() {
let first = [1_i64, 2, 3];
let second = [4_i64, 5, 6];
// `.sum()` delegates to `.fold()`, exhausting `first` in one tight loop
// and then `second` in a second tight loop without per-element state branches:
let total: i64 = first.iter().chain(second.iter()).sum();
println!("Chain sum: {total}");
}
Chain sum: 21
Pre-Allocation via size_hint and TrustedLen
When .collect::<Vec<T>>() is called on an iterator chain whose adapters preserve exact length (such as slice.iter().map(f).enumerate()), Vec::from_iter reads size_hint(), allocates the exact final capacity once, and writes elements directly into the buffer without intermediate resizing.