Change:
// compile-flags: -C opt-level=3 -Z merge-functions=disabled // only-x86_64 #![crate_type = "lib"] <del> #![feature(array_zip)] </del> // CHECK-LABEL: @auto_vectorize_direct #[no_mangle]

Discussion:
Today for no reason in particular I was curious about the behaviour of arrays when zipped together and then mapped into a single array, roughly: I wrote some tests for this and disassembled them in Godbolt to see what was going on behind the scenes and wow was a whole lot going on that seemed like it didn't need to be. So I wrote something kinda like what's in the stdlib for mapping and zipping arrays, just without the iterators, to see if I could do any better. Turns out I absolutely can: Maybe the design isn't as optimal (or as safe) as it could be, but it performs perfectly well in benchmarks (read: as fast as an unguarded version and anywhere between 3-25 times faster than the definition shown at the top of this issue. Here's a rough table of execution times averaged over the primitive ops (, , &|^`): ! Not really liking how things are looking for . So, question then is: is there something in Rust that needs to change to let these optimisations happen, or do I need to look at putting this in a library somewhere (or maybe stdlib wink wink nudge nudge?)? *The 25x comes from the benchmarks. benches at 11.230ns, and benches at 452.593ps, which seems suspicious but I can't seem to find anything wrong with the result.
I suspect the issue here is that has the wrong shape. It returns but vector instructions generally want . isn't an iterator type that describes the desired iteration and then performs it once, rather it's one loop that creates an intermediate result and then a second loop that produces the final output. Perhaps we could massage the code into a shape that the optimizer can disentangle, but I think it would be better to offer something like your , albeit with a nicer name.
This is missing an actual reproducer... Tried a few sizes here: I assume the report is about the case where this doesn't get unrolled, like with N=128?
I'm not entirely sure what you're looking for in a reproducer, but I have a benchmark program set up using to check out the performance of the various functions (see the "Benchmark" section at the bottom of this post). More or less I have as shown above (renamed to ), and both of these: Then I set up a lot of benchmarks.

These test the speed of some primitive operations (, , , , , ) on arrays of lengths 8, 16, 32, 64, 128, 256, and 512 for types u8, u16, u32, and u64. This gives a total of 168 benchmarks per function, which reduces down to 28 per test after the primitive tests are averaged for each function, length, and type. The results of this are: ! This was run on the latest nightly as of 2022-12-21. I think this is pretty hard evidence that performance using is not where it should be. and are grouped so close together in the bottom section of the graph you can't visually see the difference. But for ? It's very obviously slower than the other two. Notice how it takes more time to map together two than it takes the other methods to map together two . Even if we just look at shorter inputs like was suggested, it doesn't look good for : ! These are just the results for lengths up through 64, and we can pretty clearly see that is just always slower. <details<summary<strongBenchmark</strong</summary<p: </p</details
Part of this is the classic "too many copies" trio But some of it is probably just that, being eager, it won't optimize unless it fully unrolls.

<!-- Thank you for creating a tracking issue! Tracking issues are for tracking a feature from implementation to stabilization. Make sure to include the relevant RFC for the feature if it has one. If the new feature is small, it may be fine to skip the RFC process. In that case, you can use use in your initial implementation PR. The reviewer will ask you to open a tracking issue if they agree your feature can be without an RFC. --Feature gate: This is a tracking issue for the method on arrays. <!-- Include a short description of the feature. --Just like combines two iterators into one, returns a new array where every element is a tuple where the first element comes from the first array, and the second element comes from the second array. In other words, it zips two arrays together, into a single one. <!-- For most library features, it'd be useful to include a summarized version of the public API. (E.g. just the public function signatures without their doc comments or implementation.) --<!-- In the simplest case, this is a PR implementing the feature followed by a PR that stabilises the feature. However it's not uncommon for the feature to be changed before stabilization. For larger features, the implementation could be split up in multiple steps. --- [x] Implementation: [ ] Stabilization PR <!-- Include any open questions that need to be answered before the feature can be stabilised. If multiple (unrelated) big questions come up, it can be a good idea to open a separate issue for each, to make it easier to keep track of the discussions. It's useful to link any relevant discussions and conclusions (whether on GitHub, Zulip, or the internals forum) here. --
Could you show two use cases?
What made me open the PR was for doing element wise operations on vector-like objects without the need for any heap allocations or unnecessary zeroing. As for a second use case, well I will have to think about that one... Maybe bit far-fetched but here goes: (which I suppose is pretty much the same use case but with different names)
Wouldn't this become obsolete after ?
No because iterators don't encode the length in the type, so if you intend to go back to an array you'd have to unwrap.
Can't we give arrays a different iterator that encodes the length too?

:-)
Wouldn't that be problematic in combination with methods like or etc?
Yeah. We can't have a "filter" there. So only some iterators are possible. But "skip" is doable as long as the number of items to skip is given as a const generics value (and in my code the argument of "skip" is often a value known at compile-time).
Would that be/work with or something completely different like new trait? Just asking since it seems like might be getting stabilized
I have started doing some experimenting . Atleast to me this feels like a much more reusable and flexible solution than adding all of those of methods directly to .
As somebody who has stumbled upon this unstable method while implementing some const generic vector operations, I would like to point out something regarding the presented use case. To actually efficiently lift traits like as component-wise operations, one would rather need a operation combining and , maybe with some other name more consistent with rust conventions. Using the unstable from on would potentially write the array twice, at least given the current implementation.
any thoughts on something like ? Edit: There is an for vector addition
This method has ended up being incredibly useful in my fixed-array audio DSP library, mainly in frame addition/multiplication. It allows me to do an element-wise binary operation without needing to use iterators (non-const? pah!) or having to create an incrementally-updated output array. The latter is especially nice in cases where the output type is expensive to instantiate: having to create instances for a temporary output buffer that are immediately going to be overwritten is wasteful. Something I'd love to suggest would be a corresponding method, the inverse operation of . I imagine its signature looking like something along the lines of: I'd be happy to work on an impl of if others think it would be a useful method. I'd personally find quite a few good spots to use this in that DSP library.
can we get rid of and make return a fixed iterator (which is an iterator as well)?
The problem is that regular s shrink every time you call next() on them (unless they are infinite). So as far as I can tell, that makes it problematic (impossible?) to make a useful type that is both and (I would be happy to be proven wrong :) ). Note however that does implement .
hmm, yes it looks impossible.

I was dreaming of an iterator with a , which and similar methods are only for (unknown sizes) but map and zip and ... can handle sized ones as well as unknowns. But it still needs two methods, one with size for using next, and one with known size (or it can be generic but it will need explicit type annonations which is worse)
I think such a fixed iterator would have to implement a separate iterator trait, but that trait could have a method that converts it into a normal iterator. For instance, the fixed iterator could have methods that operate on it that transform it from one fixed length to another, and it could lazily evaluate to an array (no at all). However, the fixed iterator could have a method that converts it into a regular iterator. For instance:
// Then you could either turn the mapped fixed iterator into an array let doublearr: [i32; 3] = (); Now that method needs to be named something like "lazymap()"
Yes, I suppose it is too late to create a lazy array trait with a method at this point.
In has been reported to be slow. mentions that kind of pattern as motivation. Does anyone actually need the output? Or would a (or the iterfixed crate) cover most needs?
I'm currently working on the API which conflicts with the proposed API here. The API we're working on would work as a counterpart to our , but providing a different output ordering: In particular because I believe the plan is still to e.g. enable to work for arrays - which means what's provided by this API may be achievable through other means as well. In comparison: being able to zip arrays, tuples, and vecs through a shared API may be a lot harder, if not impossible to achieve through other means. I'm not trying to say that just because we're experimenting with something, this API should be abandoned. But I'm raising this so that when a discussing pops up about potentially stabilizing this API, the fact that it would shut out (parts of [^parts]) the other design should be weighed into consideration. [^parts]: It would certainly be possible to implement just on tuples and vecs, but tbh that would be asymmetrical with how we're looking at implementing and potentially as well. as defined in this unstable tracking issue is the only API which would present a conflict, hence I'm raising this issue here.

does, in my opinion, look a lot nicer than :)
Oh wait is for iterators which is not really the same thing as this issue... Sorry for the confusion. Guess the question/concern still stands...
The Nalgebra crate has a method for its vector types. That would be more useful than the currently proposed , because you can use to map to an array of tuples anyway. It may also be worth investigating whether it's possible to extend this to any number of arrays in a single "zip", for example the ability zip 3 arrays, 4 arrays, or any number of arrays . Nalgebra has for two arrays and for three, but doesn't go any further than that and has no generic solution.
There should also be a function that can be used with functions such as .
The usefulness of tools like was beautifully expressed in this issue about : "It regularly happens to me that I need to create an array not because that's the end product of my computation, but because that's a useful intermediate object when going from dynamic-sized entities like iterators, slices, or arbitrary generator functions, to static-sized entities like SIMD types or loops that must be unrolled." So I agree with the other remarks suggesting that we don't really want this function, especially if it optimizes poorly. It's natural for iterators which have much more dynamic qualities, but we are building up a kind of "static iteration" API here. That has different needs, even if it shares some idioms (e.g. ). While I obviously have a bias towards explicit SIMD due to my project work, I still would hope that whatever we picked very naturally autovectorized in some cases, since that's what people expect. And the problem with this function is that it actually changes the shape, eagerly, whereas iterators, being lazy, instead change what will be yielded.
As the original author of this sentence, I fully agree that many of the situations where I use arrays are really workarounds for lack of iterators whose size is known at compile time, which is what I would really want. But as someone else pointed out, static-sized iterators as we know Iterator cannot be made to work, since changes the length and thus invalidates the trait's contract, without changing the type since it only takes .

I have spent some time in the past thinking about how to make a dedicated trait for this use case, assuming infinite type system features, and reached the conclusion that making it work with a method as we know and love it would require massive code bloat and very poor API ergonomics. You would want to consume self and return alongside the output item, and then would need to use an extra-weird kind of specialization that changes a function's output type in order to handle specially (other impls must return an item on , whereas this one must not do so). So most likely we would not want to have a API, but instead use a different iteration design, where the core iteration primitive is not "consume one value", but "consume all remaining values at once". Perhaps something like with an unsafe contract (in the sense) that the underlying callable will be called exactly N times could be a better basic building block? Or perhaps people would be happier with something that switches back to the dynamic-sized world at the specific time where iteration starts, along the lines of ? Though in my experience it can be harder to make LLVM optimize the latter well. Whatever it is, in order to be considered successful, I think that core primitive should not require_ collecting into an array on every iterator adapter (which LLVM has often trouble optimizing out) nor require instantiating N different types during use. Though of course, into an array should be an option for end-user convenience.
The problem with that is that things can panic in the middle and then you need a partial drop. We have the same issue with on owning iterators. This is solvable by adding a post-iteration cleanup method that tells the adapter chain how many elements have actually been consumed but it means that step-by-step execution is indeed unsafe. If we relax that to "at most N" we can use something like instead and the source would have to keep track of how many items have been consumed. This is also unsafe but less onerous on the caller because they don't need a panic-safe drop guard. The downside is that it doesn't optimize well for more complex things like Zip where now multiple sources would have to keep track of how many items have been consumed. The 3rd alternative is to instead implement something like which are implemented by building up something like a stream-evaluator (called StreamPipeline internally) instead of adapters.

The evaluator is then in charge of executing final operation. Since it's all owned by the same module it can have internal methods that can interrogate the built-up structure to see what the optimal execution strategy is and choose between different ones depending on its overall shape. It can also do optimizing transformations before evaluation (e.g. eliminating a sort step if a later step isn't order-preserving). The downside of that approach is that it makes it more difficult for 3rd parties to extend the interface. We do something similar for std-owned types by using specialization but since that has to happen cooperatively between many separate structs it's more complicated than having a central evaluator. Anyway, that's a bit of a tangent...
So, I'll repeat my : Is there anyone who's in favor of keeping as it is today or can we rip it out and then consider replacements separately?
see my , do you have any thoughts on that? Edit: Just saw you mentioned it earlier.
Nominating this for removal since optimizes poorly and seems to be rarely what's really wanted.
close
Team member has proposed to close this. The next step is review by the rest of the tagged team members: [x] [ ] [x] [ ] [x] No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See for info about what commands tagged team members can give me.
FWIW, this now at least codegens decently well for basic cases. For example, gives a simd subtraction, with no extra ceremony But I do also think that a is probably what I'd always actually want here, because eagerly getting tuples isn't particularly useful.
:bell: This is now entering its final comment period, as per the . :bell:
The final comment period, with a disposition to close, as per the , is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.