Rust Performance Engineering
Iterator Optimization
Rust's Iterator trait is designed around two methods with complementary performance characteristics: next(&mut self) (external iteration, where the caller pulls one item at a time) and fold / try_fold (internal iteration, where the iterator drives its own tight internal loop). Understanding how rustc and LLVM optimize iterator adapters unlocks loop fusion, single-pass allocation, and SIMD vectorization.
External Iteration (`while let Some(x) = iter.next()`):
Caller loop checks adapter state on every single step.
(Fast for simple `slice::Iter`, but slower for nested `chain` / `flatten` adapters!)
Internal Iteration (`iter.fold(init, f)` / `iter.for_each(f)` / `iter.sum()`):
Adapter drives its own specialized inner loops (`slice1` loop, then `slice2` loop)
without checking a state-machine branch on every element!
Four Rules for High-Performance Iterators
| Technique | Why It Improves Performance | Example |
|---|---|---|
1. Prefer fold / for_each / sum on complex adapters |
Adapters like .chain(...), .flatten(), and VecDeque::iter() consist of multiple contiguous segments. Their custom try_fold / fold implementations run a tight, branchless loop over each segment instead of branching on every .next() call! |
a.iter().chain(b.iter()).sum() |
2. Use chunks_exact(N) instead of chunks(N) |
chunks(N) yields slices of length 1..=N (because the last chunk may be smaller), forcing the inner loop to handle variable lengths. chunks_exact(N) guarantees every chunk has length exactly N, allowing LLVM to unroll and SIMD-vectorize the chunk body! |
data.chunks_exact(8) + .remainder() |
3. Avoid intermediate .collect::<Vec<_>>() |
Keep adapter pipelines lazy; collecting halfway through allocates and frees an unnecessary heap buffer. | Pass impl Iterator<Item = T> directly. |
4. Implement size_hint on custom iterators |
When .collect::<Vec<_>>() or Vec::extend sees an accurate (lower, Some(upper)) bound from size_hint(), it allocates the final capacity in a single heap call instead of reallocating 6–10 times. |
fn size_hint(&self) -> (usize, Option<usize>) |
Unlocking SIMD Auto-Vectorization with chunks_exact
When summing or transforming blocks of data, slice::chunks_exact(8) tells the compiler that every yielded slice has a compile-time known length of 8 (and slice::as_chunks::<8>() yields &[[T; 8]] directly!):
/// Processes 8-element blocks using `chunks_exact(8)` so LLVM knows the exact
/// block length at compile time and can vectorize the inner loop without remainder branches!
pub fn sum_u32_fast(data: &[u32]) -> u64 {
let chunks = data.chunks_exact(8);
let remainder = chunks.remainder();
let mut lane_sums = [0u64; 8];
for chunk in chunks {
for i in 0..8 {
lane_sums[i] += u64::from(chunk[i]);
}
}
let chunk_total: u64 = lane_sums.iter().sum();
let rem_total: u64 = remainder.iter().copied().map(u64::from).sum();
chunk_total + rem_total
}
fn main() {
let values: Vec<u32> = (1..=100).collect();
let total = sum_u32_fast(&values);
assert_eq!(total, 5050);
println!("Vectorized chunks_exact sum of 1..=100: {total}");
}
Vectorized chunks_exact sum of 1..=100: 5050
Implementing size_hint and ExactSizeIterator
pub struct StepRange {
current: u32,
end: u32,
}
impl Iterator for StepRange {
type Item = u32;
#[inline]
fn next(&mut self) -> Option<u32> {
if self.current < self.end {
let val = self.current;
self.current += 1;
Some(val)
} else {
None
}
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
let len = self.end.saturating_sub(self.current) as usize;
(len, Some(len))
}
}
impl ExactSizeIterator for StepRange {}
fn main() {
let iter = StepRange { current: 0, end: 50 };
assert_eq!(iter.len(), 50);
// Because `size_hint` returns `(50, Some(50))`, `.collect::<Vec<_>>()`
// allocates capacity for exactly 50 elements in a single call!
let collected: Vec<u32> = iter.collect();
assert_eq!(collected.capacity(), 50);
println!("Collected 50 items with single allocation (cap={})", collected.capacity());
}
Collected 50 items with single allocation (cap=50)