From 67630e3797c1352be4d77e0012f1cecef37f4e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 29 Jul 2025 17:07:17 +0200 Subject: [PATCH] Optimize AddRange copy --- .../RecyclablePriorityQueue.AddRange.cs | 94 +++++++++++++++++++ .../RecyclablePriorityQueueTests.cs | 50 ++++++++++ 2 files changed, 144 insertions(+) create mode 100644 Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs diff --git a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs new file mode 100644 index 0000000..6c759aa --- /dev/null +++ b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs @@ -0,0 +1,94 @@ +using System.Runtime.CompilerServices; +using System.Numerics; +using Recyclable.Collections.Pools; + +namespace Recyclable.Collections +{ + internal static class zRecyclablePriorityQueueAddRange + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EnsureCapacity(RecyclablePriorityQueue queue, int min) + { + if (queue._heap.Length >= min) + { + return; + } + + int newSize = queue._heap.Length; + do + { + if (newSize >= RecyclableDefaults.MaxPooledBlockSize) + { + newSize = RecyclableDefaults.MaxPooledBlockSize; + break; + } + + newSize <<= 1; + } + while (newSize < min); + + if (newSize < min) + { + newSize = min; + } + + T[] newHeap = newSize >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(newSize) + : new T[newSize]; + System.Array.Copy(queue._heap, newHeap, queue._size); + + if (queue._heap.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(queue._heap, RecyclablePriorityQueue._needsClearing); + } + + queue._heap = newHeap; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, ReadOnlySpan items) + { + int count = items.Length; + if (count == 0) + { + return; + } + + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + for (int i = 0; i < count; i++) + { + queue._heap[startIndex + i] = items[i]; + } + queue._size = newCount; + + int half = newCount >> 1; + for (int i = half - 1; i >= 0; i--) + { + int index = i; + T item = queue._heap[index]; + while (index < half) + { + int child = (index << 1) + 1; + int right = child + 1; + if (right < newCount && queue._comparer.Compare(queue._heap[right], queue._heap[child]) < 0) + { + child = right; + } + + if (queue._comparer.Compare(queue._heap[child], item) >= 0) + { + break; + } + + queue._heap[index] = queue._heap[child]; + index = child; + } + + queue._heap[index] = item; + } + } + } +} diff --git a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs index b9a1df2..1870586 100644 --- a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs +++ b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs @@ -52,5 +52,55 @@ namespace Recyclable.CollectionsTests using var queue = new RecyclablePriorityQueue(); _ = Assert.Throws(() => queue.Dequeue()); } + + [Fact] + public void AddRangeSpanShouldAddItemsInSortedOrder() + { + using var queue = new RecyclablePriorityQueue(); + queue.AddRange((ReadOnlySpan)_testData); + + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + _ = result.Should().Equal(_testData.Order()); + } + + [Fact] + public void AddRangeSpanShouldDoNothingWhenEmpty() + { + using var queue = new RecyclablePriorityQueue(); + queue.AddRange(ReadOnlySpan.Empty); + _ = queue.Should().BeEmpty(); + } + + [Fact] + public void AddRangeSpanShouldNotOverrideItems() + { + using var queue = new RecyclablePriorityQueue(); + queue.AddRange((ReadOnlySpan)_testData); + queue.AddRange((ReadOnlySpan)_testData); + + var expected = _testData.Concat(_testData).Order().ToArray(); + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + _ = result.Should().Equal(expected); + } + + [Fact] + public void AddRangeShouldAcceptNulls() + { + using var queue = new RecyclablePriorityQueue(); + queue.AddRange(new long?[] { null, 1 }); + _ = queue.Should().HaveCount(2).And.Contain((long?)null); + } + } } +