-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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 an in-place rotate method for slices to libcore #41670
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @sfackler (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see the contribution instructions for more information. |
If the motivation is inserting an iterator in the middle of a vector, why not use #![feature(splice)]
fn main() {
let mut v = (0..10).collect::<Vec<_>>();
v.splice(7..7, 100..104);
assert_eq!(&v, &[0, 1, 2, 3, 4, 5, 6, 100, 101, 102, 103, 7, 8, 9]);
} |
Having an efficient rotate algorithm in the standard library of a system language is quite good, even expected. |
@kennytm Well, that'll teach me not to only read the stable docs when picking example use cases 😅 I updated the doctest & the first post above to a different example instead. That said, it would be nice to have a |
This would be useful for me. Currently I just use the following method: // In-place rotation algorithm (shifts to the right)
fn rotate_slice<T>(slice: &mut [T], places: usize) {
// Rotation can be implemented by reversing the slice,
// splitting the slice in two, and then reversing the
// two sub-slices.
slice.reverse();
let (a, b) = slice.split_at_mut(places);
a.reverse();
b.reverse();
} How does this compare performance wise? |
The implementation in this PR is about 10% faster in general, 50% faster for certain mid/len combinations, and 90% faster for certain types.
Raw benchmark numbers: https://gist.github.com/scottmcm/36a6d8db52870f1a60612f811b5ff69a |
This can be fixed with a specialization in another PR? |
@leonardo-m I found a way: PR #41764 But |
src/libcore/slice/rotate.rs
Outdated
} | ||
} | ||
|
||
unsafe fn ptr_swap_n<T>(a: *mut T, b: *mut T, n: usize) { |
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 seems to suffer from the same undef/padding issues as #41335
In this case it would be possible to overcome them by always doing ptr_swap_u8
, but that would likely result in worse performance
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.
Thanks for taking a look, @ranma42.
I expected this to be fine as ptr::swap
is memcpy+memmove+memcpy. I figured that if memcpy'ing padding was invalid, then ptr::read::<(u8, u16)>
would always be invalid, since it's also just memcpy. Is that logic incorrect?
Also, if this code hits those problems, does #40454 hit them too? (Ideally I'd take the swap-slices optimizations out of this and use a new ptr::swap_nonoverlapping_n
-style function that just worked well.)
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.
#40454 should be fine, because it only uses u8
values. Some of them might be undef, but as long as they are only copied and never used as condition guards, the worst that can happen is that the padding bytes are not guaranteed to be copied (as LLVM might see them as undef and use arbitrary values instead of accessing the memory).
ptr::read::<(u8, u16)>
is not invalid as long as you do not try to interpret the result as another type. The problem arises, for example, if you have a (u8, u16)
and read it as a u32
value (assuming that there is a 1-byte padding between the u8
and the u16
). In this case, the u32
result would be undefined because of the padding.
ping @sfackler, looks like this might have fallen through the cracks? (pinging you on IRC too) |
The API itself seems pretty reasonable to me, though it sounds like there are still some unanswered questions in the implementation? cc @rust-lang/libs |
I've pushed an update removing the optimization, which I believe was the only question in the implementation. (Hopefully #40454 will make it unnecessary, and if not it's probably better to share code with that that to have a different optimization here anyway.) [Edit] Changed the commit message to make travis re-run, since it failed in git fetch. Is there a better way to handle that in future? |
The API seems reasonable to me too. |
Libs team briefly discussed the API in triage meeting, seems good on that front. |
And travis failed to git fetch in the xcode 8.2 config again: https://travis-ci.org/rust-lang/rust/jobs/230284608 Poking it... |
☔ The latest upstream changes (presumably #41764) made this pull request unmergeable. Please resolve the merge conflicts. |
Merge conflict with myself 😆 Rebased and made a real tracking issue. |
sooooo sounds like everything is ready to go? wdyt @sfackler? |
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.
Couple of little things. Looks reasonable otherwise.
src/libcollections/slice.rs
Outdated
@@ -1337,6 +1337,61 @@ impl<T> [T] { | |||
core_slice::SliceExt::sort_unstable_by_key(self, f); | |||
} | |||
|
|||
/// Permutes the slice in-place such that `self[mid..]` move to the |
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.
"moves"
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.
Hmm, I guess I was mentally inserting "the elements in". Considering it as "the subslice" is probably better; will update.
src/libcollections/slice.rs
Outdated
@@ -1337,6 +1337,61 @@ impl<T> [T] { | |||
core_slice::SliceExt::sort_unstable_by_key(self, f); | |||
} | |||
|
|||
/// Permutes the slice in-place such that `self[mid..]` move to the | |||
/// beginning of the slice while `self[..mid]` move to the end of the |
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.
"moves"
src/libcore/slice/mod.rs
Outdated
rotate::ptr_rotate(mid, p.offset(mid as isize), k); | ||
} | ||
|
||
k |
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.
What's the purpose of returning k
?
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.
self[..mid]
moves to self[k..]
and self[mid..]
moves to self[..k]
, so when, say, rotating around a partition point, it gives the new partition point.
I return it because Stepanov returns the equivalent iterator, but I've definitely found the index less useful that the iterator. Obviously it's easy to calculate yourself, but the more annoying problem is that I'm usually rotating a subslice, so the returned index needs to be fixed-up since it's not slice-independent. (With iterators, in contrast, the new split point is non-trivial to calculate for non-random-access iterators, and iterators are independently-meaningful, so the returned split point can directly be used to construct ranges in the original container.)
So I don't have particularly strong feelings; let me know what you'd prefer.
Regarding the k return value, also take a look at the API of how the D standard library performs an analogous operation (and also the name of the function): |
A helpful primitive for moving chunks of data around inside a slice. In particular, adding elements to the end of a Vec then moving them somewhere else, as a way to do efficient multiple-insert. (There's drain for efficient block-remove, but no easy way to block-insert.) Talk with another example: <https://youtu.be/qH6sSOr-yk8?t=560>
Batch-insert is better done with Vec::splice
It can be revisted later after the mem::swap optimizations land.
Thanks for the link, @leonardo-m; good to see another example of returning I thought some more about my earlier comment and decided to just remove the return value. It's easy to calculate and could plausibly be added later, so might as well just be left off for now. |
ping @sfackler, sounds like this is ready for another look! |
📌 Commit 094d61f has been approved by |
⌛ Testing commit 094d61f with merge 558cd1e... |
Add an in-place rotate method for slices to libcore A helpful primitive for moving chunks of data around inside a slice. For example, if you have a range selected and are drag-and-dropping it somewhere else (Example from [Sean Parent's talk](https://youtu.be/qH6sSOr-yk8?t=560)). (If this should be an RFC instead of a PR, please let me know.) Edit: changed example
☀️ Test successful - status-appveyor, status-travis |
This adds a function which says "Safety: The type |
fix unsafety: don't call ptr_rotate for ZST `rotate::ptr_rotate` has a comment saying ``` /// # Safety /// /// The specified range must be valid for reading and writing. /// The type `T` must have non-zero size. ``` So we better make sure we don't call it on ZST... Cc @scottmcm (author of rust-lang#41670)
A helpful primitive for moving chunks of data around inside a slice.
For example, if you have a range selected and are drag-and-dropping it somewhere else (Example from Sean Parent's talk).
(If this should be an RFC instead of a PR, please let me know.)
Edit: changed example