Skip to content

Conversation

@SebastianDirks
Copy link
Contributor

This is a suggested performance improvement for C# when reusing Clipper objects.
I profiled Clipper2 perfomance in my application, which reuses ThreadLocal Clipper2 instances by repeatedly calling Clear() to reduce allocations and thus garbage collection pressure. Profiling showed that when reusing Clipper2, a large number of Vertex object is still allocated.
The proposed improvement significantly reduces these allocations by reusing previously allocated Vertex objects instead of trashing them and allocating new ones for every polygon boolean operation.

Here are the benchmarks, I added a switch for benchmarking to turn the behaviour on and off, which is not in this PR:
BenchmarkDotNet v0.15.1, Windows 11 (10.0.26100.6584/24H2/2024Update/HudsonValley)
AMD Ryzen 9 7950X3D 4.20GHz, 1 CPU, 32 logical and 16 physical cores
.NET SDK 9.0.304
[Host] : .NET 8.0.19 (8.0.1925.36514), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
Job-IJRPZB : .NET 8.0.19 (8.0.1925.36514), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI

InvocationCount=1 UnrollFactor=1

Method UseVertexPool Mean Error StdDev Completed Work Items Lock Contentions Allocated
InterlockSim False 223.7 ms 3.00 ms 2.51 ms - - 301.52 MB
InterlockSim True 209.9 ms 2.50 ms 2.09 ms - - 245.87 MB
Method UseVertexPool Mean Error StdDev Median Completed Work Items Lock Contentions Gen0 Gen1 Allocated
SingleThreadedOnce False 82.28 ms 3.715 ms 10.232 ms 79.23 ms 734.0000 1.0000 - - 1041.9 MB
SingleThreadedRepeated False 2,619.81 ms 52.217 ms 85.795 ms 2,628.33 ms 23993.0000 365.0000 18000.0000 5000.0000 33160.09 MB
MultiThreaded False 1,473.04 ms 9.818 ms 8.703 ms 1,470.41 ms 324.0000 10.0000 16000.0000 3000.0000 30331.92 MB
SingleThreadedOnce True 71.85 ms 1.431 ms 2.758 ms 71.68 ms 1043.0000 - - - 820.71 MB
SingleThreadedRepeated True 2,468.38 ms 48.061 ms 77.610 ms 2,458.17 ms 21571.0000 50.0000 13000.0000 2000.0000 25497.39 MB
MultiThreaded True 1,243.88 ms 23.446 ms 23.027 ms 1,234.85 ms 244.0000 2.0000 12000.0000 3000.0000 22741.84 MB

As seen, for my workloads (about 50% of CPU time in Clipper2 library) this reduces allocation volume by 18% - 25%. In multi threaded benchmarks this also significantly reduces the number of Gen0 and Gen1 GCs and the lock contentions they induce. This results in 6%- 16% faster execution.

To minimize the impact of this improvement to maintainability, I encapsulated the behaviour into a new internal class "VertexPoolList", which has most of its implementation like the standard C# list with reduced operations.

A List that pools objects added to it for reuse.
Indexing, growing and enumeration implementation is identical to System.Collections.Generic.List{T}.
The pooled list reuses allocated reference objects by not clearing the internal list array when Clear() is called, only reseting the count to 0. Operations are limited to read, add and clear.

In Clipper.Engine.cs we now only have to change the type of the vertex collection from List to VertexPoolList and change the 4 lines of code where new vertices are allocated to instead pass the properties to the VertexPoolList, which either reuses a Vertex or allocates a new one. When the VertexPoolList is empty (no clipper instance reuse), the behaviour is almost identical to before ( except for the inlined null check before allocating, which always gives true and thus should be an easy case for the CPU branch predictor).

I have tested this code in my own application for a few months (CI pipeline with ~200 unit tests), and have detected no errors with it until now. Any feedback is welcome, let me know what you think.

SebastianDirks and others added 10 commits December 18, 2023 13:02
…ets the internal lists capacity, even if the capacity already sufficient. This causes C# to drop the currently allocated array and create a new, smaller one.

This changes check if the capacity is sufficient before setting it to reduces allocations.
… and improves performance if a clipper instance or Reusable data container is reused. To minimize impact on main clipping module, the pooling is implemented in a separate class VertexPoolList : PooledList<Vertex>. This collection is a List that pools objects added to it for reuse. Indexing, growing and enumeration implementation is identical to "System.Collections.Generic.List{T}. The pooled list reuses allocated reference objects. Operations are limited to read, add and clear.
@AngusJohnson
Copy link
Owner

Hi Sebastian.
That looks great, thanks!!

@AngusJohnson AngusJohnson merged commit 9741103 into AngusJohnson:main Sep 18, 2025
7 checks passed
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