Skip to content

Optimize StablePriorityQueue#13

Closed
stefannikolei wants to merge 1 commit intoSixLabors:mainfrom
stefannikolei:sn/perf/stablepriorityqueue
Closed

Optimize StablePriorityQueue#13
stefannikolei wants to merge 1 commit intoSixLabors:mainfrom
stefannikolei:sn/perf/stablepriorityqueue

Conversation

@stefannikolei
Copy link
Copy Markdown
Contributor

  • Added an initial Capacity which is estimated through the amount of segments
  • Used unsafe in The PriorityQueue to get a speed increase

* Added an initial Capacity which is estimated through the amount of segments
* Used unsafe in The PriorityQueue to get a speed increase
Copilot AI review requested due to automatic review settings July 6, 2025 13:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Optimizes the StablePriorityQueue by switching from a List to an array-backed heap with unsafe inlined operations and introduces an initial capacity estimate in PolygonClipper.

  • Replaced List with a T[] buffer, manual count tracking, and unsafe MemoryMarshal/Unsafe.Add for performance.
  • Added a constructor overload to specify initial capacity and implemented an automatic Resize method.
  • Updated PolygonClipper to estimate and pass an initial capacity to the priority queue based on polygon segment counts.

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/PolygonClipper/StablePriorityQueue{T,TComparer}.cs Swapped out List<T> for a raw array, added capacity-based ctor, inlined Enqueue, Dequeue, Up, Down, and Resize.
src/PolygonClipper/PolygonClipper.cs Replaced the default queue instantiation with one that takes an estimatedEventCount computed from segment counts.
Comments suppressed due to low confidence (2)

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

  • The Peek method was removed, which breaks existing API by eliminating non-destructive access to the top element. Consider restoring Peek or providing equivalent functionality.
        else

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

  • Add tests to verify that the initial capacity is correctly applied and that the queue resizes properly when the number of enqueued items exceeds the provided capacity.
    public StablePriorityQueue(TComparer comparer, int capacity = 4)

Vertex max = new(double.NegativeInfinity);
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue = new(new SweepEventComparer());

int subjectSegments = subject.ContourCount > 0 ? subject[0].VertexCount - 1 : 0;
Copy link

Copilot AI Jul 6, 2025

Choose a reason for hiding this comment

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

The estimated segment count only accounts for the first contour via subject[0]. If multiple contours exist, this underestimates required capacity. Consider summing segments across all contours instead.

Suggested change
int subjectSegments = subject.ContourCount > 0 ? subject[0].VertexCount - 1 : 0;
int subjectSegments = 0;
for (int i = 0; i < subject.ContourCount; i++)
{
subjectSegments += subject[i].VertexCount - 1;
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The bot has a valid point here.

Copy link
Copy Markdown
Member

@JimBobSquarePants JimBobSquarePants left a comment

Choose a reason for hiding this comment

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

I'm not sure about this PR. I think we need to do some work here for correctness and safety along with performance.

Vertex max = new(double.NegativeInfinity);
StablePriorityQueue<SweepEvent, SweepEventComparer> eventQueue = new(new SweepEventComparer());

int subjectSegments = subject.ContourCount > 0 ? subject[0].VertexCount - 1 : 0;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The bot has a valid point here.

if (this.heap.Count == 0)
if (this.count == 0)
{
throw new InvalidOperationException("Queue is empty.");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be in a non-inlining throwhelper

heapRef = Unsafe.Add(ref heapRef, this.count);

// Clear the last position to avoid holding references
Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(this.heap), this.count) = default(T)!;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't believe the behavior is the same as the original code here and we are doing stuff (assigning default(T)!) when we shouldn't (structs).

/// </summary>
/// <returns>The item with the highest priority.</returns>
/// <exception cref="InvalidOperationException">Thrown if the priority queue is empty.</exception>
public T Peek()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would have kept this purely to complete the implementation.

List<T> data = this.heap;
T item = data[index];
ref T heapRef = ref MemoryMarshal.GetArrayDataReference(this.heap);
T item = Unsafe.Add(ref heapRef, index);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Here and elsewhere Unsafe.Add produces poor codegen for int. You should use uint or nuint

/// Resizes the internal array when capacity is exceeded.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private void Resize()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@stefannikolei
Copy link
Copy Markdown
Contributor Author

I'm not sure about this PR. I think we need to do some work here for correctness and safety along with performance.

Would you think this is a feasible way. I improved the performance. Just forgot to post the benchmark.

Or do you see an alternative to using unsafe? (Unsafe is a new land for me😅)

@JimBobSquarePants
Copy link
Copy Markdown
Member

I think we can and should use unsafe we just have to be very careful when doing so.

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.

3 participants