-
Notifications
You must be signed in to change notification settings - Fork 153
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
Implement sorting in place for OrderMap, OrderSet #57
Conversation
Please read the last commits in the PR, the ones that implement sort_by and sort_keys |
766c6aa
to
434f3fc
Compare
I wish I could review this but (after looking at the code) I don't know enough about the inner workings of the data structure to make a comment. Thanks for doing this! |
The improvements we could do with unsafe code are (I think) mostly about
|
@vitiral Any review would be appreciated, of course! This implementation is actually rather simple -- it makes an identity permutation of the indices, then sorts that permutation with the user's comparison function (mapping each index to its key-value pair), and then we apply the permutation to the two parts of the hash table. |
the docstrings look good to me though. Some minor suggested tweaks:
Consider adding a period at the end.
Consider changing to:
I prefer docs to have a single sentance "basic summary" and then "further description" below. I also added the "or both" description and removed the parenthesis. (In addition I alligned by 100 columns which is the default in rust, feel free to do whatever alignment you want). (Final edit: I realized that I aligned without the correct comment inlines. fixed) |
It's amazing to me that you can simply mutate the |
Simplified for the common case where capacity fits in 32-bits:
To change the order in sort_by we first go through the "actual hash table" and update each Pos there and switch the old index to the new index. Then we permutate When we iterate the ordermap in its order, we just completely overlook |
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.
Gotta love quickcheck -- doesn't take a lot of tests and it is easy to feel confident that the library does a pretty decent job :)
tests/quick.rs
Outdated
let mut answer = keyvals.0; | ||
answer.sort_by_key(|t| t.0); | ||
|
||
// reverse dedup: Because OrderMap::from_iter keeps the last value for |
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.
good comment!
I just realized, is removal pretty slow for this library since you have to remove an entry from The README states:
But for a large number of elements, removing a lower index should be pretty slow right? Honestly, fast removals isn't that useful IMO. The number of times I have to remove an item is rare. |
I get it now! You can apply the new indexes in only |
The slow order preserving removal is not implemented -- it's just a swap_replace removal. OrderMap is not a perfect structure -- we can't have it all. Slow order preserving removal is also not what we want. So my general though is: we want another variant crate of ordermap, that is not indexable, uses tombstones and has O(1) order preserving removals. Current crate will stay as it is in general because usize-indexable hash maps are also very useful. |
Yes the permutation algo is pretty wild. And it destroys the permutation vector in the process, for its own bookkeeping :-) |
ah, swap_replace -- that's right! I forgot that removal screws up the order. I think you made a very reasonable choice in terms of the different technical options. This is an AMAZING library. In particular it makes is SO much easier to work with Maps and Sets while testing since I can control the order (and I don't have the real world performance penalty of a BTree). This feature will make that even easier! |
src/lib.rs
Outdated
@@ -1110,6 +1110,65 @@ impl<K, V, S> OrderMap<K, V, S> | |||
}); | |||
} | |||
|
|||
/// Sort the map’s key-value pairs by the default ordering of the keys. | |||
pub fn sort_keys(&mut 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.
Perhaps a sort_keys_by
helper would be nice as 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.
Additionally, would sort_values
and sort_values_by
be good for completion?
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.
personal preference: I don't want API explosion and I doubt that would be used very often. Even when it is used, it would not have significant savings.
Benefit: if keys and values are comparable it might be possible to do an accidental footgun like:
h.sort_by(|(k1, k2, _, _)| k1.cmp(k2))
Needless to say, this would be annoying (IMO still not enough of a benefit to justify it though).
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 don't want API explosion either. And we need to keep some headroom for the (inevitable?) sort_unstably
family. Given the crate in question here, preferring stable sort seems obvious.
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.
Be a good sport about it, but You said “Would be nice”! We should not fall into the would be nice trap. Gets in the way of the "this is my actual usecase" stuff 😄
src/lib.rs
Outdated
new_index.sort_by(|&i, &j| { | ||
let ei = &self.entries[i]; | ||
let ej = &self.entries[j]; | ||
compare(&ei.key, &ei.value, &ej.key, &ej.value) |
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 measured and decrease in sort_by
benchmark time from using unchecked indexing is up to 10% here for getting ei
, ej
. Just FYI.
Well this is boring. In the benchmark vs the simple version that @vitiral wrote (using drain, sort, extend); the simple version wins. 100K key-value pairs key type: u32, value type: u32
We can decide what to do about that tomorrow. Now you see, sorting Vecs in place is pretty nice. (I wonder what we need to do this faster in place??) Can we change the rules to win? Key type String, Value type String; with 10K key-value pairs gives a slight preference to this PR instead
|
Ping @stjepang! Maybe you can see & superficially understand the application of The indirection in the current state of this PR is not good for performance, I think that's my conclusion. I'd like to sort the My mind is still on something I think we have talked about before -- sorting two slices in lock step. Imagine taking |
That is boring! I certainly wasn't relying to implement the fastest method,
haha
|
I appreciate the review! There are up to date benchmarks in the first post. They show the same thing as before just a bit more cleanly; the simple key/value type of u32 makes the simple sort implementation win. For complex types it should be important that this PR's sort saves us any rehashing or hash comparisons. The remaining performance challenge I can see here is not the lack of unchecked indexing or any simple fixes that we could do with We still merge this PR because this functionality is not the fastest but very useful, and we can improve upon it later. The comparison with the simple sort as of this post (now superseded with faster sort)
|
Whaaaat whaat wut wut there's a simple way to do the faster and in place-er The improvement of that new commit / new version of the sort by algo: (I may squash it later):
Now it's faster than the "simple" version too. 😄 |
- See implementation comments
Too awesome to sit around unreleased. |
ya, additional performance improvements can be done later. Glad you finally beat the simple version though 😄 |
@bluss Your solution is very nice - I think it doesn't get better than that. :) By the way, I was thinking... do we need a crate similar to // These are inspired by the STL in C++.
fn nth_element(&mut self, index: usize);
fn lower_bound(&self, &T) -> usize;
fn upper_bound(&self, &T) -> usize;
fn equal_range(&self, &T) -> (usize, usize);
fn next_permutation(&mut self) -> bool;
fn prev_permutation(&mut self) -> bool;
fn is_sorted(&self) -> bool;
// Similar to, but faster than `itertools::partition` (optimized for slices).
fn partition(&mut self, f: impl FnMut(&T) -> bool) -> usize;
// Merging.
fn merge(&mut self, mid: usize);
fn merge_in_place(&mut self, mid: usize);
// Lazy sorting.
fn sorted(self) -> impl Iterator<Item = T>;
fn sorted_unstable(self) -> impl Iterator<Item = T>;
// In-place version of stable sort (and compatible with `no_std`).
fn sort_in_place(&self);
// Sorting in lockstep.
fn sort_and_permute(&mut self, &mut [P]);
fn sort_unstable_and_permute(&mut self, &mut [P]); |
@stjepang I think that'd be lovely and I'd love to help out if you made a repo for it. Although I think that a better name might be |
Slice tools would be lovely. Crate odds contains some dusty gizmos for it as well, including BlockedIter. |
Can we use sort_unstably transparently in |
I would certainly hope so. I hadn't considered that before. |
Implementation is simple but it's not a slam dunk since it's not faster for both the implemented benchmarks. Not that we have very impressive benchmarks.
|
Benchmarks here: rust-lang/rust#40601 sorting strings is basically the case slice::sort_large_random_expensive because the benchmarks use uniform random order. |
This will seem pretty random, but benchmarks compensated for clone speed. Current stable vs unstable sort and for an element count of 24. But so that I have the numbers on file somewhere.r ordermap_sort_s 857 988 131 15.29%
ordermap_sort_u32 343 248 -95 -27.70% |
Implement an in place sorting method for OrderMap (and OrderSet). We use a very neat trick and temporarily save away the hash for each entry somewhere else and store the old index in the hash field. The benefit is that we can use
self.entries.sort_by
directly after that, which is very fast.I don't think the extra allocation is avoidable (but we could have unstable sort too, avoiding an allocation).
Benchmarks from the current version,
sort
is the sorting algorithm in this PR.The maps have 10k key-value pairs.
s
is forOrderMap<String, String>
andu32
is forOrderMap<u32, u32>
. "~As you can see, the simple implementation wins when the key/value types are simple.~~ (Now fixed!)