-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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 unstable sort to libcore #1884
Merged
Merged
Changes from 1 commit
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
4903f95
Unstable sort in libcore
98bae13
Fix spelling
8b513d0
Elaborate and improve wording
b3e7da2
New benchmark numbers
13bf425
Rename unstable_sort to sort_unstable
2bae25f
Reorder sections more logically, clearer wording
c40dc37
Rename feature to sort_unstable
7d2e940
Improve wording per brson's request
d25e483
Link to an instability-bit-me story
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
- Feature Name: unstable_sort | ||
- Start Date: 2017-02-03 | ||
- RFC PR: (leave this empty) | ||
- Rust Issue: (leave this empty) | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
Add an unstable sort to libcore. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
At the moment, the only sort function we have in libstd is `slice::sort`. It is stable, | ||
allocates additional memory, and is unavailable in `#![no_std]` environments. | ||
|
||
**Q: What is stability?**<br> | ||
A: A sort function is stable if it doesn't reorder equal elements. For example: | ||
```rust | ||
let mut orig = vec![(0, 5), (0, 4)]; | ||
let mut v = orig.clone(); | ||
|
||
// Stable sort preserves the original order of equal elements. | ||
v.sort_by_key(|p| p.0); | ||
assert!(orig == v); // OK! | ||
|
||
/// Unstable sort may or may not preserve the original order. | ||
v.unstable_sort_by_key(|p| p.0); | ||
assert!(orig == v); // MAY FAIL! | ||
``` | ||
|
||
**Q: When is stability useful?**<br> | ||
A: Not very often. A typical example is sorting columns in interactive GUI tables. | ||
If you want to have rows sorted by column X while breaking ties by column Y, you | ||
first have to click on column Y and then click on column X. | ||
|
||
**Q: Can stable sort be performed using unstable sort?**<br> | ||
A: Yes. If we transform `[T]` into `[(T, usize)]` by pairing every element with it's | ||
index, then perform unstable sort, and finally remove indices, the result will be | ||
equivalent to stable sort. | ||
|
||
**Q: Why is `slice::sort` stable?**<br> | ||
A: Because stability is a good default. A programmer might call a sort function | ||
without checking in the documentation whether it is stable or unstable. It is very | ||
inutitive to assume stability, so having `slice::sort` perform unstable sorting might | ||
cause unpleasant surprises. | ||
|
||
**Q: Why does `slice::sort` allocate?**<br> | ||
A: It is possible to implement a non-allocating stable sort, but it would be | ||
considerably slower. | ||
|
||
**Q: Why is `slice::sort` not compatible with `#![no_std]`?**<br> | ||
A: Because it allocates additional memory. | ||
|
||
**Q: How much faster can unstable sort be?**<br> | ||
A: Sorting 64-bit integers using [pdqsort][stjepang-pdqsort] (an | ||
unstable sort implementation) is **40% faster** than using `slice::sort`. | ||
Detailed benchmarks are [here](https://github.com/stjepang/pdqsort#extensive-benchmarks). | ||
|
||
**Q: Can unstable sorting benefit from allocation?**<br> | ||
A: Generally, no. There is no fundamental property in computer science saying so, | ||
but this has always been true in practice. Zero-allocation and instability go | ||
hand in hand. | ||
|
||
Stable sorting, although a good default, is very rarely useful. Users much more often | ||
value higher performance, lower memory overhead, and compatibility with `#![no_std]`. | ||
|
||
Having a really performant, non-allocating sort function in libcore would cover these | ||
needs. At the moment, Rust is compromising on these highly regarded qualities for a | ||
systems programming language by not offering a built-in alternative. | ||
|
||
The API will consist of three functions that mirror the current sort in libstd: | ||
|
||
1. `core::slice::unstable_sort` | ||
2. `core::slice::unstable_sort_by` | ||
3. `core::slice::unstable_sort_by_key` | ||
|
||
By contrast, C++ has functions `std::sort` and `std::stable_sort`, where the | ||
defaults are set up the other way around. | ||
|
||
# Detailed design | ||
[design]: #detailed-design | ||
|
||
Let's see what kinds of unstable sort algorithms other languages most commonly use: | ||
|
||
* C: quicksort | ||
* C++: introsort | ||
* D: introsort | ||
* Swift: introsort | ||
* Go: introsort | ||
* Crystal: introsort | ||
* Java: dual-pivot quicksort | ||
|
||
The most popular sort is definitely introsort. Introsort is an implementation | ||
of quicksort that limits recursion depth. As soon as depth exceeds `2 * log(n)`, | ||
it switches to heapsort in order to guarantee `O(n log n)` worst-case. This | ||
method combines the best of both worlds: great average performance of | ||
quicksort with great worst-case performance of heapsort. | ||
|
||
Java (talking about `Arrays.sort`, not `Collections.sort`) uses dual-pivot | ||
quicksort. It is an improvement of quicksort that chooses two pivots for finer | ||
grained partitioning, offering better performance in practice. | ||
|
||
A very interesting improvement of introsort is [pattern-defeating quicksort][orlp-pdqsort], | ||
offering substantially better performance in common cases. One of the key | ||
tricks pdqsort uses is block partitioning described in paper [BlockQuicksort][blockquicksort]. | ||
This algorithm still hasn't been built into in any programming language's | ||
standard library, but there are plans to include it into some C++ implementations. | ||
|
||
Block partitioning is incompatible with dual-pivot method, but offers much | ||
larger gains in performance. For all these reasons, unstable sort in libcore | ||
will be based on pdqsort. Proposed implementaton is available in | ||
[pdqsort][stjepang-pdqsort] crate. | ||
|
||
**Interface** | ||
|
||
```rust | ||
pub trait SliceExt { | ||
type Item; | ||
|
||
// ... | ||
|
||
fn unstable_sort(&mut self) | ||
where Self::Item: Ord; | ||
|
||
fn unstable_sort_by<F>(&mut self, compare: F) | ||
where F: FnMut(&Self::Item, &Self::Item) -> Ordering; | ||
|
||
fn unstable_sort_by_key<B, F>(&mut self, mut f: F) | ||
where F: FnMut(&Self::Item) -> B, | ||
B: Ord; | ||
} | ||
``` | ||
|
||
**Examples** | ||
|
||
```rust | ||
let mut v = [-5i32, 4, 1, -3, 2]; | ||
|
||
v.unstable_sort(); | ||
assert!(v == [-5, -3, 1, 2, 4]); | ||
|
||
v.unstable_sort_by(|a, b| b.cmp(a)); | ||
assert!(v == [4, 2, 1, -3, -5]); | ||
|
||
v.unstable_sort_by_key(|k| k.abs()); | ||
assert!(v == [1, 2, -3, 4, -5]); | ||
``` | ||
|
||
In most cases it's sufficient to prepend `sort` with `unstable_` and | ||
instantly benefit from higher performance and lower memory use. | ||
|
||
**Q: Is `slice::sort` ever faster than pdqsort?**<br> | ||
A: Yes, there are a few cases where it is faster. For example, if the slice | ||
consists of several pre-sorted sequences concatenated one after another, then | ||
`slice::sort` will most probably be faster. But other than that, it should be | ||
generally slower than pdqsort. | ||
|
||
**Q: What about radix sort?**<br> | ||
A: Radix sort is usually blind to patterns in slices. It treats totally random | ||
and partially sorted the same way. It is probably possible to improve it | ||
by combining it with other techniques until it becomes a hybrid sort. Moreover, | ||
radix sort is incompatible with comparison-based sorting, which makes it | ||
an awkward choice for a general-purpose API. On top of all this, it's | ||
not even that much faster than pdqsort anyway. | ||
|
||
# How We Teach This | ||
[how-we-teach-this]: #how-we-teach-this | ||
|
||
Stability is a confusing and loaded term. Function `slice::unstable_sort` might be | ||
misunderstood as a function that has unstable API. That said, there is no | ||
less confusing alternative to "unstable sorting". Documentation should | ||
clearly state what "stable" and "unstable" mean. | ||
|
||
`slice::unstable_sort` will be mentioned in the documentation for `slice::sort` | ||
as a faster non-allocating alternative. The documentation for | ||
`slice::unstable_sort` must also clearly state that it guarantees no allocation. | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
The implementation of sort algorithms will grow bigger, and there will be more | ||
code to review. | ||
|
||
It might be surprising to discover cases where `slice::sort` is faster than | ||
`slice::unstable_sort`. However, these peculiarities can be explained in | ||
documentation. | ||
|
||
# Alternatives | ||
[alternatives]: #alternatives | ||
|
||
Unstable sorting is indistinguishable from stable sorting when sorting | ||
primitive integers. It's possible to specialize `slice::sort` to fall back | ||
to `slice::unstable_sort`. This would improve performance for primitive integers in | ||
most cases, but patching cases type by type with different algorithms makes | ||
performance more inconsistent and less predictable. | ||
|
||
Unstable sort guarantees no allocation. Instead of naming it `slice::unstable_sort`, | ||
it could also be named `slice::noalloc_sort` or `slice::noalloc_unstable_sort`. | ||
This may slightly improve clarity, but feels much more awkward. | ||
|
||
Unstable sort can also be provided as a standalone crate instead of | ||
within the standard library. However, every other systems programming language | ||
has a fast unstable sort in standard library, so why shouldn't Rust, too? | ||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
None. | ||
|
||
[orlp-pdqsort]: https://github.com/orlp/pdqsort | ||
[stjepang-pdqsort]: https://github.com/stjepang/pdqsort | ||
[blockquicksort]: http://drops.dagstuhl.de/opus/volltexte/2016/6389/pdf/LIPIcs-ESA-2016-38.pdf |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Out of curiosity, is this compared against the recent improvements to the existing sort implementation?
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.
Yes, it is.