-
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
Tracking issue for sorting by expensive-to-compute keys (feature slice_sort_by_cached_key) #34447
Comments
I suspect the method works this way to avoid allocation. Most people would expect this method not to allocate, and to sort in place. If you want to calculate keys only once, perhaps you could introduce some kind of caching inside |
@Thiez |
I stand corrected :-) |
I imagine that the current method may still be more optimal for simple key functions like This is a marked difference from Python's case, where even the simplest key function still has overhead (making a Schwartzian transform-style sort the clear winner). |
I think @ExpHP is correct: CPython's string sort, which is written in C, is much faster than any key function written in Python. Note that this is not necessarily true for PyPy (which has a JIT), and is definitely not true in Rust. |
The lambda is just passed down as a comparator, you'd be surprised by how much the optimizer can do with that. The name is misleading but this is not the key argument python in python The behavior you suggest has it's uses but it's probably something that belong to an external crate. |
To play devil's advocate a bit, this is not entirely a fair assessment; rust already provides the capability of Actually, for that reason, I was surprised to learn that a Then one day while converting some Python code I came across a sort with an expensive key method, and suddenly it all made sense. There are two different idioms to sorting a list by a key, each suited to different use cases. At the time, I concluded that this must have been the reason why rust had no |
Add slice::sort_by_cached_key as a memoised sort_by_key At present, `slice::sort_by_key` calls its key function twice for each comparison that is made. When the key function is expensive (which can often be the case when `sort_by_key` is chosen over `sort_by`), this can lead to very suboptimal behaviour. To address this, I've introduced a new slice method, `sort_by_cached_key`, which has identical semantic behaviour to `sort_by_key`, except that it guarantees the key function will only be called once per element. Where there are `n` elements and the key function is `O(m)`: - `slice::sort_by_cached_key` time complexity is `O(m n log m n)`, compared to `slice::sort_by_key`'s `O(m n + n log n)`. - `slice::sort_by_cached_key` space complexity remains at `O(n + m)`. (Technically, it now reserves a slice of size `n`, whereas before it reserved a slice of size `n/2`.) `slice::sort_unstable_by_key` has not been given an analogue, as it is important that unstable sorts are in-place, which is not a property that is guaranteed here. However, this also means that `slice::sort_unstable_by_key` is likely to be slower than `slice::sort_by_cached_key` when the key function does not have negligible complexity. We might want to explore this trade-off further in the future. Benchmarks (for a vector of 100 `i32`s): ``` # Lexicographic: `|x| x.to_string()` test bench_sort_by_key ... bench: 112,638 ns/iter (+/- 19,563) test bench_sort_by_cached_key ... bench: 15,038 ns/iter (+/- 4,814) # Identity: `|x| *x` test bench_sort_by_key ... bench: 1,346 ns/iter (+/- 238) test bench_sort_by_cached_key ... bench: 1,839 ns/iter (+/- 765) # Power: `|x| x.pow(31)` test bench_sort_by_key ... bench: 3,624 ns/iter (+/- 738) test bench_sort_by_cached_key ... bench: 1,997 ns/iter (+/- 311) # Abs: `|x| x.abs()` test bench_sort_by_key ... bench: 1,546 ns/iter (+/- 174) test bench_sort_by_cached_key ... bench: 1,668 ns/iter (+/- 790) ``` (So it seems functions that are single operations do perform slightly worse with this method, but for pretty much any more complex key, you're better off with this optimisation.) I've definitely found myself using expensive keys in the past and wishing this optimisation was made (e.g. for rust-lang#47415). This feels like both desirable and expected behaviour, at the small cost of slightly more stack allocation and minute degradation in performance for extremely trivial keys. Resolves rust-lang#34447.
There now exists an unstable method, Note that, when this is stabilised, we should add a clippy lint to suggest using this method over |
Reopening as the unstable tag on the method is pointing here as a tracking issue (https://doc.rust-lang.org/nightly/std/primitive.slice.html#method.sort_by_cached_key). |
If/when this is stabilised, it is worth considering whether adding |
Also |
@varkor Can't we just cache the key for the last visited element in |
Yes, there's so little key recalculation in the two methods that there's probably no need for specific cached methods. I'll admit I hadn't thought too much about them when I referenced them here: I was just making a quick note to remind myself later! You're right, though! |
There is something we must be careful with, the current function that is used by |
Should we add |
@lcnr |
In addition, the stability of the current |
@varkor I would add an unstable version that currently has no benefit over the stable version for the sake of completeness/to express intent. If someone does not need the stability of
In case we are able to think of a slightly faster unstable version in the far distant future, there would be no need to check every single invocation of |
…ey, r=SimonSapin Stabilize slice_sort_by_cached_key I was going to ask on the tracking issue (rust-lang#34447), but decided to just send this and hope for an FCP here. The method was added last March by rust-lang#48639. Signature: https://doc.rust-lang.org/std/primitive.slice.html#method.sort_by_cached_key ```rust impl [T] { pub fn sort_by_cached_key<K, F>(&mut self, f: F) where F: FnMut(&T) -> K, K: Ord; } ``` That's an identical signature to the existing `sort_by_key`, so I think the questions are just naming, implementation, and the usual "do we want this?". The implementation seems to have proven its use in rustc at least, which many uses: https://github.com/rust-lang/rust/search?l=Rust&q=sort_by_cached_key (I'm asking because it's exactly what I just needed the other day: ```rust all_positions.sort_by_cached_key(|&n| data::CITIES.iter() .map(|x| *metric_closure.get_edge(n, x.pos).unwrap()) .sum::<usize>() ); ``` since caching that key is a pretty obviously good idea.) Closes rust-lang#34447
And it's in! Anyone want to update https://en.wikipedia.org/wiki/Schwartzian_transform#Comparison_to_other_languages? 😏 |
Hi—
Ideally, the implementation of sort_by_key() would invoke the received key function exactly once per item. This is highly beneficial when a costly computation (e.g., a distance function) needs to be used for sorting.
But Rust’s implementation as of 1.9 (link) calls the key function each time:
For comparison, on its day Python highlighted this in the release notes for 2.4. As per their current Sorting HOW TO:
Many thanks for considering. It’d be great if Rust could behave the same way.
The text was updated successfully, but these errors were encountered: