Rust’s "zero-cost abstractions" mean you can write high-level, expressive code without paying a runtime performance penalty compared to writing it manually at a lower level.
Let’s see this in action with iterators. Imagine you want to sum the squares of numbers from 1 to 10.
fn main() {
let sum: i32 = (1..=10) // Create a range iterator
.map(|x| x * x) // Square each number
.sum(); // Sum the results
println!("Sum of squares: {}", sum);
}
This looks like it’s doing a lot of work: creating a range, mapping over it, and then summing. A naive C programmer might write a for loop:
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i * i;
}
The Rust code, however, compiles down to something remarkably similar to the C loop. The magic happens in two places: iterator adapters and compiler optimizations.
When you call .map(|x| x * x) on the (1..=10) range, Rust doesn’t create a new intermediate collection of squared numbers. Instead, it creates a new iterator type that wraps the original range iterator. This new iterator, let’s call it Map for simplicity, knows how to:
- Ask its underlying iterator (the range) for the next item.
- Apply the closure
|x| x * xto that item. - Return the result.
Similarly, .sum() doesn’t iterate over a pre-computed list. It calls next() on the Map iterator repeatedly, accumulating the results. This is achieved by the sum() method being generic over the Iterator trait, and the compiler specializing it for the concrete Map iterator type.
The "zero-cost" part is that the compiler is incredibly good at optimizing away the overhead of these abstractions. It sees the chain of iterator adapters and effectively inlines the logic. For our example, the compiler will likely transform the iterator chain into a single, efficient loop:
// Conceptual representation of the compiled code
let mut sum = 0;
let mut range = 1..=10; // The range iterator is optimized away
let mut current = range.next(); // Get first element
while let Some(i) = current {
sum += i * i; // The map closure is inlined
current = range.next(); // Get next element
}
The compiler can often "see through" the iterator adapters and the closures passed to them. It understands the state each iterator needs to maintain (like the current value in the range or the closure itself) and fuses them into a single, optimized loop. This process is called iterator fusion.
The key levers you control are the types of iterators you use and the closures you provide. For example, (1..=10) is a RangeInclusive iterator. .map() returns a Map iterator. .sum() is a method on the Iterator trait that uses a Fold operation internally. The compiler specializes these generic implementations for the concrete types it encounters.
Most people don’t realize that the sum() method on Iterator is not just a simple loop. It’s a highly optimized fold operation. For instance, sum::<i32>() on an iterator of i32 will compile down to a tight loop with a single accumulator variable, very similar to the manual C loop, but it works for any type that implements Add and has a Zero value (via the num crate’s traits, for example). This means you get the benefit of abstracting the summing logic without paying for it.
The next concept you’ll likely explore is how this applies to other abstractions like traits and generics, and the role of the borrow checker in ensuring these optimizations are safe.