Iterator Chains Versus Loops in Rust and Their Real Speed
Rust iterator chains compile to the same machine code as hand-written loops in most cases. See where they tie, where they differ and the few places where a plain loop is clearer or faster.

The folk belief is that iterators are elegant and loops are fast. In Rust the belief is mostly wrong: the optimizer treats them alike, and the differences are about what each form lets you say. This post compares the two on a sum, lists where the claim fails and how to check on your own code. It sits in the performance cluster after Rust performance basics.
Rust 1.90, edition 2024.
The same computation twice
pub fn sum_even_squares_loop(v: &[u32]) -> u64 {
let mut total = 0u64;
for &x in v {
if x % 2 == 0 {
total += (x as u64) * (x as u64);
}
}
total
}
pub fn sum_even_squares_iter(v: &[u32]) -> u64 {
v.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| (x as u64) * (x as u64))
.sum()
}With --release, both compile to a loop with one branch or a select, and often a vectorized version of it. You can confirm with cargo asm or on the Compiler Explorer: the two functions typically produce the same instructions. The chain is not creating three objects and calling next three times per element at run time; those structs are inlined away.
Where they differ
Debug builds. At opt-level = 0, each adapter is a real function call, and the chain can be many times slower than the loop. This is why profiles of debug builds mislead; see release versus debug.
Intermediate collections.
let squares: Vec<u64> = v.iter().map(|&x| (x as u64).pow(2)).collect();
let total: u64 = squares.iter().sum();The collect allocates and writes N values, then reads them back. The fused form does neither. If you see collect in the middle of a pipeline, ask whether the next stage can consume the iterator directly.
Quadratic helpers.
for i in 0..s.len() {
let c = s.chars().nth(i);
}nth on Chars walks from the start each time, so the loop is O(n²) in both styles. The fix is for c in s.chars().
Early exit with complex state. A loop with break, several mutable variables and a continue is often clearer than try_fold, though both compile well.
Helping the optimizer
Iterators over slices let the compiler prove indices are in range, which removes bounds checks; the explanation is in bounds checks. Using zip on two slices and chunks_exact(4) often unlock vectorization. fold and sum over f32 do not vectorize without -ffast-math-style reordering, because floating-point addition is not associative, so the compiler keeps the order.
Choosing
- Pick the form that states the intent. Transformations and filters read well as chains.
- Switch to a loop if the closure bodies need several
?, early returns or mutation of shared state. - Never choose by assumption. Write the benchmark with representative data and compare (benchmarking pitfalls).
Limits
The equivalence holds for code the optimizer can see. Iterators behind dyn Iterator or Box<dyn Iterator> use virtual calls, and then a loop over a concrete type is faster. See static versus dynamic dispatch. The Rust book includes a performance comparison in comparing loops and iterators.


