Skip to content

Commit 4c28c6c

Browse files
authored
perf: more efficient intersection (#157)
* refactor: more efficient intersection * more comments
1 parent 4d78a64 commit 4c28c6c

File tree

1 file changed

+53
-25
lines changed

1 file changed

+53
-25
lines changed

src/range.rs

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -350,41 +350,69 @@ impl<V: Ord + Clone> Range<V> {
350350

351351
/// Computes the intersection of two sets of versions.
352352
pub fn intersection(&self, other: &Self) -> Self {
353-
let mut segments: SmallVec<Interval<V>> = SmallVec::empty();
353+
let mut output: SmallVec<Interval<V>> = SmallVec::empty();
354354
let mut left_iter = self.segments.iter().peekable();
355355
let mut right_iter = other.segments.iter().peekable();
356-
357-
while let (Some((left_start, left_end)), Some((right_start, right_end))) =
358-
(left_iter.peek(), right_iter.peek())
356+
// By the definition of intersection any point that is matched by the output
357+
// must have a segment in each of the inputs that it matches.
358+
// Therefore, every segment in the output must be the intersection of a segment from each of the inputs.
359+
// It would be correct to do the "O(n^2)" thing, by computing the intersection of every segment from one input
360+
// with every segment of the other input, and sorting the result.
361+
// We can avoid the sorting by generating our candidate segments with an increasing `end` value.
362+
while let Some(((left_start, left_end), (right_start, right_end))) =
363+
left_iter.peek().zip(right_iter.peek())
359364
{
365+
// The next smallest `end` value is going to come from one of the inputs.
366+
let left_end_is_smaller = match (left_end, right_end) {
367+
(Included(l), Included(r))
368+
| (Excluded(l), Excluded(r))
369+
| (Excluded(l), Included(r)) => l <= r,
370+
371+
(Included(l), Excluded(r)) => l < r,
372+
(_, Unbounded) => true,
373+
(Unbounded, _) => false,
374+
};
375+
// Now that we are processing `end` we will never have to process any segment smaller than that.
376+
// We can ensure that the input that `end` came from is larger than `end` by advancing it one step.
377+
// `end` is the smaller available input, so we know the other input is already larger than `end`.
378+
// Note: We can call `other_iter.next_if( == end)`, but the ends lining up is rare enough that
379+
// it does not end up being faster in practice.
380+
let (other_start, end) = if left_end_is_smaller {
381+
left_iter.next();
382+
(right_start, left_end)
383+
} else {
384+
right_iter.next();
385+
(left_start, right_end)
386+
};
387+
// `start` will either come from the input `end` came from or the other input, whichever one is larger.
388+
// The intersection is invalid if `start` > `end`.
389+
// But, we already know that the segments in our input are valid.
390+
// So we do not need to check if the `start` from the input `end` came from is smaller then `end`.
391+
// If the `other_start` is larger than end, then the intersection will be invalid.
392+
if !valid_segment(other_start, end) {
393+
// Note: We can call `this_iter.next_if(!valid_segment(other_start, this_end))` in a loop.
394+
// But the checks make it slower for the benchmarked inputs.
395+
continue;
396+
}
360397
let start = match (left_start, right_start) {
361398
(Included(l), Included(r)) => Included(std::cmp::max(l, r)),
362399
(Excluded(l), Excluded(r)) => Excluded(std::cmp::max(l, r)),
363400

364-
(Included(i), Excluded(e)) | (Excluded(e), Included(i)) if i <= e => Excluded(e),
365-
(Included(i), Excluded(e)) | (Excluded(e), Included(i)) if e < i => Included(i),
366-
(s, Unbounded) | (Unbounded, s) => s.as_ref(),
367-
_ => unreachable!(),
368-
}
369-
.cloned();
370-
let end = match (left_end, right_end) {
371-
(Included(l), Included(r)) => Included(std::cmp::min(l, r)),
372-
(Excluded(l), Excluded(r)) => Excluded(std::cmp::min(l, r)),
373-
374-
(Included(i), Excluded(e)) | (Excluded(e), Included(i)) if i >= e => Excluded(e),
375-
(Included(i), Excluded(e)) | (Excluded(e), Included(i)) if e > i => Included(i),
401+
(Included(i), Excluded(e)) | (Excluded(e), Included(i)) => {
402+
if i <= e {
403+
Excluded(e)
404+
} else {
405+
Included(i)
406+
}
407+
}
376408
(s, Unbounded) | (Unbounded, s) => s.as_ref(),
377-
_ => unreachable!(),
378-
}
379-
.cloned();
380-
left_iter.next_if(|(_, e)| e == &end);
381-
right_iter.next_if(|(_, e)| e == &end);
382-
if valid_segment(&start, &end) {
383-
segments.push((start, end))
384-
}
409+
};
410+
// Now we clone and push a new segment.
411+
// By dealing with references until now we ensure that NO cloning happens when we reject the segment.
412+
output.push((start.cloned(), end.clone()))
385413
}
386414

387-
Self { segments }.check_invariants()
415+
Self { segments: output }.check_invariants()
388416
}
389417

390418
/// Returns a simpler Range that contains the same versions

0 commit comments

Comments
 (0)