Skip to content

Commit b3ab612

Browse files
Use unsafe and remove allocation in PossibleIntersection
1 parent b6e4cb8 commit b3ab612

File tree

8 files changed

+281
-145
lines changed

8 files changed

+281
-145
lines changed

src/PolygonClipper/PolygonClipper.cs

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using System.Collections.Generic;
66
using System.Diagnostics;
77
using System.Diagnostics.CodeAnalysis;
8+
using System.Runtime.CompilerServices;
9+
using System.Runtime.InteropServices;
810

911
namespace PolygonClipper;
1012

@@ -162,6 +164,7 @@ public Polygon Run()
162164

163165
SweepEvent? prevEvent;
164166
SweepEvent? nextEvent;
167+
Span<SweepEvent> workspace = new SweepEvent[4];
165168
while (eventQueue.Count > 0)
166169
{
167170
SweepEvent sweepEvent = eventQueue.Dequeue();
@@ -188,7 +191,7 @@ public Polygon Run()
188191
if (nextEvent != null)
189192
{
190193
// Check intersection with the next neighbor
191-
if (PossibleIntersection(sweepEvent, nextEvent, eventQueue) == 2)
194+
if (PossibleIntersection(sweepEvent, nextEvent, eventQueue, workspace) == 2)
192195
{
193196
ComputeFields(sweepEvent, prevEvent, operation);
194197
ComputeFields(nextEvent, sweepEvent, operation);
@@ -199,7 +202,7 @@ public Polygon Run()
199202
if (prevEvent != null)
200203
{
201204
// Check intersection with the previous neighbor
202-
if (PossibleIntersection(prevEvent, sweepEvent, eventQueue) == 2)
205+
if (PossibleIntersection(prevEvent, sweepEvent, eventQueue, workspace) == 2)
203206
{
204207
SweepEvent? prevPrevEvent = statusLine.Prev(prevEvent.PosSL);
205208
ComputeFields(prevEvent, prevPrevEvent, operation);
@@ -218,7 +221,7 @@ public Polygon Run()
218221
// Check intersection between neighbors
219222
if (prevEvent != null && nextEvent != null)
220223
{
221-
_ = PossibleIntersection(prevEvent, nextEvent, eventQueue);
224+
_ = PossibleIntersection(prevEvent, nextEvent, eventQueue, workspace);
222225
}
223226

224227
statusLine.RemoveAt(it);
@@ -499,6 +502,10 @@ private static bool InResult(SweepEvent sweepEvent, BooleanOperation operation)
499502
/// <param name="le1">The first sweep event representing a line segment.</param>
500503
/// <param name="le2">The second sweep event representing a line segment.</param>
501504
/// <param name="eventQueue">The event queue to add new events to.</param>
505+
/// <param name="workspace">
506+
/// A scratch space for temporary storage of sweep events.
507+
/// Must be at least 4 elements long to hold the events for the two segments and their associated other events.
508+
/// </param>
502509
/// <returns>
503510
/// An integer indicating the result of the intersection:
504511
/// <list type="bullet">
@@ -514,7 +521,8 @@ private static bool InResult(SweepEvent sweepEvent, BooleanOperation operation)
514521
private static int PossibleIntersection(
515522
SweepEvent le1,
516523
SweepEvent le2,
517-
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue)
524+
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue,
525+
Span<SweepEvent> workspace)
518526
{
519527
if (le1.OtherEvent == null || le2.OtherEvent == null)
520528
{
@@ -569,41 +577,55 @@ private static int PossibleIntersection(
569577
}
570578

571579
// The line segments associated with le1 and le2 overlap.
572-
// TODO: Rewrite this to avoid allocation.
573-
List<SweepEvent> events = new(4);
574580
bool leftCoincide = le1.Point == le2.Point;
575581
bool rightCoincide = le1.OtherEvent.Point == le2.OtherEvent.Point;
576582

577-
// Populate the events
583+
// Populate the events.
584+
// The working buffer has a length of 4, which is sufficient to hold the events
585+
// for the two segments and their associated other events.
586+
// Events are assigned in a specific order to avoid overwriting shared references.
587+
ref SweepEvent wRef = ref MemoryMarshal.GetReference(workspace);
578588
if (!leftCoincide)
579589
{
580590
if (comparer.Compare(le1, le2) > 0)
581591
{
582-
events.Add(le2);
583-
events.Add(le1);
592+
Unsafe.Add(ref wRef, 0u) = le2;
593+
Unsafe.Add(ref wRef, 1u) = le1;
584594
}
585595
else
586596
{
587-
events.Add(le1);
588-
events.Add(le2);
597+
Unsafe.Add(ref wRef, 0u) = le1;
598+
Unsafe.Add(ref wRef, 1u) = le2;
589599
}
590-
}
591600

592-
if (!rightCoincide)
601+
// Positions 0 and 1 contain the left events of the segments.
602+
// Positions 2 and 3 will contain the right events of the segments.
603+
if (!rightCoincide)
604+
{
605+
Unsafe.Add(ref wRef, 2u) = le1.OtherEvent;
606+
Unsafe.Add(ref wRef, 3u) = le2.OtherEvent;
607+
}
608+
else
609+
{
610+
Unsafe.Add(ref wRef, 2u) = le2.OtherEvent;
611+
Unsafe.Add(ref wRef, 3u) = le1.OtherEvent;
612+
}
613+
}
614+
else if (leftCoincide && !rightCoincide)
593615
{
616+
// Only the right endpoints differ, so we use positions 0 and 1 for their sorted order.
594617
if (comparer.Compare(le1.OtherEvent, le2.OtherEvent) > 0)
595618
{
596-
events.Add(le2.OtherEvent);
597-
events.Add(le1.OtherEvent);
619+
Unsafe.Add(ref wRef, 0u) = le2.OtherEvent;
620+
Unsafe.Add(ref wRef, 1u) = le1.OtherEvent;
598621
}
599622
else
600623
{
601-
events.Add(le1.OtherEvent);
602-
events.Add(le2.OtherEvent);
624+
Unsafe.Add(ref wRef, 0u) = le1.OtherEvent;
625+
Unsafe.Add(ref wRef, 1u) = le2.OtherEvent;
603626
}
604627
}
605628

606-
// Handle leftCoincide case
607629
if (leftCoincide)
608630
{
609631
le2.EdgeType = EdgeType.NonContributing;
@@ -613,30 +635,31 @@ private static int PossibleIntersection(
613635

614636
if (leftCoincide && !rightCoincide)
615637
{
616-
DivideSegment(events[1].OtherEvent, events[0].Point, eventQueue, comparer);
638+
DivideSegment(Unsafe.Add(ref wRef, 1u).OtherEvent, Unsafe.Add(ref wRef, 0u).Point, eventQueue, comparer);
617639
}
618640

619641
return 2;
620642
}
621643

622-
// Handle the rightCoincide case
623644
if (rightCoincide)
624645
{
625-
DivideSegment(events[0], events[1].Point, eventQueue, comparer);
646+
// Since leftCoincide is false, the first two workspace slots contain distinct left events.
647+
DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer);
626648
return 3;
627649
}
628650

629651
// Handle general overlapping case
630-
if (events[0] != events[3].OtherEvent)
652+
// At this point: workspace[0,1] = sorted left events, workspace[2,3] = sorted right events.
653+
if (Unsafe.Add(ref wRef, 0u) != Unsafe.Add(ref wRef, 3u).OtherEvent)
631654
{
632-
DivideSegment(events[0], events[1].Point, eventQueue, comparer);
633-
DivideSegment(events[1], events[2].Point, eventQueue, comparer);
655+
DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer);
656+
DivideSegment(Unsafe.Add(ref wRef, 1u), Unsafe.Add(ref wRef, 2u).Point, eventQueue, comparer);
634657
return 3;
635658
}
636659

637660
// One segment fully contains the other
638-
DivideSegment(events[0], events[1].Point, eventQueue, comparer);
639-
DivideSegment(events[3].OtherEvent, events[2].Point, eventQueue, comparer);
661+
DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer);
662+
DivideSegment(Unsafe.Add(ref wRef, 3u).OtherEvent, Unsafe.Add(ref wRef, 2u).Point, eventQueue, comparer);
640663
return 3;
641664
}
642665

@@ -924,6 +947,7 @@ private static ReadOnlySpan<int> PrecomputeIterationOrder(List<SweepEvent> data)
924947
return map;
925948
}
926949

950+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
927951
private static void MarkProcessed(SweepEvent sweepEvent, Span<bool> processed, int pos, int contourId)
928952
{
929953
processed[pos] = true;
@@ -994,11 +1018,9 @@ private static Contour InitializeContourFromContext(SweepEvent sweepEvent, Polyg
9941018
/// starting from the given position.
9951019
/// </summary>
9961020
/// <param name="pos">The current position in the result events.</param>
997-
/// <param name="resultEvents">The list of sweep events representing result segments.</param>
9981021
/// <param name="processed">A list indicating whether each event at the corresponding index has been processed.</param>
999-
/// <param name="originalPos">The original position to return if no unprocessed event is found.</param>
1000-
/// <param name="iterationMap"></param>
1001-
/// <param name="found"></param>
1022+
/// <param name="iterationMap">A precomputed map that indicates the next position to check for unprocessed events.</param>
1023+
/// <param name="found">A boolean indicating whether an unprocessed event was found.</param>
10021024
/// <returns>The index of the next unprocessed position.</returns>
10031025
/// <remarks>
10041026
/// This method searches forward from the current position until it finds an unprocessed event with

tests/PolygonClipper.Benchmarks/ClippingLibraryComparison.cs

Lines changed: 16 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,23 @@
66
using BenchmarkDotNet.Attributes;
77
using Clipper2Lib;
88
using GeoJSON.Text.Feature;
9-
using GeoJSON.Text.Geometry;
9+
using PolygonClipper.Tests;
1010
using PolygonClipper.Tests.TestCases;
1111

12-
using GeoPolygon = GeoJSON.Text.Geometry.Polygon;
13-
1412
namespace PolygonClipper.Benchmarks;
1513

14+
/// <summary>
15+
/// <para>
16+
/// Benchmarks the performance of <c>PolygonClipper</c> against <c>Clipper2</c>
17+
/// for polygon union operations using a fixed input dataset.
18+
/// </para>
19+
/// <para>
20+
/// Note: Clipper2 produces an incorrect result for the test case used in this benchmark.
21+
/// As such, the Clipper2 timing is included for reference only and should not be considered
22+
/// a valid correctness comparison. This benchmark is intended to evaluate the performance
23+
/// of <c>PolygonClipper</c> in isolation.
24+
/// </para>
25+
/// </summary>
1626
public class ClippingLibraryComparison
1727
{
1828
private static readonly FeatureCollection Data = TestData.Generic.GetFeatureCollection("issue71.geojson");
@@ -25,17 +35,17 @@ public class ClippingLibraryComparison
2535
[GlobalSetup]
2636
public void Setup()
2737
{
28-
(Polygon subject, Polygon clipping) = BuildPolygon();
38+
(Polygon subject, Polygon clipping) = TestPolygonUtilities.BuildPolygon(Data);
2939
this.subject = subject;
3040
this.clipping = clipping;
3141

32-
(PathsD subject2, PathsD clipping2) = BuildPolygon2();
42+
(PathsD subject2, PathsD clipping2) = TestPolygonUtilities.BuildClipper2Polygon(Data);
3343
this.subject2 = subject2;
3444
this.clipping2 = clipping2;
3545
}
3646

3747
[Benchmark]
38-
public Polygon Clipper() => PolygonClipper.Union(this.subject, this.clipping);
48+
public Polygon PolygonClipper() => global::PolygonClipper.PolygonClipper.Union(this.subject, this.clipping);
3949

4050
[Benchmark(Baseline = true)]
4151
public PathsD Clipper2()
@@ -47,108 +57,4 @@ public PathsD Clipper2()
4757
clipper2.Execute(ClipType.Union, FillRule.Positive, solution);
4858
return solution;
4959
}
50-
51-
public static (Polygon Subject, Polygon Clipping) BuildPolygon()
52-
{
53-
IGeometryObject subjectGeometry = Data.Features[0].Geometry;
54-
IGeometryObject clippingGeometry = Data.Features[1].Geometry;
55-
56-
Polygon subject = ConvertToPolygon(subjectGeometry);
57-
Polygon clipping = ConvertToPolygon(clippingGeometry);
58-
59-
return (subject, clipping);
60-
}
61-
62-
public static (PathsD Subject, PathsD Clipping) BuildPolygon2()
63-
{
64-
IGeometryObject subjectGeometry = Data.Features[0].Geometry;
65-
IGeometryObject clippingGeometry = Data.Features[1].Geometry;
66-
67-
PathsD subject = ConvertToPolygon2(subjectGeometry);
68-
PathsD clipping = ConvertToPolygon2(clippingGeometry);
69-
70-
return (subject, clipping);
71-
}
72-
73-
private static Polygon ConvertToPolygon(IGeometryObject geometry)
74-
{
75-
if (geometry is GeoPolygon geoJsonPolygon)
76-
{
77-
// Convert GeoJSON Polygon to our Polygon type
78-
Polygon polygon = new();
79-
foreach (LineString ring in geoJsonPolygon.Coordinates)
80-
{
81-
Contour contour = new();
82-
foreach (IPosition xy in ring.Coordinates)
83-
{
84-
contour.AddVertex(new Vertex(xy.Longitude, xy.Latitude));
85-
}
86-
polygon.Push(contour);
87-
}
88-
89-
return polygon;
90-
}
91-
else if (geometry is MultiPolygon geoJsonMultiPolygon)
92-
{
93-
// Convert GeoJSON MultiPolygon to our Polygon type
94-
Polygon polygon = new();
95-
foreach (GeoPolygon geoPolygon in geoJsonMultiPolygon.Coordinates)
96-
{
97-
foreach (LineString ring in geoPolygon.Coordinates)
98-
{
99-
Contour contour = new();
100-
foreach (IPosition xy in ring.Coordinates)
101-
{
102-
contour.AddVertex(new Vertex(xy.Longitude, xy.Latitude));
103-
}
104-
polygon.Push(contour);
105-
}
106-
}
107-
108-
return polygon;
109-
}
110-
111-
throw new InvalidOperationException("Unsupported geometry type.");
112-
}
113-
114-
private static PathsD ConvertToPolygon2(IGeometryObject geometry)
115-
{
116-
if (geometry is GeoPolygon geoJsonPolygon)
117-
{
118-
// Convert GeoJSON Polygon to our Polygon type
119-
PathsD polygon = [];
120-
foreach (LineString ring in geoJsonPolygon.Coordinates)
121-
{
122-
PathD contour = [];
123-
foreach (IPosition xy in ring.Coordinates)
124-
{
125-
contour.Add(new PointD(xy.Longitude, xy.Latitude));
126-
}
127-
polygon.Add(contour);
128-
}
129-
130-
return polygon;
131-
}
132-
else if (geometry is MultiPolygon geoJsonMultiPolygon)
133-
{
134-
// Convert GeoJSON MultiPolygon to our Polygon type
135-
PathsD polygon = [];
136-
foreach (GeoPolygon geoPolygon in geoJsonMultiPolygon.Coordinates)
137-
{
138-
foreach (LineString ring in geoPolygon.Coordinates)
139-
{
140-
PathD contour = [];
141-
foreach (IPosition xy in ring.Coordinates)
142-
{
143-
contour.Add(new PointD(xy.Longitude, xy.Latitude));
144-
}
145-
polygon.Add(contour);
146-
}
147-
}
148-
149-
return polygon;
150-
}
151-
152-
throw new InvalidOperationException("Unsupported geometry type.");
153-
}
15460
}

tests/PolygonClipper.Benchmarks/PolygonClipper.Benchmarks.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@
3131
<ItemGroup>
3232
<Compile Include="..\PolygonClipper.Tests\TestData.cs" Link="TestData.cs" />
3333
<Compile Include="..\PolygonClipper.Tests\TestEnvironment.cs" Link="TestEnvironment.cs" />
34+
<Compile Include="..\PolygonClipper.Tests\TestPolygonUtilities.cs" Link="TestPolygonUtilities.cs" />
3435
</ItemGroup>
3536

3637
<ItemGroup>
3738
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
38-
<PackageReference Include="Clipper2" Version="1.4.0" />
39+
<PackageReference Include="Clipper2" Version="1.5.4" />
3940
</ItemGroup>
4041

4142
<ItemGroup>

tests/PolygonClipper.Tests/GenericTestCases.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public void GenericTestCase(string testCaseFile)
5454
Polygon clipping = ConvertToPolygon(clippingGeometry);
5555

5656
#pragma warning disable RCS1124 // Inline local variable
57-
List<ExpectedResult> expectedResults = ExtractExpectedResults(data.Features.Skip(2).ToList(), data.Type);
57+
List<ExpectedResult> expectedResults = ExtractExpectedResults([.. data.Features.Skip(2)], data.Type);
5858
#pragma warning restore RCS1124 // Inline local variable
5959

6060
// ExpectedResult result = expectedResults[1];
@@ -157,7 +157,8 @@ private static List<ExpectedResult> ExtractExpectedResults(List<Feature> feature
157157

158158
return new ExpectedResult
159159
{
160-
Operation = operation, Coordinates = ConvertToPolygon(feature.Geometry as MultiPolygon)
160+
Operation = operation,
161+
Coordinates = ConvertToPolygon(feature.Geometry as MultiPolygon)
161162
};
162163
});
163164

tests/PolygonClipper.Tests/PolygonClipper.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<PackageReference Include="xunit" Version="2.4.0" />
2424
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
2525
<PackageReference Include="coverlet.collector" Version="1.2.0" />
26+
<PackageReference Include="Clipper2" Version="1.5.4" />
2627
</ItemGroup>
2728

2829
<ItemGroup>

0 commit comments

Comments
 (0)