-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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
Improve BTreeSet::Intersection::size_hint #64383
Conversation
Thanks for the pull request, and welcome! The Rust team is excited to review your changes, and you should hear from @dtolnay (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. Please see the contribution instructions for more information. |
FYI @ssomers -- let me know if you have a preference for how is best to address this. |
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.
Thanks for catching this, @pcpthm!
@@ -1254,7 +1255,12 @@ impl<'a, T: Ord> Iterator for Intersection<'a, T> { | |||
match Ord::cmp(small_next, other_next) { | |||
Less => small_next = small_iter.next()?, | |||
Greater => other_next = other_iter.next()?, | |||
Equal => return Some(small_next), | |||
Equal => { | |||
if small_iter.len() > other_iter.len() { |
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.
Why swap them? It looks like the implementation of next
would be correct without swapping (though we would want to change the name of small_iter
as you observed). The reason to avoid swapping would be that this code typically runs many times while size_hint
would typically run at most once. We could have instead size_hint
take the minimum between the two child lengths.
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.
Thank you. I have changed to this way.
The commented invariant that an iterator is smaller than other iterator was violated after next is called and two iterators are consumed at different rates.
ee6af0b
to
4333b86
Compare
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.
Nice, looks good to me. Thanks!
@bors r+ |
📌 Commit 4333b86 has been approved by |
Improve BTreeSet::Intersection::size_hint A comment on `IntersectionInner` mentions `small_iter` should be smaller than `other_iter` but this condition is broken while iterating because those two iterators can be consumed at a different rate. I added a test to demonstrate this situation. <del>I made `small_iter.len() < other_iter.len()` always true by swapping two iterators when that condition became false. This change affects the return value of `size_hint`. The previous result was also correct but this new version always returns smaller upper bound than the previous version.</del> I changed `size_hint` to taking minimum of both lengths of iterators and renamed fields to `a` and `b` to match `Union` iterator.
In my mind, the "small" prefix was about the original sets; and completely out of my mind, was the possibility to get the size hint while intersection is ongoing. Well spotted and fixed.
|
☀️ Test successful - checks-azure |
Now the rust coming out of my brainworks has settled, I was mixing up stuff. The BTreeSet iterator already counts the remaining length. I think the only genuine case is draining down from a similar size to a much smaller size. For example, set A contains the numbers 0 to 999 and 2019, set B contains 1000 to 1999 and 2019. Currently, we iterate all of A and B to finally meet up at 2019. Instead we could see around 990 that it's more efficient to continue searching B for the few remaining items in A. At the cost of comparing the remaining sizes in every iteration (actually, only in the Less or Greater case if we've taken from the smaller one), and the cost of storing references to both sets. And considering that, in more complex examples, searching B will be costlier than the measurement the current rule for deciding up front was based on, because we can only search the entire B, not a set of the same size as the range we have yet to iterate. PS And an additional cost in other examples. Remove 2019 from A in the above example: the current code stops as soon as A peters out, but with the search "improvement", it needlessly searches B for some of A's elements. |
@ssomers I think both cases ( fn skip_while_less_than(&mut self, target: &K) -> Option<&K> {
loop {
let cur = self.next()?;
if cur >= target {
return Some(cur);
}
}
} But with more efficient searching. The intersection can be implemented like this: fn next(&mut self) -> Option<&'a K> {
loop {
let a_next = self.a.next()?;
let b_next = self.b.skip_while_less_than(a_next)?;
if a_next == b_next {
return Some(a_next);
}
swap(&mut self.a, &mut self.b); // or swap based on remaining size.
}
} Do you think it is worth to code? |
I have trouble wrapping my head around it right now, but it looks like the alternative "swivel" at https://github.com/ssomers/rust_bench_btreeset_intersection. It performed better in purposefully crafted examples, but not in general. |
@ssomers The operations are the same. However, by adding the lower-bound operation into the iterator, the time complexity should be improved compared to just calling |
But that experimental |
Not sure I should continue to post here in this thread, but I guess it does no harm. I managed to switch from stitch to search when needed, but it's quite an overhaul to change the value of an enum variant in a mutable member function. I think the code is much more clear about what is mutating, but not easier to understand. For the search case, reassessing which set is the small one is probably even more difficult: the existing Range do not have a size concept at all (that I could find). So nothing accomplished there. |
BTreeSet intersection, is_subset & difference optimizations ...based on the range of values contained; in particular, a massive improvement when these ranges are disjoint (or merely touching), like in the neg-vs-pos benchmarks already in liballoc. Inspired by rust-lang#64383 but none of the ideas there worked out. I introduced another variant in IntersectionInner and in DifferenceInner, because I couldn't find a way to initialize these iterators as empty if there's no empty set around. Also, reduced the size of "large" sets in test cases - if Miri can't handle it, it was needlessly slowing down everyone.
BTreeSet intersection, is_subset & difference optimizations ...based on the range of values contained; in particular, a massive improvement when these ranges are disjoint (or merely touching), like in the neg-vs-pos benchmarks already in liballoc. Inspired by rust-lang#64383 but none of the ideas there worked out. I introduced another variant in IntersectionInner and in DifferenceInner, because I couldn't find a way to initialize these iterators as empty if there's no empty set around. Also, reduced the size of "large" sets in test cases - if Miri can't handle it, it was needlessly slowing down everyone.
BTreeSet intersection, is_subset & difference optimizations ...based on the range of values contained; in particular, a massive improvement when these ranges are disjoint (or merely touching), like in the neg-vs-pos benchmarks already in liballoc. Inspired by rust-lang#64383 but none of the ideas there worked out. I introduced another variant in IntersectionInner and in DifferenceInner, because I couldn't find a way to initialize these iterators as empty if there's no empty set around. Also, reduced the size of "large" sets in test cases - if Miri can't handle it, it was needlessly slowing down everyone.
…t, r=Mark-Simulacrum BTreeMap: comment why drain_filter's size_hint is somewhat pessimistic The `size_hint` of the `DrainFilter` iterator doesn't adjust as you iterate. This hardly seems important to me, but there has been a comparable PR rust-lang#64383 in the past. I guess a scenario is that you first iterate half the map manually and keep most of the key/value pairs in the map, and then tell the predicate to drain most of the key/value pairs and `.collect` the iterator over the remaining half of the map. I am totally ambivalent whether this is better or not. r? @Mark-Simulacrum
A comment on
IntersectionInner
mentionssmall_iter
should be smaller thanother_iter
but this condition is broken while iterating because those two iterators can be consumed at a different rate. I added a test to demonstrate this situation.I madesmall_iter.len() < other_iter.len()
always true by swapping two iterators when that condition became false. This change affects the return value ofsize_hint
. The previous result was also correct but this new version always returns smaller upper bound than the previous version.I changed
size_hint
to taking minimum of both lengths of iterators and renamed fields toa
andb
to matchUnion
iterator.