-
Notifications
You must be signed in to change notification settings - Fork 12.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Iterator::map_windows
#82413
Add Iterator::map_windows
#82413
Conversation
r? @sfackler (rust-highfive has picked a reviewer for you, use r? to override) |
buffer.rotate_left(1); | ||
buffer[N - 1] = next; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is slightly wasteful in theory: the first element does not need to be copied to the last slot as it's overwritten later anyway. I expect this to be optimized out anyway, but I haven't checked.
I haven't found a better non-unsafe
way to do this. copy_within
requires T: Copy
, swap
would work, but I doubt doing a bunch of swaps
is faster than the current version. Of course, the unsafe
would be straight-forward but I still wanted to avoid it for now. Any ideas or opinions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using unsafe in std lib is acceptable if it leads to a performance advantage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to just write to the first element then rotate?
Edit: On reflection I think this is probably not that costly either way, have to look at godbolt for it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Writing to the first element doesn't seem to change that much compared to the current approach.
I played a bit on godbolt and I found 3 situations:
- They're optimized almost the same way. This often happen for tiny
T
s, but sometimes also for bigger ones. - The
unsafe
version may be optimized to 4-5x less assembly. This often happen for biggerT
s but smallN
. - The
unsafe
version is optimized to amemmove
call and the safe version to either a lot of moves or a loop. This often happen for biggerT
s and biggerN
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that, if .rotate_right(1)
and .rotate_left(1)
don't optimize to the obvious memmove
, then that's a bug in those methods that should be fixed.
(I vaguely recall someone on Discord was going to file me a bug about this...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@SkiFire13 Thanks for testing this stuff. But I agree with @scottmcm in that this should probably optimize to the optimal code basically. The implementation of this can always be changed later, so I wouldn't say it is hugely important to get this as efficient as possible in this PR. Especially before we even agreed we want this method.
This comment has been minimized.
This comment has been minimized.
35b6692
to
acaf8a6
Compare
This comment has been minimized.
This comment has been minimized.
There are a few places where this can be done more efficiently: |
Wouldn't this be the same as IMO a great advantage of Anyway I don't think this is ready to be in the stdlib. I think it would make more sense to have it in itertools for now. There's also the possibility that we could get the same result by combining an hypotetical |
Oh. Yeah, exactly what I had in mind.
Of course this is still limiting compared to a general iterator adapter, but you could collect into an intermediate
|
A possible way to avoid the excess data movement: use a |
Could you elaborate on that? Why do you feel this is not ready yet?
Absolutely, and I would love to immediately go that path. But as I said,
That's a great idea! Do you think I should already change my implementation? |
Is there an unsafe way to concat two slices of known size to a new size using |
Even ignoring the issue that it isn't safe in general it would only work on linear memory. If the data is out of order (i.e. needs rotation) then the slices aren't pointing to a linear section of memory. |
Hm that's true, but in theory we could add a layer of indirection Edit: Long post-facto correction: it won't be memcpies but just writing ranges of ptrs into an array, which should be fast? |
Thinking about this some more, I think this api (too) easily hides that Maybe |
To be honest, I'm not sure. When it comes to |
@sfackler any updates on this? |
r? @m-ou-se as Steven is now alumni and Mara left some thoughts. |
/// # Panics | ||
/// | ||
/// Panics if `N` is 0. This check will most probably get changed to a | ||
/// compile time error before this method gets stabilized. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added an item for this to the tracking issue.
// to 0, we treat them as uninitialized and treat their copies | ||
// as initialized. | ||
unsafe { | ||
ptr::copy_nonoverlapping(buffer_ptr.add(N), buffer_ptr, N - 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The index math on this line seems wrong.
Repro:
fn main() {
for () in std::iter::repeat("0".to_owned())
.map_windows(|_: &[_; 3]| {})
.take(4) {}
}
free(): double free detected in tcache 2
Aborted (core dumped)
@dtolnay Thanks for catching that logic bug. I was surprised my tests did not catch that. Digging deeper, I now believe my whole logic is flawed. In particular, I prepare Soooo I think I will have to rethink my approach. I'm not sure how quickly I can find the time to do that. Just as a heads up that me fixing this could take a while. |
☔ The latest upstream changes (presumably #89703) made this pull request unmergeable. Please resolve the merge conflicts. |
Triage: |
Yeah let's close it. I don't see myself fixing this PR anytime soon. I still think the API is very interesting and worth playing around with, but right now I don't have the resources to land this. |
…, r=Mark-Simulacrum Add `Iterator::map_windows` Tracking issue: rust-lang#87155. This is inherited from the old PR rust-lang#82413. Unlike rust-lang#82413, this PR implements the `MapWindows` to be lazy: only when pulling from the outer iterator, `.next()` of the inner iterator will be called. ## Implementaion Steps - [x] Implement `MapWindows` to keep the iterators' [*Laziness*](https://doc.rust-lang.org/std/iter/index.html#laziness) contract. - [x] Fix the known bug of memory access error. - [ ] Full specialization of iterator-related traits for `MapWindows`. - [x] `Iterator::size_hint`, - [x] ~`Iterator::count`~, - [x] `ExactSizeIterator` (when `I: ExactSizeIterator`), - [x] ~`TrustedLen` (when `I: TrustedLen`)~, - [x] `FusedIterator`, - [x] ~`Iterator::advance_by`~, - [x] ~`Iterator::nth`~, - [ ] ... - [ ] More tests and docs. ## Unresolved Questions: - [ ] Is there any more iterator-related traits should be specialized? - [ ] Is the double-space buffer worth? - [ ] Should there be `rmap_windows` or something else? - [ ] Taking GAT for consideration, should the mapper function be `FnMut(&[I::Item; N]) -> R` or something like `FnMut(ArrayView<'_, I::Item, N>) -> R`? Where `ArrayView` is mentioned in rust-lang/generic-associated-types-initiative#2. - It can save memory, only the same size as the array window is needed, - It is more efficient, which requires less data copies, - It is possibly compatible with the GATified version of `LendingIterator::windows`. - But it prevents the array pattern matching like `iter.map_windows(|_arr: [_; N]| ())`, unless we extend the array pattern to allow matching the `ArrayView`.
…, r=Mark-Simulacrum Add `Iterator::map_windows` Tracking issue: rust-lang#87155. This is inherited from the old PR rust-lang#82413. Unlike rust-lang#82413, this PR implements the `MapWindows` to be lazy: only when pulling from the outer iterator, `.next()` of the inner iterator will be called. ## Implementaion Steps - [x] Implement `MapWindows` to keep the iterators' [*Laziness*](https://doc.rust-lang.org/std/iter/index.html#laziness) contract. - [x] Fix the known bug of memory access error. - [ ] Full specialization of iterator-related traits for `MapWindows`. - [x] `Iterator::size_hint`, - [x] ~`Iterator::count`~, - [x] `ExactSizeIterator` (when `I: ExactSizeIterator`), - [x] ~`TrustedLen` (when `I: TrustedLen`)~, - [x] `FusedIterator`, - [x] ~`Iterator::advance_by`~, - [x] ~`Iterator::nth`~, - [ ] ... - [ ] More tests and docs. ## Unresolved Questions: - [ ] Is there any more iterator-related traits should be specialized? - [ ] Is the double-space buffer worth? - [ ] Should there be `rmap_windows` or something else? - [ ] Taking GAT for consideration, should the mapper function be `FnMut(&[I::Item; N]) -> R` or something like `FnMut(ArrayView<'_, I::Item, N>) -> R`? Where `ArrayView` is mentioned in rust-lang/generic-associated-types-initiative#2. - It can save memory, only the same size as the array window is needed, - It is more efficient, which requires less data copies, - It is possibly compatible with the GATified version of `LendingIterator::windows`. - But it prevents the array pattern matching like `iter.map_windows(|_arr: [_; N]| ())`, unless we extend the array pattern to allow matching the `ArrayView`.
…, r=Mark-Simulacrum Add `Iterator::map_windows` Tracking issue: rust-lang#87155. This is inherited from the old PR rust-lang#82413. Unlike rust-lang#82413, this PR implements the `MapWindows` to be lazy: only when pulling from the outer iterator, `.next()` of the inner iterator will be called. ## Implementaion Steps - [x] Implement `MapWindows` to keep the iterators' [*Laziness*](https://doc.rust-lang.org/std/iter/index.html#laziness) contract. - [x] Fix the known bug of memory access error. - [ ] Full specialization of iterator-related traits for `MapWindows`. - [x] `Iterator::size_hint`, - [x] ~`Iterator::count`~, - [x] `ExactSizeIterator` (when `I: ExactSizeIterator`), - [x] ~`TrustedLen` (when `I: TrustedLen`)~, - [x] `FusedIterator`, - [x] ~`Iterator::advance_by`~, - [x] ~`Iterator::nth`~, - [ ] ... - [ ] More tests and docs. ## Unresolved Questions: - [ ] Is there any more iterator-related traits should be specialized? - [ ] Is the double-space buffer worth? - [ ] Should there be `rmap_windows` or something else? - [ ] Taking GAT for consideration, should the mapper function be `FnMut(&[I::Item; N]) -> R` or something like `FnMut(ArrayView<'_, I::Item, N>) -> R`? Where `ArrayView` is mentioned in rust-lang/generic-associated-types-initiative#2. - It can save memory, only the same size as the array window is needed, - It is more efficient, which requires less data copies, - It is possibly compatible with the GATified version of `LendingIterator::windows`. - But it prevents the array pattern matching like `iter.map_windows(|_arr: [_; N]| ())`, unless we extend the array pattern to allow matching the `ArrayView`.
…, r=Mark-Simulacrum Add `Iterator::map_windows` Tracking issue: rust-lang#87155. This is inherited from the old PR rust-lang#82413. Unlike rust-lang#82413, this PR implements the `MapWindows` to be lazy: only when pulling from the outer iterator, `.next()` of the inner iterator will be called. ## Implementaion Steps - [x] Implement `MapWindows` to keep the iterators' [*Laziness*](https://doc.rust-lang.org/std/iter/index.html#laziness) contract. - [x] Fix the known bug of memory access error. - [ ] Full specialization of iterator-related traits for `MapWindows`. - [x] `Iterator::size_hint`, - [x] ~`Iterator::count`~, - [x] `ExactSizeIterator` (when `I: ExactSizeIterator`), - [x] ~`TrustedLen` (when `I: TrustedLen`)~, - [x] `FusedIterator`, - [x] ~`Iterator::advance_by`~, - [x] ~`Iterator::nth`~, - [ ] ... - [ ] More tests and docs. ## Unresolved Questions: - [ ] Is there any more iterator-related traits should be specialized? - [ ] Is the double-space buffer worth? - [ ] Should there be `rmap_windows` or something else? - [ ] Taking GAT for consideration, should the mapper function be `FnMut(&[I::Item; N]) -> R` or something like `FnMut(ArrayView<'_, I::Item, N>) -> R`? Where `ArrayView` is mentioned in rust-lang/generic-associated-types-initiative#2. - It can save memory, only the same size as the array window is needed, - It is more efficient, which requires less data copies, - It is possibly compatible with the GATified version of `LendingIterator::windows`. - But it prevents the array pattern matching like `iter.map_windows(|_arr: [_; N]| ())`, unless we extend the array pattern to allow matching the `ArrayView`.
This method returns an iterator over mapped windows of the starting iterator. Adding the more straight-forward
Iterator::windows
is not easily possible right now as the items are stored in the iterator type, meaning thenext
call would return references toself
. This is not allowed by the currentIterator
trait design. This might change once GATs have landed.The idea has been brought up by @m-ou-se here.
I think this method is quite versatile and useful. I hope the examples in the doc comment sufficiently demonstrate that.
One open question: the current design basically requires that the internal array buffer is shifted on each
next
call, meaning it has an O(iter.count()
*N
) runtime complexity pernext
(whereN
is the const parameter). I expect this method to be called with very smallN
s only, but of course it could be used with large arrays. To avoid this shifting, we would need an array version ofVecDeque
basically. But this would also mean the closure wouldn't get a simple parameter anymore. I think we want this simpler but "slower for large Ns" version, but what do others thing? Maybe the docs should mention this caveat?