From 30e79db12ae06794894060ca475dfe6450579233 Mon Sep 17 00:00:00 2001 From: Scott McMurray Date: Thu, 6 Jun 2024 12:49:31 -0700 Subject: [PATCH] Reword the caveats on `array::map` Thanks to 107634 and some improvements in LLVM (particularly `dead_on_unwind`), the method actually optimizes reasonably well now. So focus the discussion on the fundamental ordering differences where the optimizer might never be able to fix it because of the different behaviour, and encouraging `Iterator::map` where an array wasn't actually ever needed. --- library/core/src/array/mod.rs | 55 ++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/library/core/src/array/mod.rs b/library/core/src/array/mod.rs index 05874ab6c4cbb..df7722fe2e07c 100644 --- a/library/core/src/array/mod.rs +++ b/library/core/src/array/mod.rs @@ -462,20 +462,47 @@ impl [T; N] { /// /// # Note on performance and stack usage /// - /// Unfortunately, usages of this method are currently not always optimized - /// as well as they could be. This mainly concerns large arrays, as mapping - /// over small arrays seem to be optimized just fine. Also note that in - /// debug mode (i.e. without any optimizations), this method can use a lot - /// of stack space (a few times the size of the array or more). - /// - /// Therefore, in performance-critical code, try to avoid using this method - /// on large arrays or check the emitted code. Also try to avoid chained - /// maps (e.g. `arr.map(...).map(...)`). - /// - /// In many cases, you can instead use [`Iterator::map`] by calling `.iter()` - /// or `.into_iter()` on your array. `[T; N]::map` is only necessary if you - /// really need a new array of the same size as the result. Rust's lazy - /// iterators tend to get optimized very well. + /// Note that this method is *eager*. In terms of `Iterator` methods, it's + /// more like `.map(…).collect()`, since it returns a new array. + /// + /// That means that `arr.map(f).map(g)` is, in general, *not* equivalent to + /// `array.map(|x| g(f(x)))`, as the former calls `f` 4 times then `g` 4 times, + /// whereas the latter interleaves the calls (`fgfgfgfg`). + /// + /// A consequence of this is that it can have fairly-high stack usage, especially + /// in debug mode or for long arrays. The backend may be able to optimize it + /// away, but especially for complicated mappings it might not be able to. + /// + /// If you're doing a one-step `map` and really want an array as the result, + /// then absolutely use this method. Its implementation uses a bunch of tricks + /// to help the optimizer handle it well. + /// + /// However, in many cases you can instead use [`Iterator::map`] by calling + /// `.iter()` or `.into_iter()` on your array. Rust's lazy iterators tend to + /// get optimized very well. + /// + /// For example, rather than doing an array-to-array map of all the elements + /// in the array up-front and only iterating after that completes, + /// + /// ``` + /// # let my_array = [1, 2, 3]; + /// # let f = |x: i32| x + 1; + /// for x in my_array.map(f) { + /// // ... + /// } + /// ``` + /// + /// It's often better to use a lazy iterator map like this + /// + /// ``` + /// # let my_array = [1, 2, 3]; + /// # let f = |x: i32| x + 1; + /// for x in my_array.into_iter().map(f) { + /// // ... + /// } + /// ``` + /// + /// so that the elements are mapped-then-processed one at a time instead. /// /// /// # Examples