-
-
Notifications
You must be signed in to change notification settings - Fork 14.3k
Implement partial_sort_unstable for slice #149318
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
base: main
Are you sure you want to change the base?
Conversation
This comment has been minimized.
This comment has been minimized.
7af04ad to
115ac5c
Compare
This comment has been minimized.
This comment has been minimized.
115ac5c to
0e87d5d
Compare
|
cc @orlp |
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.
Left some remarks, some on style, but also some with substance.
Besides comments on the code that's written, I do note a lack of tests?
Doc tests cover most branches. I don't find a dedicated file to cover its cousin |
|
The examples can change at any time. And you didn't test, for example, the post-condition that all elements |
Thanks and yes. Do you know where the unit tests of |
|
I believe the bulk is found in https://github.com/rust-lang/rust/blob/main/library/alloctests/tests/sort/tests.rs. |
|
What I suggested in the ACP was a sketch implementation, I did some more thinking and I think the following handles all corner cases nicely: pub fn partial_sort<T, F, R>(mut v: &mut [T], range: R, is_less: &mut F)
where
F: FnMut(&T, &T) -> bool,
R: RangeBounds<usize>,
{
let len = v.len();
let Range { start, end } = slice::range(range, ..len);
if end - start <= 1 {
// Can be resolved in at most a single partition_at_index call, without
// further sorting. Do nothing if it is an empty range at start or end.
if start != len && end != 0 {
sort::select::partition_at_index(v, start, is_less);
}
return;
}
// Don't bother reducing the slice to sort if it eliminates fewer than 8 elements.
if end + 8 <= len {
v = sort::select::partition_at_index(v, end - 1, is_less).0;
}
if start >= 8 {
v = sort::select::partition_at_index(v, start, is_less).2;
}
sort::unstable::sort(v, is_less);
}And to formalize the post-conditions, I think the following should hold after a call to for i in 0..b {
for j in b..n {
assert!(v[i] <= v[j]);
}
}
for i in 0..e {
for j in e..n {
assert!(v[i] <= v[j]);
}
}
for i in b..e {
for j in i..e {
assert!(v[i] <= v[j]);
}
} |
A lot of those individual comparisons are implied by transitivity of the ordering, so it can be reduced to choosing the maximum of the prefix (if any), the minimum of the suffix (if any), and then asserting that the concatenation is sorted. Informally, let max_before = v[..b].iter().max().into_iter();
let sorted_range = v[b..e].iter();
let min_after = v[e..].iter().min().into_iter();
let seq = max_before.chain(sorted_range).chain(min_after);
assert!(seq.is_sorted());That's pretty much what you said in rust-lang/libs-team#685 (comment) , just using transitivity of the comparison. Without assuming that, the implementation couldn't guarantee the universally quantified property anyway. |
f9a09e0 to
372589e
Compare
|
This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed. Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers. |
|
Pushed a new implementation. I'm writing tests but perhaps we'd have a new mod under |
cc @Amanieu for early review for the direction and advice on where to organize the tests. |
This comment has been minimized.
This comment has been minimized.
6ef6ab4 to
10d053f
Compare
This comment has been minimized.
This comment has been minimized.
10d053f to
43fc006
Compare
Signed-off-by: tison <wander4096@gmail.com> Co-Authored-By: Orson Peters <orsonpeters@gmail.com>
43fc006 to
bbca3c0
Compare
|
Regarding the tests I'm happy with either a separate file or as part of the existing tests as you see fit. |
Signed-off-by: tison <wander4096@gmail.com>
|
Pushed some basic test cases at 01bfcc0. The existing sort tests have many assumptions, like I added the basic test cases and will try to leverage the |
|
r? libs |
Signed-off-by: tison <wander4096@gmail.com>
|
Added some pattern tests - can be extended later. |
|
cc @tgross35 can you take a look here? |
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 have a few requests, mostly stylistic. I think the tests could use some improvements but I'm not sure what would be reasonable - @orlp would you mind providing some suggestions here? (In general, I'm happy to defer this review to you)
| mod linked_list; | ||
| mod misc_tests; | ||
| mod num; | ||
| mod partial_sort; |
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.
Optional nit: since sort is a module with children, maybe organize this as sort::partial instead.
| use crate::slice::sort::shared::find_existing_run; | ||
| #[cfg(not(any(feature = "optimize_for_size", target_pointer_width = "16")))] | ||
| use crate::slice::sort::shared::smallsort::insertion_sort_shift_left; | ||
| use crate::slice::{self}; |
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.
Nit, unusual import style
| use crate::slice::{self}; | |
| use crate::slice; |
| if start != len && end != 0 { | ||
| partition_at_index(v, start, &mut is_less); | ||
| } else { | ||
| // Do nothing if it is an empty range at start or end: all guarantees | ||
| // are already upheld. | ||
| } | ||
|
|
||
| return; |
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.
Nit: the positive condition reads easier than negative, and this avoids the empty else just for a comment
if end == 0 || start == len {
// Do nothing if it is an empty range at start or end: all guarantees
// are already upheld.
return;
}
partition_at_index(v, start, &mut is_less);
return;| // Avoid partitioning the slice when it eliminates only a few elements to sort. | ||
| // The threshold of 8 elements was determined empirically. | ||
| let mut v = v; | ||
| if end + 8 <= len { | ||
| v = partition_at_index(v, end - 1, &mut is_less).0; | ||
| } | ||
| if start >= 8 { | ||
| v = partition_at_index(v, start, &mut is_less).2; | ||
| } |
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.
Put the "8" in a constant, similar to MAX_LEN_ALWAYS_INSERTION_SORT above. It would also be nice to include a link to further context on how the heuristic was determined (even if it's just the GH comment here) so the next person to update this doesn't need to dig too much.
| #[inline(always)] | ||
| pub fn partial_sort<T, F, R>(v: &mut [T], range: R, mut is_less: F) |
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.
#[inline(always)] seems a bit strong here, is it actually needed? I think #[inline] would probably be fine so small code size heuristics can pick an outline point here if advantageous.
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 follows how sort_unstable was implemented now -
rust/library/core/src/slice/sort/unstable/mod.rs
Lines 19 to 20 in 7eadf83
| #[inline(always)] | |
| pub fn sort<T, F: FnMut(&T, &T) -> bool>(v: &mut [T], is_less: &mut F) { |
I tend to keep this flavor and review them together later, rather than make a difference here.
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.
After a close look, the cyclomatic complexity of sort is about 4, while partial_sort is about 7, with the final sort to be inlined always, so perhaps 10.
This is typically small enough, but it's reasonable if you insist on making it #[inline] now.
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 rule of thumb nowadays is that using #[inline(always)] rather than #[inline] should be backed up by benchmarks and come with a comment explaining why, because it tends to hurt the size-optimized case. So yeah, I'd prefer #[inline] unless there is something to back up always making a difference.
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.
Note also that inlining is bottom-up, so inline(always) applies after other things might have been inlined into the body. Thus the cyclomatic complexity of this function is actually unknown.
In general, any argument of the form "it's typically small enough" is only enough to say #[inline], because if it really is small enough that's already sufficient to get it inlined -- and means that since "typically" isn't "always", leaving the flexibility to say "well actually no this is the atypical case" in the inliner's heuristics, which is also a good thing. (Making a case for always is easier in leaves, where for example you can argue that things like pointer::add shouldn't have function-call overhead even in opt-level=0, but something like sort is very much not that.)
It's possible that what you're looking for here would be inline(trampoline) if we got rust-lang/rfcs#3778 , but today you just want a normal #[inline].
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 actually intended to ping you here to double check, but somehow you have a way of appearing whenever inlining is discussed anyway :)
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.
@scottmcm Thanks for your information!
One more question: as this partial_sort method is only used within the crate, even without #[inline], the compiler may still heuristically inline the function when desired?
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.
So long as it has the information needed available (the MIR for the MIR inliner, the LLVM-IR for the LLVM inliner) then everything is an inlining candidate (though of course inline(never) is rarely inlined). In practice, that means that in -C codegen-units=1 with -C opt-level=3 there's not much of a difference between generic things (since those always need MIR available) and inline things.
And if you're using LTO, everything is an inlining candidate -- that's a big part of why LTO is useful (and a big part of why rustc doesn't have all that many inline annotations).
| /// Partially sorts the slice in ascending order **without** preserving the initial order of equal elements. | ||
| /// | ||
| /// Upon completion, for the specified range `start..end`, it's guaranteed that: | ||
| /// | ||
| /// 1. Every element in `self[..start]` is smaller than or equal to | ||
| /// 2. Every element in `self[start..end]`, which is sorted, and smaller than or equal to | ||
| /// 3. Every element in `self[end..]`. | ||
| /// | ||
| /// This partial sort is unstable (i.e., may reorder equal elements), in-place (i.e., does not | ||
| /// allocate), and *O*(*n* + *k* \* log(*k*)) worst-case, where *n* is the length of the slice and | ||
| /// *k* is the length of the specified range. | ||
| /// | ||
| /// See the documentation of [`sort_unstable`] for implementation notes. |
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 it's worth a comment noting the (lack of) guarantees about ..start and end.., given this isn't exactly covered by stability, and that the current implementation may just sort the whole thing. IOW, users shouldn't be surprised if partial_sort_unstable([0, 1, 3, 2, 4, 5], 2..4) returns [1, 0, 2, 3, 5, 4].
|
Reminder, once the PR becomes ready for a review, use |
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 the overall structure of tests look fine, just some stuff missing.
|
|
||
| check_is_partial_sorted::<T, _>(&mut v.to_vec(), ..); | ||
| check_is_partial_sorted::<T, _>(&mut v.to_vec(), 0..0); | ||
| check_is_partial_sorted::<T, _>(&mut v.to_vec(), len - 1..len - 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.
Missing test for len..len which is a valid empty range at the end.
| check_is_partial_sorted::<T, _>(&mut v.to_vec(), mid..mid); | ||
| check_is_partial_sorted::<T, _>(&mut v.to_vec(), mid - 1..mid + 1); | ||
| check_is_partial_sorted::<T, _>(&mut v.to_vec(), mid - 1..mid); | ||
| check_is_partial_sorted::<T, _>(&mut v.to_vec(), mid..mid + 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.
Missing tests for substantial slices somewhere in the middle.
|
Thanks for your comments @orlp @tgross35! One comment inline #149318 (comment) Rest SGTM. I'll integrate them in a few days and re-request a review :D |
This refers to #149046.