-
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
Bundle and document 6 BTreeMap navigation algorithms #67073
Conversation
r? @kennytm (rust_highfive has picked a reviewer for you, use r? to override) |
Let me try that again... r? @cuviper |
Checking again with miri (properly or with newer version), there are plenty of leaks. |
Sorry I lost the review on this.
Having closed this PR, do you intend to address those issues and reopen it? |
Sure, reopening is my intent. But it doesn't look as straightforward as it did hours ago. |
This comment has been minimized.
This comment has been minimized.
dd21044
to
1d3e6ed
Compare
Meanwhile miri says drain_filter building on this PR also works fine |
This comment has been minimized.
This comment has been minimized.
☔ The latest upstream changes (presumably #67540) made this pull request unmergeable. Please resolve the merge conflicts. |
026467e
to
9b50b02
Compare
Last 2 commits were only to rebase on a recent master branch (which includes code reformatting for the 1st commit) |
This comment has been minimized.
This comment has been minimized.
9b50b02
to
faaa9f2
Compare
This comment has been minimized.
This comment has been minimized.
5201143
to
58a00f3
Compare
I forgot the rest of the changes I wanted to do, i.e. revert to the order of handle manipulation that is in master (which is easy once there were 3 macros). So that this PR is purely about reorganizing code and exposing functions for later use (i.e. drain_filter). Same points as above, but the performance now is on par with master:
It turns out that delaying the |
One danger I struggled with, is on a higher level of duplication: in |
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 this a lot better! There's a clearer value now in having these all defined in the same focused module, and the required safety is much better explained.
/// It is, however, safe to pass the leaf edge variable again to this function, | ||
/// if the two preconditions above hold. | ||
/// - using the updated handle in a safe way may well invalidate the returned references. | ||
pub unsafe fn hop_to_next_back_unchecked_deallocating<K, V>( |
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.
(bikeshed) These names are quite a mouthful. What if we just made them all inherent methods on Handle
? They can even overlap names since their borrow-type markers are different. Something like:
impl<'a, K, V> Handle<NodeRef<marker::Immut<'a>, K, V, marker::Leaf>, marker::Edge> {
pub unsafe fn next_unchecked(&mut self) -> (&'a K, &'a V) { ... }
pub unsafe fn next_back_unchecked(&mut self) -> (&'a K, &'a V) { ... }
}
impl<'a, K, V> Handle<NodeRef<marker::Mut<'a>, K, V, marker::Leaf>, marker::Edge> {
pub unsafe fn next_unchecked(&mut self) -> (&'a K, &'a mut V) { ... }
pub unsafe fn next_back_unchecked(&mut self) -> (&'a K, &'a mut V) { ... }
}
impl<K, V> Handle<NodeRef<marker::Owned, K, V, marker::Leaf>, marker::Edge> {
pub unsafe fn next_unchecked(&mut self) -> (K, V) { ... }
pub unsafe fn next_back_unchecked(&mut self) -> (K, V) { ... }
}
I think that will improve readability at the call sites too:
- let (k, v) =
- unsafe { navigate::hop_to_next_back_unchecked_deallocating(&mut self.back) };
- Some((k, v))
+ Some(unsafe { self.back.next_back_unchecked() })
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 am happy either way, I don't know what's the usual or desired style is, but are you aware that first_leaf_edge
/last_leaf_edge
were standalone functions in map.rs, and that all impl
blocks in map.rs are for types defined in itself? So I figured that in order to make for instance first_leaf_edge
a method on NodeRef
, like first_edge
is, it would have to move to node.rs. Which would be both good (there is some similarity) and bad (node.rs is pretty fat already, though not as fat as map.rs). search.rs seemed most similar, and it has standalone functions.
Secondly, is it obvious that iterating over an owned tree deallocates it? I guess since it returns ownership of K and V, you have to realize the tree can't survive and the function has to be used in an IntoIterator. But if you're the owner of a tree and you somehow want to iterate through it (sending everyone a letter of resignation) without already destroying it?
Thirdly, if we do that, then do we remove the paper thin, private Range::next_back_unchecked function?
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.
Also, I stuck on the hop_on_ prefix specifically to distinguish them from the existing next_... functions. But since they're prefixed with navigate::
when used, that was pointless.
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.
Naming is certainly subjective, which is why I prefixed this as a bikeshed comment. 🙂
it would have to move to node.rs.
It does not have to move -- methods can be added from anywhere in the same crate. Maybe the original author of first
/last_leaf_edge
just didn't know you could do that. Such methods even get privacy according to where they are defined, rather than from the type definition.
These are all pub
anyway, though not fully publicly reachable -- maybe they should be tightened to pub(crate)
or even pub(super)
. (That's a possible cleanup across most of btree
.)
Secondly, is it obvious that iterating over an owned tree deallocates it?
Seems ok to me, but that's a subjective point too.
Thirdly, if we do that, then do we remove the paper thin, private Range::next_back_unchecked function?
There's still a little value in discriminating the range's front
or back
for those that are calling self.inner.next_unchecked
etc.
Also, I stuck on the hop_on_ prefix specifically to distinguish them from the existing next_... functions. But since they're prefixed with
navigate::
when used, that was pointless.
Well, there aren't existing next*
methods on Handle
, at least, so I think it's ok. It's a nice symmetry IMO that outer next
just calls the handle's next_unchecked
, and next_back
calls next_back_unchecked
.
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.
maybe they should be tightened to
pub(crate)
or evenpub(super)
.
Aren't they already effectively something like that because mod.rs does not pub-lish module 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.
Aren't they already effectively something like that because mod.rs does not pub-lish module node?
Right, they're not actually reachable, and the compiler knows that -- it will even tell you that with RUSTFLAGS="-W unreachable_pub"
or #![warn(unreachable_pub)]
in the code. That lint is allowed (silent) by default.
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 that while rebasing, I noticed #66648 has added a mut
to the return signature of next_unchecked
. I'm doing the same in next_back_unchecked
.
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.
Changing the pub
visibility would be more about making sure they're never exposed by accident. But the compiler enforces stability attributes on truly public items in the standard library anyway, due to #![feature(staged_api)]
, so I guess that doesn't matter.
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.
Also, added the same is_empty
to Range
as was added to RangeMut
and narrowed down the unsafe sections.
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.
These are all pub anyway, though not fully publicly reachable
Not sure what you mean with "not fully", but if they're not explicitly pub
in navigate.rs, trying to call them in map.rs makes the compiler bark violently that they're private. So I'd rather say they're fully not public.
☔ The latest upstream changes (presumably #68659) made this pull request unmergeable. Please resolve the merge conflicts. |
6d64954
to
0db734a
Compare
Miri is still happy, full test build with debug-assertions passes. |
0db734a
to
3cf724d
Compare
I missed that the comments in navigate.rs had to adapt to their handle method ecosystem. |
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.
Looks great!
@bors r+ |
📌 Commit 3cf724d has been approved by |
Bundle and document 6 BTreeMap navigation algorithms - Expose a function to step through trees, without necessarily extracting the KV pair, that helps future operations like drain/retain (as demonstrated in [this drain_filter implementation](https://github.com/ssomers/rust/compare/btree_navigation_v3...ssomers:btree_drain_filter?expand=1)) - ~~Also aligns the implementation of the 2 x 3 iterators already using such navigation:~~ - ~~Delay the moment the K,V references are plucked from the tree, for the 4 iterators on immutable and owned maps, just for symmetry. The same had to be done to the two iterators for mutable maps in #58431.~~ - ~~Always explicitly use ptr::read to derive two handles from one handle. While the existing implementations for immutable maps (i.e. Range::next_unchecked and Range::next_back_unchecked) rely on implicit copying. There's no change in unsafe tags because these two functions were already (erroneously? prophetically?) tagged unsafe. I don't know whether they should be tagged unsafe. I guess they should be for mutable and owned maps, because you can change the map through one handle and leave the other handle invalid.~~ - Preserve the way two handles are (temporarily) derived from one handle: implementations for immutable maps (i.e. Range::next_unchecked and Range::next_back_unchecked) rely on copying (implicitly before, explicitly now) and the others do `ptr::read`. - ~~I think the functions that support iterators on immutable trees (i.e. `Range::next_unchecked` and `Range::next_back_unchecked`) are erroneously tagged unsafe since you can already create multiple instances of such ranges, thus obtain multiple handles into the same tree. I did not change that but removed unsafe from the functions underneath.~~ Tested with miri in liballoc/tests/btree, except those that should_panic. cargo benchcmp the best of 3 samples of all btree benchmarks before and after this PR: ``` name old1.txt ns/iter new2.txt ns/iter diff ns/iter diff % speedup btree::map::find_rand_100 17 17 0 0.00% x 1.00 btree::map::find_rand_10_000 57 55 -2 -3.51% x 1.04 btree::map::find_seq_100 17 17 0 0.00% x 1.00 btree::map::find_seq_10_000 42 39 -3 -7.14% x 1.08 btree::map::first_and_last_0 14 14 0 0.00% x 1.00 btree::map::first_and_last_100 36 37 1 2.78% x 0.97 btree::map::first_and_last_10k 52 52 0 0.00% x 1.00 btree::map::insert_rand_100 34 34 0 0.00% x 1.00 btree::map::insert_rand_10_000 34 34 0 0.00% x 1.00 btree::map::insert_seq_100 46 46 0 0.00% x 1.00 btree::map::insert_seq_10_000 90 89 -1 -1.11% x 1.01 btree::map::iter_1000 2,811 2,771 -40 -1.42% x 1.01 btree::map::iter_100000 360,635 355,995 -4,640 -1.29% x 1.01 btree::map::iter_20 39 42 3 7.69% x 0.93 btree::map::iter_mut_1000 2,662 2,864 202 7.59% x 0.93 btree::map::iter_mut_100000 336,825 346,550 9,725 2.89% x 0.97 btree::map::iter_mut_20 40 43 3 7.50% x 0.93 btree::set::build_and_clear 4,184 3,994 -190 -4.54% x 1.05 btree::set::build_and_drop 4,151 3,976 -175 -4.22% x 1.04 btree::set::build_and_into_iter 4,196 4,155 -41 -0.98% x 1.01 btree::set::build_and_pop_all 5,176 5,241 65 1.26% x 0.99 btree::set::build_and_remove_all 6,868 6,903 35 0.51% x 0.99 btree::set::difference_random_100_vs_100 721 691 -30 -4.16% x 1.04 btree::set::difference_random_100_vs_10k 2,420 2,411 -9 -0.37% x 1.00 btree::set::difference_random_10k_vs_100 54,726 52,215 -2,511 -4.59% x 1.05 btree::set::difference_random_10k_vs_10k 164,384 170,256 5,872 3.57% x 0.97 btree::set::difference_staggered_100_vs_100 739 709 -30 -4.06% x 1.04 btree::set::difference_staggered_100_vs_10k 2,320 2,265 -55 -2.37% x 1.02 btree::set::difference_staggered_10k_vs_10k 68,020 70,246 2,226 3.27% x 0.97 btree::set::intersection_100_neg_vs_100_pos 23 24 1 4.35% x 0.96 btree::set::intersection_100_neg_vs_10k_pos 28 29 1 3.57% x 0.97 btree::set::intersection_100_pos_vs_100_neg 24 24 0 0.00% x 1.00 btree::set::intersection_100_pos_vs_10k_neg 28 28 0 0.00% x 1.00 btree::set::intersection_10k_neg_vs_100_pos 27 27 0 0.00% x 1.00 btree::set::intersection_10k_neg_vs_10k_pos 30 29 -1 -3.33% x 1.03 btree::set::intersection_10k_pos_vs_100_neg 27 28 1 3.70% x 0.96 btree::set::intersection_10k_pos_vs_10k_neg 29 29 0 0.00% x 1.00 btree::set::intersection_random_100_vs_100 592 572 -20 -3.38% x 1.03 btree::set::intersection_random_100_vs_10k 2,271 2,269 -2 -0.09% x 1.00 btree::set::intersection_random_10k_vs_100 2,301 2,333 32 1.39% x 0.99 btree::set::intersection_random_10k_vs_10k 147,879 150,148 2,269 1.53% x 0.98 btree::set::intersection_staggered_100_vs_100 622 632 10 1.61% x 0.98 btree::set::intersection_staggered_100_vs_10k 2,101 2,032 -69 -3.28% x 1.03 btree::set::intersection_staggered_10k_vs_10k 60,341 61,834 1,493 2.47% x 0.98 btree::set::is_subset_100_vs_100 417 426 9 2.16% x 0.98 btree::set::is_subset_100_vs_10k 1,281 1,324 43 3.36% x 0.97 btree::set::is_subset_10k_vs_100 2 2 0 0.00% x 1.00 btree::set::is_subset_10k_vs_10k 41,054 41,612 558 1.36% x 0.99 ``` r? cuviper
☀️ Test successful - checks-azure |
…ulacrum BTreeMap navigation done safer & faster It turns out that there was a faster way to do the tree navigation code bundled in #67073, by moving from edge to KV and from KV to next edge separately. It extracts most of the code as safe functions, and contains the duplication of handles within the short wrapper functions. This somehow hits a sweet spot in the compiler because it reports boosts all over the board: ``` >cargo benchcmp pre3.txt posz4.txt --threshold 5 name pre3.txt ns/iter posz4.txt ns/iter diff ns/iter diff % speedup btree::map::first_and_last_0 40 37 -3 -7.50% x 1.08 btree::map::first_and_last_100 58 44 -14 -24.14% x 1.32 btree::map::iter_1000 8,920 3,419 -5,501 -61.67% x 2.61 btree::map::iter_100000 1,069,290 411,615 -657,675 -61.51% x 2.60 btree::map::iter_20 169 58 -111 -65.68% x 2.91 btree::map::iter_mut_1000 8,701 3,303 -5,398 -62.04% x 2.63 btree::map::iter_mut_100000 1,034,560 405,975 -628,585 -60.76% x 2.55 btree::map::iter_mut_20 165 58 -107 -64.85% x 2.84 btree::set::clone_100 1,831 1,562 -269 -14.69% x 1.17 btree::set::clone_100_and_clear 1,831 1,565 -266 -14.53% x 1.17 btree::set::clone_100_and_into_iter 1,917 1,541 -376 -19.61% x 1.24 btree::set::clone_100_and_pop_all 2,609 2,441 -168 -6.44% x 1.07 btree::set::clone_100_and_remove_all 4,598 3,927 -671 -14.59% x 1.17 btree::set::clone_100_and_remove_half 2,765 2,551 -214 -7.74% x 1.08 btree::set::clone_10k 191,610 164,616 -26,994 -14.09% x 1.16 btree::set::clone_10k_and_clear 192,003 164,616 -27,387 -14.26% x 1.17 btree::set::clone_10k_and_into_iter 200,037 163,010 -37,027 -18.51% x 1.23 btree::set::clone_10k_and_pop_all 267,023 250,913 -16,110 -6.03% x 1.06 btree::set::clone_10k_and_remove_all 536,230 464,100 -72,130 -13.45% x 1.16 btree::set::clone_10k_and_remove_half 453,350 430,545 -22,805 -5.03% x 1.05 btree::set::difference_random_100_vs_100 1,787 801 -986 -55.18% x 2.23 btree::set::difference_random_100_vs_10k 2,978 2,696 -282 -9.47% x 1.10 btree::set::difference_random_10k_vs_100 111,075 54,734 -56,341 -50.72% x 2.03 btree::set::difference_random_10k_vs_10k 246,380 175,980 -70,400 -28.57% x 1.40 btree::set::difference_staggered_100_vs_100 1,789 951 -838 -46.84% x 1.88 btree::set::difference_staggered_100_vs_10k 2,798 2,606 -192 -6.86% x 1.07 btree::set::difference_staggered_10k_vs_10k 176,452 97,401 -79,051 -44.80% x 1.81 btree::set::intersection_100_neg_vs_10k_pos 34 32 -2 -5.88% x 1.06 btree::set::intersection_100_pos_vs_100_neg 30 27 -3 -10.00% x 1.11 btree::set::intersection_random_100_vs_100 1,537 613 -924 -60.12% x 2.51 btree::set::intersection_random_100_vs_10k 2,793 2,649 -144 -5.16% x 1.05 btree::set::intersection_random_10k_vs_10k 222,127 147,166 -74,961 -33.75% x 1.51 btree::set::intersection_staggered_100_vs_100 1,447 622 -825 -57.01% x 2.33 btree::set::intersection_staggered_100_vs_10k 2,606 2,382 -224 -8.60% x 1.09 btree::set::intersection_staggered_10k_vs_10k 143,620 58,790 -84,830 -59.07% x 2.44 btree::set::is_subset_100_vs_100 1,349 488 -861 -63.83% x 2.76 btree::set::is_subset_100_vs_10k 1,720 1,428 -292 -16.98% x 1.20 btree::set::is_subset_10k_vs_10k 135,984 48,527 -87,457 -64.31% x 2.80 ``` The `first_and_last` ones are noise (they don't do iteration), the others seem genuine. As always, approved by Miri. Also, a separate commit with some more benchmarks of mutable behaviour (which also benefit). r? @cuviper
Also aligns the implementation of the 2 x 3 iterators already using such navigation:Delay the moment the K,V references are plucked from the tree, for the 4 iterators on immutable and owned maps, just for symmetry. The same had to be done to the two iterators for mutable maps in fix overlapping references in BTree #58431.Always explicitly use ptr::read to derive two handles from one handle. While the existing implementations for immutable maps (i.e. Range::next_unchecked and Range::next_back_unchecked) rely on implicit copying. There's no change in unsafe tags because these two functions were already (erroneously? prophetically?) tagged unsafe. I don't know whether they should be tagged unsafe. I guess they should be for mutable and owned maps, because you can change the map through one handle and leave the other handle invalid.ptr::read
.I think the functions that support iterators on immutable trees (i.e.Range::next_unchecked
andRange::next_back_unchecked
) are erroneously tagged unsafe since you can already create multiple instances of such ranges, thus obtain multiple handles into the same tree. I did not change that but removed unsafe from the functions underneath.Tested with miri in liballoc/tests/btree, except those that should_panic.
cargo benchcmp the best of 3 samples of all btree benchmarks before and after this PR:
r? cuviper