Skip to content

Conversation

souma4
Copy link
Contributor

@souma4 souma4 commented Sep 30, 2025

Upon reviewing the performance of comparative implementations, I decided to move from the very complicated and numerically unstable implementation in #1168 to a simpler interval sweepline (ie, we iterate over a sorted sequence by the furthest left x and check intersections if two segments have overlapping intervals).

This is equivalent to the Bentley-Ottmann algorithm in sparse cases and is O(N^2) in dense cases. However, in dense cases, Bentley-Ottmann computes many more intersections (to traverse the Binary search tree). These roughly balance out. In unofficial tests against a dense vector of segments, this implementation was the second fastest, only slower than crate::geo.

Profiling shows most time is spent exclusively in runtime and GC dispatch within the Intersection(...) do block, so improving efficiency there would be valuable.

Copy link
Contributor

github-actions bot commented Sep 30, 2025

Benchmark Results (Julia v1)

Time benchmarks
master 63a26f9... master / 63a26f9...
clipping/SutherlandHodgman 3.36 ± 0.22 μs 3.39 ± 0.22 μs 0.991 ± 0.092
discretization/simplexify 25.4 ± 3.8 ms 24.6 ± 3.9 ms 1.03 ± 0.22
intersection/ray-triangle 0.06 ± 0.001 μs 0.06 ± 0 μs 1 ± 0.017
sideof/ring/large 6.78 ± 0.25 μs 6.84 ± 0.061 μs 0.991 ± 0.038
sideof/ring/small 0.06 ± 0.009 μs 0.06 ± 0.01 μs 1 ± 0.22
topology/half-edge 2.81 ± 0.23 ms 2.81 ± 0.19 ms 0.999 ± 0.11
winding/mesh 0.0427 ± 0.0026 s 0.0425 ± 0.0029 s 1.01 ± 0.092
time_to_load 1.54 ± 0.012 s 1.52 ± 0.01 s 1.01 ± 0.01
Memory benchmarks
master 63a26f9... master / 63a26f9...
clipping/SutherlandHodgman 0.053 k allocs: 4.83 kB 0.053 k allocs: 4.83 kB 1
discretization/simplexify 0.324 M allocs: 21.8 MB 0.324 M allocs: 21.8 MB 1
intersection/ray-triangle 0 allocs: 0 B 0 allocs: 0 B
sideof/ring/large 0 allocs: 0 B 0 allocs: 0 B
sideof/ring/small 0 allocs: 0 B 0 allocs: 0 B
topology/half-edge 20.8 k allocs: 2.92 MB 20.8 k allocs: 2.92 MB 1
winding/mesh 0.329 M allocs: 22.9 MB 0.329 M allocs: 22.9 MB 1
time_to_load 0.159 k allocs: 11.2 kB 0.159 k allocs: 11.2 kB 1

Copy link
Contributor

github-actions bot commented Sep 30, 2025

Benchmark Results (Julia vlts)

Time benchmarks
master 63a26f9... master / 63a26f9...
clipping/SutherlandHodgman 3.74 ± 0.34 μs 3.85 ± 1.6 μs 0.971 ± 0.42
discretization/simplexify 27.8 ± 1.5 ms 28.1 ± 1.6 ms 0.987 ± 0.077
intersection/ray-triangle 0.05 ± 0.001 μs 0.05 ± 0.001 μs 1 ± 0.028
sideof/ring/large 6.53 ± 0.01 μs 6.53 ± 0.01 μs 1 ± 0.0022
sideof/ring/small 0.05 ± 0.001 μs 0.05 ± 0.001 μs 1 ± 0.028
topology/half-edge 2.71 ± 0.036 ms 2.71 ± 0.049 ms 0.998 ± 0.022
winding/mesh 0.0435 ± 0.0017 s 0.0438 ± 0.0017 s 0.991 ± 0.056
time_to_load 1.46 ± 0.004 s 1.45 ± 0.0015 s 1 ± 0.0029
Memory benchmarks
master 63a26f9... master / 63a26f9...
clipping/SutherlandHodgman 0.053 k allocs: 4.97 kB 0.053 k allocs: 4.97 kB 1
discretization/simplexify 0.226 M allocs: 21.8 MB 0.226 M allocs: 21.8 MB 1
intersection/ray-triangle 0 allocs: 0 B 0 allocs: 0 B
sideof/ring/large 0 allocs: 0 B 0 allocs: 0 B
sideof/ring/small 0 allocs: 0 B 0 allocs: 0 B
topology/half-edge 18.1 k allocs: 2.92 MB 18.1 k allocs: 2.92 MB 1
winding/mesh 0.231 M allocs: 23 MB 0.231 M allocs: 23 MB 1
time_to_load 0.153 k allocs: 14.5 kB 0.153 k allocs: 14.5 kB 1

Copy link

codecov bot commented Sep 30, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.89%. Comparing base (db4881f) to head (e8b8585).
⚠️ Report is 7 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1251      +/-   ##
==========================================
+ Coverage   87.86%   87.89%   +0.02%     
==========================================
  Files         196      197       +1     
  Lines        6181     6235      +54     
==========================================
+ Hits         5431     5480      +49     
- Misses        750      755       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@souma4 souma4 marked this pull request as ready for review September 30, 2025 18:49
Copy link
Member

@juliohm juliohm left a comment

Choose a reason for hiding this comment

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

Thank you @souma4! First round of review attached. This one should be much easier to get ready for a merge.

souma4 added 2 commits October 3, 2025 13:12
…s namespace open for a 2D sweep (if needed) in the future. Also fixed overlaps logic to properly break early
Comment on lines 54 to 63
struct SweepLineInterval{T<:Number}
start::T
stop::T
segment::Any
index::Int
end

function overlaps(i₁::SweepLineInterval, i₂::SweepLineInterval)
i₁.start i₂.start && i₁.stop i₂.start
end
Copy link
Member

Choose a reason for hiding this comment

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

Do we really need this data structure?

The new algorithm seems very simple:

For each segment, do

  1. Compute start and save in a vector starts
  2. Compute stop and save in a vector stops

Sort vectors segs, starts and stops in terms of sortperm(starts)

For each pair of segments, do

  1. Fast check with starts and stops
  2. If check is true, call expensive intersection
  3. Otherwise, advance the loop

If we simply implement this algorithm with raw vectors segs, starts and stops, it will be probably be more readable and type stable.

Also, the function sweep1d can probably be merged into the main pairwiseintersect function after that. The only part of the code that deserves encapsulation is the kernel of the loop that uses starts, stops for a fast check and only then computes intersections with segs.

Copy link
Member

@juliohm juliohm Oct 4, 2025

Choose a reason for hiding this comment

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

Alternatively, if you feel that the readability with a single sort is superior, then use a simple vector of tuples preproc = [(start, stop, seg), ...] and sort(preproc, by=first).

When you introduce a data structure like SweepLineInterval and use its fields in a separate function (e.g., by=x->x.start), you are accessing internals.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We really don't. The main idea was to have everything tied to the same object, but this way works well. It's actually significantly faster because it is effectively a struct of arrays rather than array of structs. It's now the fastest test I've run thus far.

… to vectors of indices because sets are expensive to initialized
oldindices = collect(1:n)[sortedindices]

# primary sweepline algorithm
𝐺 = Dict{Point,Vector{Int}}()
Copy link
Member

Choose a reason for hiding this comment

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

This type is not concrete. Please use typeof if necessary. Is Dict the best data structure for this loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

doh, you're right, Point isn't concrete. Easy fix. that simple fix improved the speed 2x 😮

I argue that Dict is the best data structure largely because what we want is a set of unique points and a set of corresponding segment indices for each point. In the case of more than two lines intersecting at a point, the dictionary allows us to easily check if that point already exists, then we can just append new indices to the indices.

Vector isfeasible, but they are a bit messier to handle with little to no speed improvement. That's largely because if we want unique keys in a Vector, we still have to find which indices are duplicated (a large batch membership equality check) then append segment indices as needed.

Set wouldn't work because the order of the set may be different from the segment indices, thus losing which indices belong to which intersection point.

Now, a true "graph" would probably be optimal, but adds the complexity of implementing said system.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did a check on if we could derive a graph from this output easily, and yes we can. We can build adjacency and segment-points (reverse of current point-segments) graphs in 50% of the time as creating the line segment intersections (in a dense case).

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