For vs for_each

The documentation of Iterator::for_each says:

In some cases for_each may also be faster than a loop, because it will use internal iteration on adapters like Chain .

Now I am wondering what kind of cases are there and if there are simple examples of this? I feel like the advice in this documentation is a bit contradicting, i.e. it says for loops are generally more idiomatic but they might be slower. So if performance is important to me does this mean I should always use for_each instead of for loops?

2 Likes

That depends. Performance is not everything. Depending on the complexity of the code in its entirety, a plain old for-loop may contribute to readability and maintainability.
Also, if you need to e.g. break out of the outer function within the loop, Iterator::for_each() cannot be used, since it requires a closure which cannot trigger a return from the outer function.

7 Likes

Interestingly for your example, nightly produces the same ASM for for_each and for.

On stable, for_each indeed produces better code.

Would love to know the reason behind this difference.

3 Likes

Thanks for your answer. So just for me to get a feeling why this makes a difference in your example: Why wouldn't the compiler be able to do the same optimisations as it does for for_each if we do this:

    fn mask(&mut self) {
       for (byte, mask) in self.iter_mut().zip(MaskGenerator::default()) {
           *byte ^= mask
       }
    }

I am not a compiler expert, so I cannot answer that. But, as @vdrn has found out, on nightly, there seems to have been some improvement regarding the plain for loop.

In general, a loop for item in source desugars to:

let iter = source.into_iter();
while let Some(item) = iter.next() {
    ...
}

Therefore, before optimization, there must be code in the iterator's next() implementation which decides what item to produce next. In the case of chain(), that decision includes "is the first iterator exhausted? okay, use the second one". It's slow to re-check that condition every time. But, when for_each() is called, the version provided by chain() can just call the two inner iterators’ for_each() functions in sequence, so there is no check. (In practice, this actually happens through fold(), not for_each() specifically.)

In general, using chain(), fold(), and sometimes even collect() (depends on the collection type) allows the iterator to be responsible for the control flow directly, making a program that is more efficient unoptimized and more straightforward to optimize.

Now, the optimizer can sometimes figure out these things, and transform the next() version into code equivalent to the for_each() version. The point of using the specialized for_each() is in case it doesn't figure that out (or decides not to perform that transformation for whatever heuristic reason).

In general, the optimizer is going to be reliably good at taking a soup of operations and sorting it — making the best possible arrangement of various arithmetic operators, bitwise operators, comparisons, value copies, etc. It is not and cannot be as reliably good at recognizing large-scale patterns and transforming them into better large-scale patterns, because those are computationally harder to detect. Thus, there is value to be had in designing your functions to start in a better place rather than relying solely on the optimizer.

13 Likes

The usual link for this is Rust’s iterators are inefficient, and here’s what we can do about it. | by Veedrac | Medium

My usual advice: use for_each for trivial bodies (where the amount of "stuff" in the body is similar order of magnitude to the loop overhead, like .for_each(|x| a[x] += 1)), otherwise prefer for loops.

Short form of the difference: for_each takes self so it consumes the iterator, and thus there's no need for it to worry about stopping iterating in the middle (other than panicking) nor about it needing to be able to resume again later. That means it can sometimes skip a bit of state management and thereby sometimes be slightly faster.

12 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.