-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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 the collection views API for TrieMap. #18287
Conversation
Thanks a ton! On my phone will dig into this in a bit. |
Fixed some unused import warnings and removed the |
/// for very small numbers. For example, 1 and 2 are identical in all but their least significant | ||
/// 4 bits. If both numbers are used as keys, a chain of maximum length will be created to | ||
/// differentiate them. | ||
/// |
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.
Is there a name for this kind of Radix Trie? The Radix Trie I know allocates exactly one node per-element. Just enough to distinguish it from everything else. This implementation is like a hybrid xfast-radix trie, as described. I can see that it could be more efficient to have fixed-width chunks, 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.
Or maybe wikipedia's being dumb and radix/prefix/patricia aren't all synonyms?
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 wondered exactly this and I didn't find anything online until just now. These notes from CMU refer to this data structure simply as a "trie".
http://www.cs.cmu.edu/afs/cs/academic/class/15451-f04/www/lectures/lect0928.txt
I think the " radix" prefix is erroneous unless we implement node compression. Perhaps @thestinger, who seems to have written most of this, could shed some light?
It's quite tempting to implement a Patricia Trie or one of the X/Y trees to see how fast they are. That said, we already have Btrees for ordered maps, and I suspect the main use case for a trie would be semantic (finding prefix nodes, etc). This is what I designed my "generic trie" for, but the performance is pretty awful (about 10x slower than a HashMap, depending on key length).
Ack, got some stuff I need to take care of, might not be able to get back into this for a bit. If no one else picks this up in a few days, feel free to ping me as a reminder. |
Ok, I'll fix up the things you caught and play around with boxing a bit. I'm also suddenly very busy (3 midterms next week, yay!). |
Just fixed an awful bug in Working on performance now. |
@michaelsproul Sorry for the delay. Unfortunately Collections Reform just passed, which means everything is getting refactored. This might mess up your PR. @alexcrichton has volunteered to review this PR in the hopes that we can get this in before the really harsh breakage kicks in, though. (my reviews can't get things merged, anyway) Beyond tons of methods getting renamed and all the traits getting removed, TrieMap is getting moved into a trie directory and split into map.rs and set.rs. I'm trying to do this in a way that git can maybe understand and handle gracefully, but it'll probably cause a merge conflict. Ideally we can get this in first, and I can deal with the merge conflict for you. Worst-case scenario, you might have to do some patch-fiddling or copy-pasting. Again, I hope to avoid this. |
Yeah, I don't mind a bit of merge conflict resolution. |
Sorry for the delay! Just landed The Big Break. We can start reasonably handling normal collections PRs again. Ready to review once you rebase! |
Hey! All done! Quite a painless merge thanks to the magic of Git. I gave up working on performance, as I think our energy would be better spent implementing a Patricia Trie with the collection views API in mind from the start. I've started digging in to the literature and I'll be starting soon (other workloads permitting). Here are some benchmarks for
|
key, value, 1); | ||
if ret.is_none() { self.length += 1 } | ||
ret | ||
let (_, old_val) = insert( &mut self.root.count, |
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.
Absolutely trivial style nit: any reason everything in the parens has an extra space? tab-alignment?
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.
Yeah, that was for tab alignment. Could just flow it.
// Extract the value from the leaf-node of interest. | ||
let leaf_node = mem::replace(search_stack.pop_ref(), Nothing); | ||
|
||
let value: T = match leaf_node { |
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.
Is this type annotation necessary?
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.
Nope, but I like an occasional type annotation...
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.
shrug It's probably fine.
@michaelsproul I've reviewed everything. If you're uncomfortable with fiddling with the transmutes, I think it's fine if you just put in a FIXME to the effect of "these transmutes might be invoking UB, and are unidiomatic at best; find a way to remove them". When you think you've addressed everything, post a new comment. |
Oh: and I'm not super confident about all the re-checking you do on the key. I think it's fine for the entries to assume they were initialized correctly as long as failing to do so only produces wrong results, and not unsafe results. |
@@ -24,7 +24,7 @@ | |||
|
|||
#![allow(unknown_features)] | |||
#![feature(macro_rules, default_type_params, phase, globs)] | |||
#![feature(unsafe_destructor, import_shadowing, slicing_syntax)] | |||
#![feature(unsafe_destructor, import_shadowing, slicing_syntax, if_let)] |
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.
In general I'd prefer to avoid introducing even more gated features to the stdlib for the moment. I mean, yeah, it's already got a huge pile of gated features and we're probably going to ungate if-let for 1.0, so I'm not going to reject on these grounds... but it's still something to keep in mind.
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.
Ok, I'll take it out.
Punting transmute stuff to #18817 |
// While no appropriate slot is found, keep descending down the Trie, | ||
// adding nodes to the search stack. | ||
let search_successful: bool; | ||
unsafe { |
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.
Which of the calls in the following block are actually unsafe? If nothing else, could we move this inside the loop?
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 call to next_child
is unsafe, so we could only put the unsafe block inside the loop.
Alternatively, we could make next_child
provide a safe interface...
Looks good to me, r+ waiting on getting an answer to those last two questions. |
Waiting on your thoughts re: unsafety of |
let search_successful: bool; | ||
loop { | ||
unsafe { | ||
match next_child(search_stack.peek(), key, search_stack.length) { |
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.
If next_child
is the only unsafe call, then I'd prefer to reduce the scope of the unsafe like so:
match unsafe { next_child(search_stack.peek(), key, search_stack.length) } {
This makes it much more obvious where the unsafe bits lie, and precludes more unsafety from sneaking into the match body later. Also, I'm a bit of a stickler for shrinking unsafe scopes. :) This is my last criticism, I swear!
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 like it!
I lied! One more minor nit: I think it's a shame that changing those |
Thanks! |
Thank you! Should I do a rebase? There are lots of messy commits. |
Sure thing. |
Thanks a bunch for persisting on this! 😍 |
@gankro: It was an absolute pleasure 😄 |
Thanks! 💃 |
…=bstrie I've implemented the new collection views API for TrieMap. I more or less followed the approach set out by @gankro in BTreeMap, by using a `SearchStack`. There's quite a bit of unsafe code, but I've wrapped it safely where I think is appropriate. I've added tests to ensure everything works, and performance seems quite good. ``` test trie::bench_map::bench_find ... bench: 67879 ns/iter (+/- 4192) test trie::bench_map::bench_find_entry ... bench: 186814 ns/iter (+/- 18748) test trie::bench_map::bench_insert_large ... bench: 716612 ns/iter (+/- 160121) test trie::bench_map::bench_insert_large_entry ... bench: 851219 ns/iter (+/- 20331) test trie::bench_map::bench_remove ... bench: 838856 ns/iter (+/- 27998) test trie::bench_map::bench_remove_entry ... bench: 981711 ns/iter (+/- 53046) ``` Using an entry is slow compared to a plain find, but is only ~15% slower for inserts and removes, which is where this API is most useful. I'm tempted to remove the standalone `remove` function in favour of an entry-based approach (to cut down on complexity). I've added some more comments to the general part of the code-base, which will hopefully help the next person looking over this. I moved the three key structures to the top of the file so that the nesting structure is clearly visible, and renamed `Child<T>` to `TrieNode<T>` and `TrieNode<T>` to `InternalNode<T>` to improve clarity. If these changes are creeping, I'm happy to revert them. Let me know if my use of `fail!` is ok, I was a little unsure of how specific to be. Some of the data-structures have various invariants that shouldn't be broken, so using `fail!` seemed appropriate. ## Still to do * Modernise iterators (make them double-ended). * Make the keys generic, or rename this data-structure (see: #14902). * Possibly move this code out of libcollections. [Searching Github for TrieMap turns up very few real results.][triemap-search] Related issues: #18009 and #17320 [triemap-search]: https://github.com/search?utf8=%E2%9C%93&q=TrieMap+language%3ARust&type=Code&ref=searchresults
fix: private items are shown in completions for modules in fn body Close: rust-lang#18287
I've implemented the new collection views API for TrieMap. I more or less followed the approach set out by @gankro in BTreeMap, by using a
SearchStack
. There's quite a bit of unsafe code, but I've wrapped it safely where I think is appropriate. I've added tests to ensure everything works, and performance seems quite good.Using an entry is slow compared to a plain find, but is only ~15% slower for inserts and removes, which is where this API is most useful. I'm tempted to remove the standalone
remove
function in favour of an entry-based approach (to cut down on complexity).I've added some more comments to the general part of the code-base, which will hopefully help the next person looking over this. I moved the three key structures to the top of the file so that the nesting structure is clearly visible, and renamed
Child<T>
toTrieNode<T>
andTrieNode<T>
toInternalNode<T>
to improve clarity. If these changes are creeping, I'm happy to revert them.Let me know if my use of
fail!
is ok, I was a little unsure of how specific to be. Some of the data-structures have various invariants that shouldn't be broken, so usingfail!
seemed appropriate.Still to do
Related issues: #18009 and #17320