Repository navigation
Pool OutPt/OutRec/Active allocations and avoid repeated laps in CleanCollinear - #1117
Open
xaviergonz wants to merge 3 commits into
Open
xaviergonz wants to merge 3 commits into
xaviergonz wants to merge 3 commits into
Conversation
Every OutPt and Active was a separate heap allocation that was deleted individually, and malloc/free ended up being a big chunk of the time spent in offsets (lots of short lived OutPts) and open path clipping. Now they're allocated from chunks owned by ClipperBase and all released together in CleanUp, so deleting an OutPt is just unlinking it, and deleted Actives go on a free list and get reused. This mirrors what the C# port already does (OutPtPoolList, NewActive/PoolDeletedActive), so only C++ and Delphi are changed. Output is unchanged.
xaviergonz
force-pushed
the
perf/outpt-active-arena
branch
from
October 15, 2026 18:15
9e6cb12 to
dd30e75
Compare
OutRecs are created for every local minima, so with lots of short output paths (open path offsets especially) allocating and deleting them was still a noticeable cost. They're now allocated from an ObjectPool like OutPts, destroyed in DisposeAllOutRecs and freed together. C# already pools these (OutRecPoolList). In Delphi, FPC's allocator is already fast for records this size and pooling them made no difference.
Each time CleanCollinear removed a vertex, it restarted its "full lap" end condition, so on paths with lots of collinear vertices it ended up doing about two full laps. But removing a vertex can only make its two neighbours removable, and the next one is checked straight away. So after the first lap, only the vertices before removed ones are rechecked, in the same order that further full laps would reach them. The result is exactly the same.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hi Angus,
This is another one from profiling Clipper2 for a WebAssembly build. It's independent of #1115 and #1116, but it builds on the same work. After those two, offsets (open paths especially) were spending most of their remaining time on the allocator, plus one avoidable extra lap in
CleanCollinear. There are three commits, which can be reviewed separately.1. Allocate
OutPtandActiveobjects from a per-clipper pool (C++, Delphi)Every
OutPtand everyActivewas its own heap allocation and got deleted one at a time. In an open-path offset, about 30% of the time was going to malloc/free.The C# port already avoids this. It has
OutPtPoolListforOutPts, andNewActive/PoolDeletedActivewith a free list forActives. This commit does the same in C++ and Delphi, using the same names where it makes sense:OutPts come from a pool owned byClipperBase(outpt_pool_/FOutPtPool). They're all released together inCleanUp, so disposing of one during the sweep is just unlinking it.Actives come fromNewActive().DeleteFromAELhands them toPoolDeletedActive(), which clears them and puts them on a free list (free_actives_/FFreeActives) for reuse. As in C#, that's a separate list, so the "already deleted" check inDeleteFromAELstill works.ObjectPool<T>inclipper.engine.handTObjectPoolin Delphi. Chunks grow from 32 to 4096 objects. I didn't usestd::dequebecause MSVC's deque allocates objects this size one at a time, so it would gain nothing on Windows.Nothing is global or shared between instances, and nothing is kept once
Executefinishes.2. Allocate
OutRecs from a pool too (C++ only)There's an
OutRecfor every local minima, so with lots of short output paths these allocations were still noticeable. They now come from anObjectPoolas well.DisposeAllOutRecsdestroys each one (they all live inoutrec_list_) before the pool is released. C# already pools these (OutRecPoolList). I tried the same in Delphi, but FPC's allocator is already fast for records this size and it made no measurable difference, so I left Delphi as it was.3. Recheck only the affected vertices in
CleanCollinear(all ports)Each time
CleanCollinearremoved a vertex, it restarted its "full lap" end condition. On paths with lots of collinear vertices it ended up doing about two full laps; an open-path offset of a 5000-point circle walks around 33k vertices to remove 7.7k. But removing a vertex can only make its two neighbours removable, and the next one is checked straight away. So after the first lap, only the vertices before removed ones are rechecked, in the same order that further full laps would reach them. The result is exactly the same.Numbers
These are on an M1 Max. Times are ms per operation, best of 5. The C++ numbers were measured with #1115 and #1116 already applied, and the Delphi numbers (FPC 3.2.2) against plain
main.In C++, about 25% of the open-path improvement comes from commit 1, 10% from commit 2 and 3% from commit 3. Together with #1115 and #1116, C++ offsets are now about as fast as Clipper1's (the open-path square case is 0.98× of Clipper1, and the polygon cases are faster). In C#, commit 3 is within noise, since C# spends its time elsewhere.
The trade-off is that disposed
OutPts aren't returned to the heap until the clip finishes. That's the same as C#'sOutPtPoolList, and they're a small fraction of the total.Testing
maingives identical output in C++, C# and Delphi, including after each commit on its own. It runs 20k random cases with all clip types, fill rules,PreserveCollinearon and off, open paths, PolyTree output and offsets. To check that it actually exercises theCleanCollinearchange, I also tried a version that never rechecks, and the fuzz caught it.