19.7Loops vs iterators
You can now write the same computation two ways: a hand-rolled for loop with a mutable accumulator, or an iterator pipeline of adapters and a consumer. The previous lesson (19.6) showed they're the same machinery underneath, so the choice between them is real but it isn't about capability. This lesson settles the two questions everyone asks: which is faster, and which should you write?
The zero-cost claim
Rust makes a strong promise about iterators, often phrased as a zero-cost abstraction: using the high-level pipeline costs nothing at runtime compared to the low-level loop. The closures, the map, the filter, all of that apparent machinery compiles away. The optimizer inlines each adapter's next, sees through the closures, and emits the same instructions a careful hand-written loop would. You pay for the abstraction at compile time, in the optimizer's effort, not at run time.
This is why lesson 0.10 warned that the dev-versus-release gap is largest for iterator-heavy code. In a debug build none of that inlining happens, so the pipeline really does carry overhead and runs much slower than the loop. In a release build (cargo build --release) the optimizer collapses it, and the two converge. Any timing of iterator code in a debug build is measuring the abstraction before it's been made free, which is to say, measuring nothing useful.
A benchmark
Let's see it. Here's a program that sums the squares of the even numbers from 0 to 100 million, written both ways, and times each. (Timing code with std::time::Instant is the practical descendant of lesson 0.10's benchmarking; std::hint::black_box keeps the optimizer from deleting the whole computation as unused.)
use std::time::Instant;
use std::hint::black_box;
fn main() {
let n: u64 = 100_000_000;
let start = Instant::now();
let mut loop_sum: u64 = 0;
for i in 0..n {
if i % 2 == 0 {
loop_sum += i * i;
}
}
println!("loop: {} in {:?}", black_box(loop_sum), start.elapsed());
let start = Instant::now();
let iter_sum: u64 = (0..n).filter(|i| i % 2 == 0).map(|i| i * i).sum();
println!("iterator: {} in {:?}", black_box(iter_sum), start.elapsed());
}
Built in release mode, the two timings come out essentially identical, within measurement noise of each other:
loop: 333333328333333350000000 in 31.71ms
iterator: 333333328333333350000000 in 31.69ms
(Exact numbers vary by machine and run; the point is that the two lines match, not the specific milliseconds.) Same answer, same time. The iterator version, three method calls and two closures, compiled to the same loop. If you built this in debug mode instead, the iterator line would be several times slower, the zero-cost promise is a release-mode promise.
Always benchmark in release mode
cargo run and cargo test use the unoptimized dev profile. Timing iterator code there will tell you iterators are slow, and you'll believe a lie. Benchmark with cargo run --release (or a benchmarking tool that builds optimized). This is the single most common way people convince themselves Rust's abstractions are expensive, they measured the debug build. The zero-cost claim is true, but only after the optimizer has run.
So which do you write?
Since performance is a wash in release builds, the decision is about clarity, and clarity depends on the task. The honest guidance:
Prefer the iterator pipeline when the computation is a transformation: take a collection, keep some, change each, combine into a result. "Sum the squares of the evens" reads beautifully as filter().map().sum(), a description of the what, with no index, no accumulator to initialize, no off-by-one to get wrong. The pipeline also makes a whole class of bugs unwriteable: there's no loop counter to mismanage and no early-mutation of the accumulator. For map/filter/reduce shapes, the pipeline is usually the clearer and safer choice, and it's what experienced Rust programmers reach for by default.
Prefer the explicit loop when the body is doing something irregular: complex control flow with break/continue on intricate conditions, mutating several things at once, side effects like printing or writing files where there's no value being built, or logic that simply doesn't fold neatly into "transform each element." Forcing such a body through fold and a tangle of closures is a real anti-pattern, it's less readable than the loop, and readability was the only thing on the table. A for loop is not a failure to use iterators; it's the right tool when the work isn't a pipeline.
The deciding question is: is this a data transformation, or a procedure? Transformations want pipelines; procedures want loops. Most experienced Rust reaches for the pipeline first and falls back to the loop when the body gets gnarly, but neither is "more advanced", they compile to the same thing, and the goal is the version a future reader understands fastest.
Best practice
Default to an iterator pipeline for map/filter/reduce-shaped work, where it reads as a description of the result and rules out index bugs. Drop to an explicit for loop when the body has irregular control flow, multiple side effects, or simply doesn't express cleanly as a chain. Optimize for the reader, not for looking clever in either direction; performance is identical in release builds.
Quiz time
Question #1
What does "zero-cost abstraction" mean for iterators, and what's the one condition on the claim?
Show solution
It means an iterator pipeline compiles to the same machine code as the equivalent hand-written loop, the adapters and closures are inlined away by the optimizer, so there's no runtime penalty for the higher-level style. The condition is that it's a release-build property: in a debug build the optimizations don't run, and the pipeline really is slower. You must benchmark with optimizations on (--release) to see the abstraction become free.
Question #2
A colleague benchmarks an iterator chain with cargo run, finds it much slower than a loop, and concludes iterators are slow. What's the flaw?
Show solution
cargo run builds the unoptimized dev profile, where the iterator's adapters and closures aren't inlined, so the abstraction carries real overhead that the optimizer would otherwise remove. They measured the debug build. Rebuilding with cargo run --release makes the optimizer collapse the pipeline into the same code as the loop, and the timings converge.
Question #3
Give one situation where a plain for loop is the better choice over an iterator pipeline, even though both perform identically.
Show solution
Any case where the loop body isn't a clean transformation: irregular control flow with break/continue on complex conditions, mutating several variables at once, or running side effects (printing, writing files) with no value being accumulated. Cramming such logic into fold and nested closures is less readable than a direct loop, and readability is the whole point once performance is equal. A for loop is the right tool for procedures; pipelines are the right tool for transformations.
That closes the chapter's material. The summary and quiz (19.x) pull closures and iterators together and put the whole toolkit to work on a capstone.