Skip to content

Conversation

@cookiedan42
Copy link
Contributor

  • I agree to follow the project's code of conduct.
  • I added an entry to CHANGES.md if knowledge of this change could be valuable to users.

Description

Flatten the performance of Line-Rect intersection checks to ~ 4ns
The old best case scenario of Line Start being inside the Rect is slower (was ~ 2ns)

  • The short circuit option exists, but it adds to the worst case timing so...

But the worst case of disjoint geoms drops dramatically (was ~ 15ns)

Approach

  1. if corners of the Rect are all to the left or all to the right of the infinitely extended Line, the Rect cannot intersect the Line
  2. Since there is some part of the Rect which intersects or crosses the infinitely extended Line, then the Line must be pointing "toward" the Rect
  3. Due to this direction, the corner of the Line's bounding box which is closest to the Rect is one of the terminals of the Line
  4. Therefore if the Line's bounding box intersects the Rect, the Line must also intersect the Rect

using explicit variables instead of coords_iter because iter-map-collect approach benches at ~26ns

Benches

Line intersects Rect (end intersect)
                        time:   [3.9922 ns 3.9990 ns 4.0059 ns]
                        change: [+95.373% +96.046% +96.690%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) low severe

Line intersects Rect cross
                        time:   [4.0872 ns 4.1004 ns 4.1158 ns]
                        change: [-11.778% -11.384% -10.968%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
  3 (3.00%) low mild
  3 (3.00%) high mild
  2 (2.00%) high severe

Line disjoint Rect bbox disjoint
                        time:   [2.0466 ns 2.0495 ns 2.0525 ns]
                        change: [-83.383% -83.317% -83.262%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 5 outliers among 100 measurements (5.00%)
  1 (1.00%) low mild
  3 (3.00%) high mild
  1 (1.00%) high severe

Line disjoint Rect bbox overlap
                        time:   [4.1009 ns 4.1096 ns 4.1192 ns]
                        change: [-73.490% -73.423% -73.351%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 3 outliers among 100 measurements (3.00%)
  1 (1.00%) low mild
  1 (1.00%) high mild
  1 (1.00%) high severe

Copy link
Member

@michaelkirk michaelkirk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This LGTM. I have a suggestion which I think lessens the downsides, but I'm curious for your take on it.

I'll leave this open for another little bit in case anyone else wants to review.

let o2 = T::Ker::orient2d(rhs.start, rhs.end, c2);
let o3 = T::Ker::orient2d(rhs.start, rhs.end, c3);

o0 != o1 || o1 != o2 || o2 != o3 || o2 == Orientation::Collinear
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest hit to existing benchmarks seems to be:

// vs. `main`
ls within rect          time:   [10.871 ns 10.878 ns 10.887 ns]
                        change: [+145.22% +145.59% +145.98%] (p = 0.00 < 0.05)
                        Performance has regressed.

Though it's a LineString bench, I think it corresponds to your observation:

The old best case scenario of Line Start being inside the Rect is slower (was ~ 2ns)

Since the orient checks are expensive, one thing that helps a bit is to exit early:

if !self.intersects(&rhs.bounding_rect()) {
    return false;
}

let c0 = self.min();
let c1 = coord! {x: self.max().x, y: self.min().y};

let o0 = T::Ker::orient2d(rhs.start, rhs.end, c0);
let o1 = T::Ker::orient2d(rhs.start, rhs.end, c1);
if o0 != o1 {
    return true;
}

let c2 = self.max();
let o2 = T::Ker::orient2d(rhs.start, rhs.end, c2);
if o0 != o2 {
    return true;
}

let c3 = coord! {x: self.min().x, y: self.max().y};
let o3 = T::Ker::orient2d(rhs.start, rhs.end, c3);
if o0 != o3 {
    return true;
}

// safe to use n-1 comparisons because we know that if there is a different orientation,
// then there must be at least two edges along which the orientation of its points is different
debug_assert_eq!(o0, T::Ker::orient2d(rhs.start, rhs.end, self.max()));
        
// At this point we know all the orientations are equal and that the bounding boxes overlap.
// The only way there could be an intersection is if self (Rect) has degenerated to a line,
// and if `rhs` (Line) is on that line.
o0 == Orientation::Collinear
// vs. cookiedan42:perf/rect-intersect-line
ls within rect          time:   [8.7368 ns 8.7514 ns 8.7677 ns]
                        change: [-19.912% -19.754% -19.605%] (p = 0.00 < 0.05)
                        Performance has improved.

That bench is still slower than main, but less so, and seemingly no major downsides. Also it speeds up this new bench a bit:

// vs. cookiedan42:perf/rect-intersect-line
Line intersects Rect (end intersect)
                        time:   [5.6234 ns 5.6302 ns 5.6378 ns]
                        change: [-27.564% -27.414% -27.245%] (p = 0.00 < 0.05)
                        Performance has improved.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooh this is nice
It does subtly change the algorithm (comparing adjacent corners --> comparing corners against c0), but it still satisfies correctness, so I've updated the inline comments to reflect the new approach

@cookiedan42 cookiedan42 force-pushed the perf/rect-intersect-line branch from d8a801b to 3f85c11 Compare December 13, 2025 09:55
@cookiedan42 cookiedan42 force-pushed the perf/rect-intersect-line branch from 3f85c11 to 8ebdcca Compare December 13, 2025 10:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants