Skip to content
This repository was archived by the owner on Oct 28, 2026. It is now read-only.

Skip the intersection merge sort when no edges cross in a scanbeam - #1116

Open
xaviergonz wants to merge 1 commit into
AngusJohnson:mainfrom
xaviergonz:perf/sorted-ael-fast-path
Open

xaviergonz wants to merge 1 commit into
AngusJohnson:mainfrom
xaviergonz:perf/sorted-ael-fast-path

Conversation

@xaviergonz

@xaviergonz xaviergonz commented Oct 14, 2026 •

Copy link
Copy Markdown

Hi Angus,

This one came out of the same profiling session as #1115, but it's the bigger win.

In practice most scanbeams have no intersections at all, but BuildIntersectList still runs the full bottom-up merge sort over the active edges every time. Being bottom-up, it does about log2(n) complete passes over the list even when the list is already sorted. With a few hundred to a thousand active edges, that's around 10 passes per scanbeam just to find out nothing crossed. (Clipper1's bubble sort happened to bail out after a single pass in that case.)

So the change is small:

  • AdjustCurrXAndCopyToSEL already walks every edge to compute curr_x. It now also checks whether the edges are still in order, and returns that.
  • If they are, BuildIntersectList returns false straight away. The merge sort only records an intersection (or moves an edge) when right->curr_x < left->curr_x, so on a sorted list it would do nothing anyway.
  • In that same case every edge's curr_x is already TopX(top_y), since nothing modified it afterwards. So DoTopOfScanbeam skips recomputing it for edges that aren't at their top. When there are intersections everything runs exactly as before.

Done the same way in C++, C# and Delphi.

Numbers

These are unions (NonZero) of overlapping noisy circular blobs ("64×256" = 64 blobs of 256 vertices), on an M1 Max. Times are ms per operation, best of 5.

C++ before → after C# before → after Delphi (FPC 3.2.2) before → after
8×256 0.179 → 0.116 (1.54×) 0.576 → 0.433 (1.33×) 0.295 → 0.203 (1.45×)
32×64 0.365 → 0.258 (1.41×) 1.095 → 0.766 (1.43×) 0.621 → 0.472 (1.32×)
64×256 3.42 → 2.20 (1.56×) 11.28 → 7.29 (1.55×) 6.08 → 4.11 (1.48×)
128×256, separated 6.22 → 4.44 (1.40×) 23.89 → 17.56 (1.36×) 10.28 → 7.36 (1.40×)
256×256 27.26 → 14.52 (1.88×) 84.07 → 48.52 (1.73×) 47.50 → 28.50 (1.67×)
64×1024 13.04 → 8.09 (1.61×) 45.78 → 28.99 (1.58×) 23.00 → 15.50 (1.48×)

I also tried it in a WebAssembly build (emscripten), where it's roughly 1.5–2.1× faster. That's actually what got me looking. Without it, Clipper2 compiled to wasm was a bit slower than Clipper1, even though it's faster natively.

Testing

  • The C++ test suite passes (96/96), and so do the C# Tests1 and TestsZ projects.

  • I also ran a differential fuzz test against main in all three ports. It does 200k random cases with tiny coordinate ranges, to force collinear points, horizontals and duplicates. The cases cover:

    • all clip types and fill rules
    • closed and open subjects
    • polytree and paths output
    • PreserveCollinear and ReverseSolution
    • offsets with every join and end type

    It hashes every output, and the hashes are identical to main's in every port. As a sanity check, deliberately breaking the DoTopOfScanbeam part does change the hash, so the test is actually sensitive to this code.

It doesn't overlap with #1115 (different functions), so they can go in in either order.

Thanks for Clipper2, by the way. Happy to adjust anything.

Copilot AI balanced review requested due to automatic review settings October 14, 2026 22:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

BuildIntersectList's bottom-up merge sort always makes about log2(n)
passes over the active edge list, even when no edges cross in the
scanbeam, which is the usual case. AdjustCurrXAndCopyToSEL now also
checks whether the edges are still in order, and if so there's nothing
to find, so the merge sort is skipped. In that case every edge's curr_x
is already TopX(top_y), so DoTopOfScanbeam doesn't recompute it either.

Output is unchanged. Done in all three ports.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants