Skip to content

Commit 5e53144

Browse files
Merge pull request #16 from SixLabors/js/optimize-possible-intersection
Optimize PossibleIntersection and StablePriorityQueue
2 parents b6e4cb8 + 5ae35c0 commit 5e53144

File tree

9 files changed

+322
-172
lines changed

9 files changed

+322
-172
lines changed

src/PolygonClipper/PolygonClipper.cs

Lines changed: 55 additions & 31 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

@@ -120,7 +122,9 @@ public Polygon Run()
120122
// Process all segments in the subject polygon
121123
Vertex min = new(double.PositiveInfinity);
122124
Vertex max = new(double.NegativeInfinity);
123-
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue = new(new SweepEventComparer());
125+
126+
int eventCount = (subject.GetVertexCount() + clipping.GetVertexCount()) * 2;
127+
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue = new(new SweepEventComparer(), eventCount);
124128
int contourId = 0;
125129
for (int i = 0; i < subject.ContourCount; i++)
126130
{
@@ -162,6 +166,7 @@ public Polygon Run()
162166

163167
SweepEvent? prevEvent;
164168
SweepEvent? nextEvent;
169+
Span<SweepEvent> workspace = new SweepEvent[4];
165170
while (eventQueue.Count > 0)
166171
{
167172
SweepEvent sweepEvent = eventQueue.Dequeue();
@@ -188,7 +193,7 @@ public Polygon Run()
188193
if (nextEvent != null)
189194
{
190195
// Check intersection with the next neighbor
191-
if (PossibleIntersection(sweepEvent, nextEvent, eventQueue) == 2)
196+
if (PossibleIntersection(sweepEvent, nextEvent, eventQueue, workspace) == 2)
192197
{
193198
ComputeFields(sweepEvent, prevEvent, operation);
194199
ComputeFields(nextEvent, sweepEvent, operation);
@@ -199,7 +204,7 @@ public Polygon Run()
199204
if (prevEvent != null)
200205
{
201206
// Check intersection with the previous neighbor
202-
if (PossibleIntersection(prevEvent, sweepEvent, eventQueue) == 2)
207+
if (PossibleIntersection(prevEvent, sweepEvent, eventQueue, workspace) == 2)
203208
{
204209
SweepEvent? prevPrevEvent = statusLine.Prev(prevEvent.PosSL);
205210
ComputeFields(prevEvent, prevPrevEvent, operation);
@@ -218,7 +223,7 @@ public Polygon Run()
218223
// Check intersection between neighbors
219224
if (prevEvent != null && nextEvent != null)
220225
{
221-
_ = PossibleIntersection(prevEvent, nextEvent, eventQueue);
226+
_ = PossibleIntersection(prevEvent, nextEvent, eventQueue, workspace);
222227
}
223228

224229
statusLine.RemoveAt(it);
@@ -499,6 +504,10 @@ private static bool InResult(SweepEvent sweepEvent, BooleanOperation operation)
499504
/// <param name="le1">The first sweep event representing a line segment.</param>
500505
/// <param name="le2">The second sweep event representing a line segment.</param>
501506
/// <param name="eventQueue">The event queue to add new events to.</param>
507+
/// <param name="workspace">
508+
/// A scratch space for temporary storage of sweep events.
509+
/// Must be at least 4 elements long to hold the events for the two segments and their associated other events.
510+
/// </param>
502511
/// <returns>
503512
/// An integer indicating the result of the intersection:
504513
/// <list type="bullet">
@@ -514,7 +523,8 @@ private static bool InResult(SweepEvent sweepEvent, BooleanOperation operation)
514523
private static int PossibleIntersection(
515524
SweepEvent le1,
516525
SweepEvent le2,
517-
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue)
526+
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue,
527+
Span<SweepEvent> workspace)
518528
{
519529
if (le1.OtherEvent == null || le2.OtherEvent == null)
520530
{
@@ -569,41 +579,55 @@ private static int PossibleIntersection(
569579
}
570580

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

577-
// Populate the events
585+
// Populate the events.
586+
// The working buffer has a length of 4, which is sufficient to hold the events
587+
// for the two segments and their associated other events.
588+
// Events are assigned in a specific order to avoid overwriting shared references.
589+
ref SweepEvent wRef = ref MemoryMarshal.GetReference(workspace);
578590
if (!leftCoincide)
579591
{
580592
if (comparer.Compare(le1, le2) > 0)
581593
{
582-
events.Add(le2);
583-
events.Add(le1);
594+
Unsafe.Add(ref wRef, 0u) = le2;
595+
Unsafe.Add(ref wRef, 1u) = le1;
584596
}
585597
else
586598
{
587-
events.Add(le1);
588-
events.Add(le2);
599+
Unsafe.Add(ref wRef, 0u) = le1;
600+
Unsafe.Add(ref wRef, 1u) = le2;
589601
}
590-
}
591602

592-
if (!rightCoincide)
603+
// Positions 0 and 1 contain the left events of the segments.
604+
// Positions 2 and 3 will contain the right events of the segments.
605+
if (!rightCoincide)
606+
{
607+
Unsafe.Add(ref wRef, 2u) = le1.OtherEvent;
608+
Unsafe.Add(ref wRef, 3u) = le2.OtherEvent;
609+
}
610+
else
611+
{
612+
Unsafe.Add(ref wRef, 2u) = le2.OtherEvent;
613+
Unsafe.Add(ref wRef, 3u) = le1.OtherEvent;
614+
}
615+
}
616+
else if (leftCoincide && !rightCoincide)
593617
{
618+
// Only the right endpoints differ, so we use positions 0 and 1 for their sorted order.
594619
if (comparer.Compare(le1.OtherEvent, le2.OtherEvent) > 0)
595620
{
596-
events.Add(le2.OtherEvent);
597-
events.Add(le1.OtherEvent);
621+
Unsafe.Add(ref wRef, 0u) = le2.OtherEvent;
622+
Unsafe.Add(ref wRef, 1u) = le1.OtherEvent;
598623
}
599624
else
600625
{
601-
events.Add(le1.OtherEvent);
602-
events.Add(le2.OtherEvent);
626+
Unsafe.Add(ref wRef, 0u) = le1.OtherEvent;
627+
Unsafe.Add(ref wRef, 1u) = le2.OtherEvent;
603628
}
604629
}
605630

606-
// Handle leftCoincide case
607631
if (leftCoincide)
608632
{
609633
le2.EdgeType = EdgeType.NonContributing;
@@ -613,30 +637,31 @@ private static int PossibleIntersection(
613637

614638
if (leftCoincide && !rightCoincide)
615639
{
616-
DivideSegment(events[1].OtherEvent, events[0].Point, eventQueue, comparer);
640+
DivideSegment(Unsafe.Add(ref wRef, 1u).OtherEvent, Unsafe.Add(ref wRef, 0u).Point, eventQueue, comparer);
617641
}
618642

619643
return 2;
620644
}
621645

622-
// Handle the rightCoincide case
623646
if (rightCoincide)
624647
{
625-
DivideSegment(events[0], events[1].Point, eventQueue, comparer);
648+
// Since leftCoincide is false, the first two workspace slots contain distinct left events.
649+
DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer);
626650
return 3;
627651
}
628652

629653
// Handle general overlapping case
630-
if (events[0] != events[3].OtherEvent)
654+
// At this point: workspace[0,1] = sorted left events, workspace[2,3] = sorted right events.
655+
if (Unsafe.Add(ref wRef, 0u) != Unsafe.Add(ref wRef, 3u).OtherEvent)
631656
{
632-
DivideSegment(events[0], events[1].Point, eventQueue, comparer);
633-
DivideSegment(events[1], events[2].Point, eventQueue, comparer);
657+
DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer);
658+
DivideSegment(Unsafe.Add(ref wRef, 1u), Unsafe.Add(ref wRef, 2u).Point, eventQueue, comparer);
634659
return 3;
635660
}
636661

637662
// 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);
663+
DivideSegment(Unsafe.Add(ref wRef, 0u), Unsafe.Add(ref wRef, 1u).Point, eventQueue, comparer);
664+
DivideSegment(Unsafe.Add(ref wRef, 3u).OtherEvent, Unsafe.Add(ref wRef, 2u).Point, eventQueue, comparer);
640665
return 3;
641666
}
642667

@@ -924,6 +949,7 @@ private static ReadOnlySpan<int> PrecomputeIterationOrder(List<SweepEvent> data)
924949
return map;
925950
}
926951

952+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
927953
private static void MarkProcessed(SweepEvent sweepEvent, Span<bool> processed, int pos, int contourId)
928954
{
929955
processed[pos] = true;
@@ -994,11 +1020,9 @@ private static Contour InitializeContourFromContext(SweepEvent sweepEvent, Polyg
9941020
/// starting from the given position.
9951021
/// </summary>
9961022
/// <param name="pos">The current position in the result events.</param>
997-
/// <param name="resultEvents">The list of sweep events representing result segments.</param>
9981023
/// <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>
1024+
/// <param name="iterationMap">A precomputed map that indicates the next position to check for unprocessed events.</param>
1025+
/// <param name="found">A boolean indicating whether an unprocessed event was found.</param>
10021026
/// <returns>The index of the next unprocessed position.</returns>
10031027
/// <remarks>
10041028
/// This method searches forward from the current position until it finds an unprocessed event with

src/PolygonClipper/StablePriorityQueue{T,TComparer}.cs

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

44
using System;
55
using System.Collections.Generic;
66
using System.Diagnostics;
7+
using System.Runtime.CompilerServices;
8+
using System.Runtime.InteropServices;
79

810
namespace PolygonClipper;
911

@@ -16,14 +18,18 @@ namespace PolygonClipper;
1618
internal sealed class StablePriorityQueue<T, TComparer>
1719
where TComparer : IComparer<T>
1820
{
19-
private readonly List<T> heap = [];
21+
private readonly List<T> heap;
2022

2123
/// <summary>
2224
/// Initializes a new instance of the <see cref="StablePriorityQueue{T, TComparer}"/> class with a specified comparer.
2325
/// </summary>
2426
/// <param name="comparer">The comparer to determine the priority of the elements.</param>
25-
public StablePriorityQueue(TComparer comparer)
26-
=> this.Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
27+
/// <param name="capacity">The initial capacity of the priority queue.</param>
28+
public StablePriorityQueue(TComparer comparer, int capacity)
29+
{
30+
this.Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
31+
this.heap = new List<T>(capacity > 0 ? capacity : 16);
32+
}
2733

2834
/// <summary>
2935
/// Gets the number of elements in the priority queue.
@@ -42,7 +48,7 @@ public StablePriorityQueue(TComparer comparer)
4248
public void Enqueue(T item)
4349
{
4450
this.heap.Add(item);
45-
this.Up(this.heap.Count - 1);
51+
this.Up((uint)this.heap.Count - 1);
4652
}
4753

4854
/// <summary>
@@ -86,61 +92,66 @@ public T Peek()
8692
}
8793

8894
/// <summary>
89-
/// Restores the heap property by moving the item at the specified index upward.
95+
/// Restores the min-heap property by moving the item at the specified index upward
96+
/// through the heap until it is in the correct position. This is called after insertion.
9097
/// </summary>
91-
/// <param name="index">The index of the item to move upward.</param>
92-
private void Up(int index)
98+
/// <param name="index">The index of the newly added item to sift upward.</param>
99+
private void Up(uint index)
93100
{
94-
List<T> data = this.heap;
95-
T item = data[index];
101+
ref T dRef = ref MemoryMarshal.GetReference(CollectionsMarshal.AsSpan(this.heap));
102+
T item = Unsafe.Add(ref dRef, index);
96103
TComparer comparer = this.Comparer;
97104

98105
while (index > 0)
99106
{
100-
int parent = (index - 1) >> 1;
101-
T current = data[parent];
107+
uint parent = (index - 1u) >> 1;
108+
T current = Unsafe.Add(ref dRef, parent);
102109
if (comparer.Compare(item, current) >= 0)
103110
{
104111
break;
105112
}
106113

107-
data[index] = current;
114+
Unsafe.Add(ref dRef, index) = current;
108115
index = parent;
109116
}
110117

111-
data[index] = item;
118+
Unsafe.Add(ref dRef, index) = item;
112119
}
113120

114121
/// <summary>
115-
/// Restores the heap property by moving the item at the specified index downward.
122+
/// Restores the min-heap property by moving the item at the specified index downward
123+
/// through the heap until it is in the correct position. This is called after removal of the root.
116124
/// </summary>
117-
/// <param name="index">The index of the item to move downward.</param>
118-
private void Down(int index)
125+
/// <param name="index">The index of the item to sift downward (typically the root).</param>
126+
private void Down(uint index)
119127
{
120-
List<T> data = this.heap;
121-
int halfLength = data.Count >> 1;
122-
T item = data[index];
128+
Span<T> data = CollectionsMarshal.AsSpan(this.heap);
129+
ref T dRef = ref MemoryMarshal.GetReference(data);
130+
131+
uint length = (uint)data.Length;
132+
uint halfLength = length >> 1;
133+
T item = Unsafe.Add(ref dRef, index);
123134
TComparer comparer = this.Comparer;
124135

125136
while (index < halfLength)
126137
{
127-
int bestChild = (index << 1) + 1; // Initially left child
128-
int right = bestChild + 1;
138+
uint bestChild = (index << 1) + 1; // Initially left child
139+
uint right = bestChild + 1u;
129140

130-
if (right < data.Count && comparer.Compare(data[right], data[bestChild]) < 0)
141+
if (right < length && comparer.Compare(Unsafe.Add(ref dRef, right), Unsafe.Add(ref dRef, bestChild)) < 0)
131142
{
132143
bestChild = right;
133144
}
134145

135-
if (comparer.Compare(data[bestChild], item) >= 0)
146+
if (comparer.Compare(Unsafe.Add(ref dRef, bestChild), item) >= 0)
136147
{
137148
break;
138149
}
139150

140-
data[index] = data[bestChild];
151+
Unsafe.Add(ref dRef, index) = Unsafe.Add(ref dRef, bestChild);
141152
index = bestChild;
142153
}
143154

144-
data[index] = item;
155+
Unsafe.Add(ref dRef, index) = item;
145156
}
146157
}

0 commit comments

Comments
 (0)