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 1/9] 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); + } + } } + From d8440fe8c906c5641ba5ac6fb903d59370850c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 16 Sep 2025 13:52:10 +0200 Subject: [PATCH 2/9] Add AddRange overloads for priority queue --- .../RecyclablePriorityQueue.AddRange.cs | 258 ++++++++++++++++++ .../RecyclablePriorityQueueTests.cs | 47 ++++ 2 files changed, 305 insertions(+) diff --git a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs index 6c759aa..c91513e 100644 --- a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs +++ b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs @@ -1,4 +1,7 @@ using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Collections; +using System.Collections.Generic; using System.Numerics; using Recyclable.Collections.Pools; @@ -90,5 +93,260 @@ namespace Recyclable.Collections queue._heap[index] = item; } } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, Span items) + => AddRange(queue, (ReadOnlySpan)items); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, T[] items) + => AddRange(queue, (ReadOnlySpan)items); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, in Array items) + { + int count = items.Length; + if (count == 0) + { + return; + } + + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + Array.Copy(items, items.GetLowerBound(0), queue._heap, startIndex, count); + 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, List items) + => AddRange(queue, CollectionsMarshal.AsSpan(items)); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, ICollection items) + { + int count = items.Count; + if (count == 0) + { + return; + } + + T[] buffer = count >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(checked((int)BitOperations.RoundUpToPowerOf2((uint)count))) + : new T[count]; + + items.CopyTo(buffer, 0); + + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + Array.Copy(buffer, 0, queue._heap, startIndex, count); + queue._size = newCount; + + if (count >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(buffer, RecyclablePriorityQueue._needsClearing); + } + + 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, ICollection items) + { + int count = items.Count; + if (count == 0) + { + return; + } + + T[] buffer = count >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(checked((int)BitOperations.RoundUpToPowerOf2((uint)count))) + : new T[count]; + + items.CopyTo(buffer, 0); + + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + Array.Copy(buffer, 0, queue._heap, startIndex, count); + queue._size = newCount; + + if (count >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(buffer, RecyclablePriorityQueue._needsClearing); + } + + 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, RecyclableList items) + => AddRange(queue, new ReadOnlySpan(items._memoryBlock, 0, items._count)); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, RecyclablePriorityQueue items) + => AddRange(queue, new ReadOnlySpan(items._heap, 0, items._size)); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, IReadOnlyList items) + { + int count = items.Count; + 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, IEnumerable items) + { + if (items is RecyclableList recyclableList) + { + AddRange(queue, recyclableList); + } + else if (items is RecyclablePriorityQueue priorityQueue) + { + AddRange(queue, priorityQueue); + } + else if (items is T[] array) + { + AddRange(queue, array); + } + else if (items is List list) + { + AddRange(queue, list); + } + else if (items is ICollection genericCollection) + { + AddRange(queue, genericCollection); + } + else if (items is ICollection collection) + { + AddRange(queue, collection); + } + else if (items is IReadOnlyList readOnlyList) + { + AddRange(queue, readOnlyList); + } + else + { + foreach (T item in items) + { + queue.Enqueue(item); + } + } + } } } diff --git a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs index 1870586..ac92e46 100644 --- a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs +++ b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs @@ -93,6 +93,53 @@ namespace Recyclable.CollectionsTests _ = result.Should().Equal(expected); } + [Fact] + public void AddRangeArrayShouldAddItemsInSortedOrder() + { + using var queue = new RecyclablePriorityQueue(); + queue.AddRange(_testData); + + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + _ = result.Should().Equal(_testData.Order()); + } + + [Fact] + public void AddRangeListShouldAddItemsInSortedOrder() + { + using var queue = new RecyclablePriorityQueue(); + queue.AddRange(new List(_testData)); + + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + _ = result.Should().Equal(_testData.Order()); + } + + [Fact] + public void AddRangeArrayShouldNotOverrideItems() + { + using var queue = new RecyclablePriorityQueue(); + queue.AddRange(_testData); + queue.AddRange(_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() { From 6d80b36d7042c63b218c56bbd3b06ee3117dec6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 16 Sep 2025 13:52:15 +0200 Subject: [PATCH 3/9] Implement standalone AddRange overloads --- .../RecyclablePriorityQueue.AddRange.cs | 222 +++++++++++++++++- 1 file changed, 217 insertions(+), 5 deletions(-) diff --git a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs index c91513e..3346bc5 100644 --- a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs +++ b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs @@ -96,11 +96,95 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclablePriorityQueue queue, Span items) - => AddRange(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; + } + } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclablePriorityQueue queue, T[] items) - => AddRange(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; + } + } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclablePriorityQueue queue, in Array items) @@ -147,7 +231,49 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclablePriorityQueue queue, List items) - => AddRange(queue, CollectionsMarshal.AsSpan(items)); + { + int count = items.Count; + 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; + } + } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclablePriorityQueue queue, ICollection items) @@ -257,11 +383,97 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclablePriorityQueue queue, RecyclableList items) - => AddRange(queue, new ReadOnlySpan(items._memoryBlock, 0, items._count)); + { + int count = items._count; + if (count == 0) + { + return; + } + + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + T[] source = items._memoryBlock; + for (int i = 0; i < count; i++) + { + queue._heap[startIndex + i] = source[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; + } + } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclablePriorityQueue queue, RecyclablePriorityQueue items) - => AddRange(queue, new ReadOnlySpan(items._heap, 0, items._size)); + { + int count = items._size; + if (count == 0) + { + return; + } + + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + T[] source = items._heap; + for (int i = 0; i < count; i++) + { + queue._heap[startIndex + i] = source[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; + } + } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclablePriorityQueue queue, IReadOnlyList items) From ff38aa302a0b9a18a0bc0f0c10089b9ad93ad9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 16 Sep 2025 13:52:21 +0200 Subject: [PATCH 4/9] Add more PriorityQueue AddRange overloads --- .../RecyclablePriorityQueue.AddRange.cs | 370 ++++++++++++++++++ .../RecyclablePriorityQueueTests.cs | 75 ++++ 2 files changed, 445 insertions(+) diff --git a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs index 3346bc5..f406bb5 100644 --- a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs +++ b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs @@ -448,6 +448,352 @@ namespace Recyclable.Collections } 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, RecyclableQueue items) + { + long longCount = items._count; + if (longCount == 0) + { + return; + } + + int count = checked((int)longCount); + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + var sourceChunk = items._head; + int destIndex = startIndex; + while (sourceChunk != null) + { + int sourceIndex = sourceChunk.Bottom; + int sourceTop = sourceChunk.Top; + T[] source = sourceChunk.Value; + + while (sourceIndex < sourceTop) + { + queue._heap[destIndex++] = source[sourceIndex++]; + } + + sourceChunk = sourceChunk.Next; + } + + 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, RecyclableStack items) + { + long longCount = items._count; + if (longCount == 0) + { + return; + } + + int count = checked((int)longCount); + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + var sourceChunk = items._current; + while (sourceChunk.Previous != null) + { + sourceChunk = sourceChunk.Previous; + } + + int destIndex = startIndex; + while (sourceChunk != null) + { + T[] source = sourceChunk.Value; + int sourceCount = sourceChunk.Index; + int sourceIndex = 0; + + while (sourceIndex < sourceCount) + { + queue._heap[destIndex++] = source[sourceIndex++]; + } + + sourceChunk = sourceChunk.Next; + } + + 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, RecyclableLinkedList items) + { + long longCount = items._count; + if (longCount == 0) + { + return; + } + + int count = checked((int)longCount); + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + var sourceChunk = items._head; + int destIndex = startIndex; + while (sourceChunk != null) + { + int sourceIndex = sourceChunk.Bottom; + int sourceTop = sourceChunk.Top; + T[] source = sourceChunk.Value; + + while (sourceIndex < sourceTop) + { + queue._heap[destIndex++] = source[sourceIndex++]; + } + + sourceChunk = sourceChunk.Next; + } + + 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, RecyclableSortedSet items) + { + int count = items._count; + if (count == 0) + { + return; + } + + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + T[] source = items._items; + for (int i = 0; i < count; i++) + { + queue._heap[startIndex + i] = source[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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, RecyclableHashSet items) + { + int count = items._count; + if (count == 0) + { + return; + } + + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + var entries = items._entries; + int shift = items._blockShift, + mask = items._blockSizeMinus1; + + for (int i = 0; i < count; i++) + { + queue._heap[startIndex + i] = entries[i >> shift][i & mask].Value; + } + 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, RecyclableLongList items) + { + long longCount = items._longCount; + if (longCount == 0) + { + return; + } + + int blockSize = items._blockSize; + int fullBlocks = (int)(longCount >> items._blockSizePow2BitShift); + int lastBlockLength = (int)(longCount & items._blockSizeMinus1); + + int startIndex = queue._size; + int newCount = startIndex + checked((int)longCount); + EnsureCapacity(queue, newCount); + + int destIndex = startIndex; + for (int blockIndex = 0; blockIndex < fullBlocks; blockIndex++) + { + T[] block = items._memoryBlocks[blockIndex]; + int sourceIndex = 0; + while (sourceIndex < blockSize) + { + queue._heap[destIndex++] = block[sourceIndex++]; + } + } + + if (lastBlockLength > 0) + { + T[] block = items._memoryBlocks[fullBlocks]; + int sourceIndex = 0; + while (sourceIndex < lastBlockLength) + { + queue._heap[destIndex++] = block[sourceIndex++]; + } + } + + queue._size = newCount; + int half = newCount >> 1; for (int i = half - 1; i >= 0; i--) { @@ -532,6 +878,30 @@ namespace Recyclable.Collections { AddRange(queue, priorityQueue); } + else if (items is RecyclableQueue recyclableQueue) + { + AddRange(queue, recyclableQueue); + } + else if (items is RecyclableStack recyclableStack) + { + AddRange(queue, recyclableStack); + } + else if (items is RecyclableLinkedList linkedList) + { + AddRange(queue, linkedList); + } + else if (items is RecyclableSortedSet sortedSet) + { + AddRange(queue, sortedSet); + } + else if (items is RecyclableHashSet hashSet) + { + AddRange(queue, hashSet); + } + else if (items is RecyclableLongList longList) + { + AddRange(queue, longList); + } else if (items is T[] array) { AddRange(queue, array); diff --git a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs index ac92e46..15903c3 100644 --- a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs +++ b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs @@ -148,6 +148,81 @@ namespace Recyclable.CollectionsTests _ = queue.Should().HaveCount(2).And.Contain((long?)null); } + [Fact] + public void AddRangeQueueShouldAddItemsInSortedOrder() + { + using var source = new RecyclableQueue(_testData); + using var queue = new RecyclablePriorityQueue(); + + queue.AddRange(source); + + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + _ = result.Should().Equal(_testData.Order()); + } + + [Fact] + public void AddRangeQueueShouldNotOverrideItems() + { + using var source = new RecyclableQueue(_testData); + using var queue = new RecyclablePriorityQueue(); + + queue.AddRange(source); + queue.AddRange(source); + + 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 AddRangeStackShouldAddItemsInSortedOrder() + { + using var source = new RecyclableStack(_testData); + using var queue = new RecyclablePriorityQueue(); + + queue.AddRange(source); + + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + _ = result.Should().Equal(_testData.Order()); + } + + [Fact] + public void AddRangeSortedSetShouldAddItemsInSortedOrder() + { + using var source = new RecyclableSortedSet(); + foreach (int item in _testData) + { + source.Add(item); + } + + using var queue = new RecyclablePriorityQueue(); + + queue.AddRange(source); + + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + _ = result.Should().Equal(_testData.Order()); + } + } } From 33187c105363aebfdbf8351d2323ccf8c334de34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 16 Sep 2025 13:52:29 +0200 Subject: [PATCH 5/9] Add dictionary overloads for priority queue --- .../RecyclablePriorityQueue.AddRange.cs | 244 ++++++++++++++++++ .../RecyclablePriorityQueueTests.cs | 148 +++++++++++ 2 files changed, 392 insertions(+) diff --git a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs index f406bb5..c7786e2 100644 --- a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs +++ b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs @@ -2,6 +2,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Collections; using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; using System.Numerics; using Recyclable.Collections.Pools; @@ -9,6 +11,96 @@ namespace Recyclable.Collections { internal static class zRecyclablePriorityQueueAddRange { + private static class AddRangeHelper + { + private static readonly Action, object>? _dictionaryAdder; + private static readonly Action, object>? _sortedListAdder; + private static readonly Action, object>? _sortedDictionaryAdder; + private static readonly Type? _dictionaryType; + private static readonly Type? _sortedListType; + private static readonly Type? _sortedDictionaryType; + + static AddRangeHelper() + { + Type elementType = typeof(T); + if (elementType.IsGenericType && elementType.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) + { + Type[] args = elementType.GetGenericArguments(); + _dictionaryType = typeof(RecyclableDictionary<,>).MakeGenericType(args); + _sortedDictionaryType = typeof(RecyclableSortedDictionary<,>).MakeGenericType(args); + + MethodInfo? method = typeof(zRecyclablePriorityQueueAddRange).GetMethod( + nameof(AddRange), + BindingFlags.NonPublic | BindingFlags.Static, + binder: null, + types: new[] { typeof(RecyclablePriorityQueue<>).MakeGenericType(elementType), _dictionaryType }, + modifiers: null); + if (method != null) + { + var queueParam = Expression.Parameter(typeof(RecyclablePriorityQueue)); + var objParam = Expression.Parameter(typeof(object)); + var call = Expression.Call(method, queueParam, Expression.Convert(objParam, _dictionaryType)); + _dictionaryAdder = Expression.Lambda, object>>(call, queueParam, objParam).Compile(); + } + + method = typeof(zRecyclablePriorityQueueAddRange).GetMethod( + nameof(AddRange), + BindingFlags.NonPublic | BindingFlags.Static, + binder: null, + types: new[] { typeof(RecyclablePriorityQueue<>).MakeGenericType(elementType), _sortedDictionaryType }, + modifiers: null); + if (method != null) + { + var queueParam = Expression.Parameter(typeof(RecyclablePriorityQueue)); + var objParam = Expression.Parameter(typeof(object)); + var call = Expression.Call(method, queueParam, Expression.Convert(objParam, _sortedDictionaryType)); + _sortedDictionaryAdder = Expression.Lambda, object>>(call, queueParam, objParam).Compile(); + } + } + else if (elementType.IsGenericType && elementType.GetGenericTypeDefinition() == typeof(ValueTuple<,>)) + { + Type[] args = elementType.GetGenericArguments(); + _sortedListType = typeof(RecyclableSortedList<,>).MakeGenericType(args); + MethodInfo? method = typeof(zRecyclablePriorityQueueAddRange).GetMethod( + nameof(AddRange), + BindingFlags.NonPublic | BindingFlags.Static, + binder: null, + types: new[] { typeof(RecyclablePriorityQueue<>).MakeGenericType(elementType), _sortedListType }, + modifiers: null); + if (method != null) + { + var queueParam = Expression.Parameter(typeof(RecyclablePriorityQueue)); + var objParam = Expression.Parameter(typeof(object)); + var call = Expression.Call(method, queueParam, Expression.Convert(objParam, _sortedListType)); + _sortedListAdder = Expression.Lambda, object>>(call, queueParam, objParam).Compile(); + } + } + } + + internal static bool TryAddRange(RecyclablePriorityQueue queue, IEnumerable items) + { + if (_dictionaryAdder != null && _dictionaryType!.IsInstanceOfType(items)) + { + _dictionaryAdder(queue, items); + return true; + } + + if (_sortedDictionaryAdder != null && _sortedDictionaryType!.IsInstanceOfType(items)) + { + _sortedDictionaryAdder(queue, items); + return true; + } + + if (_sortedListAdder != null && _sortedListType!.IsInstanceOfType(items)) + { + _sortedListAdder(queue, items); + return true; + } + + return false; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void EnsureCapacity(RecyclablePriorityQueue queue, int min) { @@ -821,6 +913,155 @@ namespace Recyclable.Collections } } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue> queue, RecyclableDictionary items) + where TKey : notnull + { + int count = items._count; + if (count == 0) + { + return; + } + + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + var entries = items._entries; + int shift = items._blockShift, + mask = items._blockSizeMinus1; + + for (int i = 0; i < count; i++) + { + ref var src = ref entries[i >> shift][i & mask]; + queue._heap[startIndex + i] = new KeyValuePair(src.Key, src.Value); + } + queue._size = newCount; + + int half = newCount >> 1; + for (int i = half - 1; i >= 0; i--) + { + int index = i; + var 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue> queue, RecyclableSortedDictionary items) + where TKey : notnull + { + int count = items.Count; + 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] = new KeyValuePair(items.GetKey(i), items.GetValue(i)); + } + queue._size = newCount; + + int half = newCount >> 1; + for (int i = half - 1; i >= 0; i--) + { + int index = i; + var 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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue<(TKey Key, TValue Value)> queue, RecyclableSortedList items) + where TKey : notnull + { + int count = items._count; + if (count == 0) + { + return; + } + + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + TKey[] keys = items._keys; + TValue[] values = items._values; + + for (int i = 0; i < count; i++) + { + queue._heap[startIndex + i] = (keys[i], values[i]); + } + queue._size = newCount; + + int half = newCount >> 1; + for (int i = half - 1; i >= 0; i--) + { + int index = i; + var 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; + } + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclablePriorityQueue queue, IReadOnlyList items) { @@ -918,6 +1159,9 @@ namespace Recyclable.Collections { AddRange(queue, collection); } + else if (AddRangeHelper.TryAddRange(queue, items)) + { + } else if (items is IReadOnlyList readOnlyList) { AddRange(queue, readOnlyList); diff --git a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs index 15903c3..98d90fb 100644 --- a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs +++ b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs @@ -223,6 +223,154 @@ namespace Recyclable.CollectionsTests _ = result.Should().Equal(_testData.Order()); } + [Fact] + public void AddRangeDictionaryShouldAddItemsInSortedOrder() + { + using var source = new RecyclableDictionary(); + for (int i = 0; i < _testData.Length; i++) + { + source.Add(i, _testData[i]); + } + + using var queue = new RecyclablePriorityQueue>(comparer: Comparer>.Create((a, b) => a.Key.CompareTo(b.Key))); + + queue.AddRange(source); + + var result = new List>(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + var expected = _testData.Select((v, i) => new KeyValuePair(i, v)).ToList(); + _ = result.Should().Equal(expected); + } + + [Fact] + public void AddRangeDictionaryShouldNotOverrideItems() + { + using var source = new RecyclableDictionary(); + for (int i = 0; i < _testData.Length; i++) + { + source.Add(i, _testData[i]); + } + + using var queue = new RecyclablePriorityQueue>(comparer: Comparer>.Create((a, b) => a.Key.CompareTo(b.Key))); + + queue.AddRange(source); + queue.AddRange(source); + + var result = new List>(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + var expected = _testData.Select((v, i) => new KeyValuePair(i, v)) + .Concat(_testData.Select((v, i) => new KeyValuePair(i, v))) + .ToList(); + _ = result.Should().Equal(expected); + } + + [Fact] + public void AddRangeSortedListShouldAddItemsInSortedOrder() + { + using var source = new RecyclableSortedList(); + for (int i = 0; i < _testData.Length; i++) + { + source.Add(i, _testData[i]); + } + + using var queue = new RecyclablePriorityQueue<(int Key, int Value)>(comparer: Comparer<(int Key, int Value)>.Create((a, b) => a.Key.CompareTo(b.Key))); + + queue.AddRange(source); + + var result = new List<(int Key, int Value)>(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + var expected = _testData.Select((v, i) => (i, v)).ToList(); + _ = result.Should().Equal(expected); + } + + [Fact] + public void AddRangeSortedListShouldNotOverrideItems() + { + using var source = new RecyclableSortedList(); + for (int i = 0; i < _testData.Length; i++) + { + source.Add(i, _testData[i]); + } + + using var queue = new RecyclablePriorityQueue<(int Key, int Value)>(comparer: Comparer<(int Key, int Value)>.Create((a, b) => a.Key.CompareTo(b.Key))); + + queue.AddRange(source); + queue.AddRange(source); + + var result = new List<(int Key, int Value)>(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + var expected = _testData.Select((v, i) => (i, v)) + .Concat(_testData.Select((v, i) => (i, v))) + .OrderBy(p => p.Item1) + .ToList(); + _ = result.Should().Equal(expected); + } + + [Fact] + public void AddRangeSortedDictionaryShouldAddItemsInSortedOrder() + { + using var source = new RecyclableSortedDictionary(); + for (int i = 0; i < _testData.Length; i++) + { + source.Add(i, _testData[i]); + } + + using var queue = new RecyclablePriorityQueue>(comparer: Comparer>.Create((a, b) => a.Key.CompareTo(b.Key))); + + queue.AddRange(source); + + var result = new List>(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + var expected = _testData.Select((v, i) => new KeyValuePair(i, v)).ToList(); + _ = result.Should().Equal(expected); + } + + [Fact] + public void AddRangeSortedDictionaryShouldNotOverrideItems() + { + using var source = new RecyclableSortedDictionary(); + for (int i = 0; i < _testData.Length; i++) + { + source.Add(i, _testData[i]); + } + + using var queue = new RecyclablePriorityQueue>(comparer: Comparer>.Create((a, b) => a.Key.CompareTo(b.Key))); + + queue.AddRange(source); + queue.AddRange(source); + + var result = new List>(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + var expected = _testData.Select((v, i) => new KeyValuePair(i, v)) + .Concat(_testData.Select((v, i) => new KeyValuePair(i, v))) + .ToList(); + _ = result.Should().Equal(expected); + } + } } From 050d7fd38687f5822c5e57cd51edba334c88e569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 16 Sep 2025 13:52:36 +0200 Subject: [PATCH 6/9] Add non-generic enumerable AddRange to priority queue --- .../RecyclablePriorityQueue.AddRange.cs | 12 +++++++ .../RecyclablePriorityQueueTests.cs | 35 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs index c7786e2..44bdefd 100644 --- a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs +++ b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs @@ -1108,6 +1108,18 @@ namespace Recyclable.Collections } } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static IEnumerator AddRange(this RecyclablePriorityQueue queue, IEnumerable source) + { + IEnumerator enumerator = source.GetEnumerator(); + while (enumerator.MoveNext()) + { + queue.Enqueue((T)enumerator.Current!); + } + + return enumerator; + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclablePriorityQueue queue, IEnumerable items) { diff --git a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs index 98d90fb..ca701ba 100644 --- a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs +++ b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs @@ -371,6 +371,41 @@ namespace Recyclable.CollectionsTests _ = result.Should().Equal(expected); } + [Fact] + public void AddRangeEnumerableShouldAddItemsInSortedOrder() + { + using var queue = new RecyclablePriorityQueue(); + + _ = queue.AddRange((System.Collections.IEnumerable)_testData); + + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + var expected = _testData.OrderBy(v => v).ToList(); + _ = result.Should().Equal(expected); + } + + [Fact] + public void AddRangeEnumerableShouldNotOverrideItems() + { + using var queue = new RecyclablePriorityQueue(); + + _ = queue.AddRange((System.Collections.IEnumerable)_testData); + _ = queue.AddRange((System.Collections.IEnumerable)_testData); + + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + var expected = _testData.Concat(_testData).OrderBy(v => v).ToList(); + _ = result.Should().Equal(expected); + } + } } From f6640deaf1271f8a7453d4275e518cef6ff12820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 16 Sep 2025 13:52:42 +0200 Subject: [PATCH 7/9] Add IReadOnlyCollection AddRange overload --- .../RecyclablePriorityQueue.AddRange.cs | 51 ++++++++++++++++++ .../RecyclablePriorityQueueTests.cs | 52 +++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs index 44bdefd..205d7c2 100644 --- a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs +++ b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs @@ -1108,6 +1108,53 @@ namespace Recyclable.Collections } } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, IReadOnlyCollection items) + { + int count = items.Count; + if (count == 0) + { + return; + } + + int startIndex = queue._size; + int newCount = startIndex + count; + EnsureCapacity(queue, newCount); + + int index = startIndex; + foreach (T item in items) + { + queue._heap[index++] = item; + } + queue._size = newCount; + + int half = newCount >> 1; + for (int i = half - 1; i >= 0; i--) + { + int idx = i; + T current = queue._heap[idx]; + while (idx < half) + { + int child = (idx << 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], current) >= 0) + { + break; + } + + queue._heap[idx] = queue._heap[child]; + idx = child; + } + + queue._heap[idx] = current; + } + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static IEnumerator AddRange(this RecyclablePriorityQueue queue, IEnumerable source) { @@ -1178,6 +1225,10 @@ namespace Recyclable.Collections { AddRange(queue, readOnlyList); } + else if (items is IReadOnlyCollection readOnlyCollection) + { + AddRange(queue, readOnlyCollection); + } else { foreach (T item in items) diff --git a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs index ca701ba..d6866ff 100644 --- a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs +++ b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs @@ -1,5 +1,6 @@ using FluentAssertions; using Recyclable.Collections; +using System.Collections; namespace Recyclable.CollectionsTests { @@ -406,6 +407,57 @@ namespace Recyclable.CollectionsTests _ = result.Should().Equal(expected); } + [Fact] + public void AddRangeReadOnlyCollectionShouldAddItemsInSortedOrder() + { + var source = new TestReadOnlyCollection(_testData); + using var queue = new RecyclablePriorityQueue(); + + queue.AddRange(source); + + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + _ = result.Should().Equal(_testData.Order()); + } + + [Fact] + public void AddRangeReadOnlyCollectionShouldNotOverrideItems() + { + var source = new TestReadOnlyCollection(_testData); + using var queue = new RecyclablePriorityQueue(); + + queue.AddRange(source); + queue.AddRange(source); + + var expected = _testData.Concat(_testData).Order().ToArray(); + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + _ = result.Should().Equal(expected); + } + + private sealed class TestReadOnlyCollection : IReadOnlyCollection + { + private readonly T[] _items; + + public TestReadOnlyCollection(T[] items) + { + _items = items; + } + + public int Count => _items.Length; + + public IEnumerator GetEnumerator() => ((IEnumerable)_items).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator(); + } } } From b0e7f96496851d84749a3e3d58896bde33cd4d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 16 Sep 2025 14:20:11 +0200 Subject: [PATCH 8/9] Add memory-based AddRange overloads --- .../RecyclablePriorityQueue.AddRange.cs | 94 +++++++++++++++++++ .../RecyclablePriorityQueueTests.cs | 32 +++++++ 2 files changed, 126 insertions(+) diff --git a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs index 205d7c2..1fd4b36 100644 --- a/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs +++ b/Recyclable.Collections/RecyclablePriorityQueue.AddRange.cs @@ -232,6 +232,100 @@ namespace Recyclable.Collections } } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, ReadOnlyMemory items) + { + ReadOnlySpan span = items.Span; + int count = span.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] = span[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; + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclablePriorityQueue queue, Memory items) + { + Span span = items.Span; + int count = span.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] = span[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; + } + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclablePriorityQueue queue, T[] items) { diff --git a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs index d6866ff..39936f1 100644 --- a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs +++ b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs @@ -124,6 +124,38 @@ namespace Recyclable.CollectionsTests _ = result.Should().Equal(_testData.Order()); } + [Fact] + public void AddRangeReadOnlyMemoryShouldAddItemsInSortedOrder() + { + using var queue = new RecyclablePriorityQueue(); + queue.AddRange((ReadOnlyMemory)_testData); + + var result = new List(); + while (queue.LongCount > 0) + { + result.Add(queue.Dequeue()); + } + + _ = result.Should().Equal(_testData.Order()); + } + + [Fact] + public void AddRangeMemoryShouldNotOverrideItems() + { + using var queue = new RecyclablePriorityQueue(); + queue.AddRange(_testData.AsMemory()); + queue.AddRange(_testData.AsMemory()); + + 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 AddRangeArrayShouldNotOverrideItems() { From 37f147a262a7cb1a47d4857af1725578c10cc906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Tue, 16 Sep 2025 14:20:18 +0200 Subject: [PATCH 9/9] Fix priority queue tests compilation --- .../RecyclablePriorityQueueTests.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs index 39936f1..44d03d4 100644 --- a/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs +++ b/Recyclable.CollectionsTests/RecyclablePriorityQueueTests.cs @@ -66,7 +66,7 @@ namespace Recyclable.CollectionsTests result.Add(queue.Dequeue()); } - _ = result.Should().Equal(_testData.Order()); + _ = result.Should().Equal(_testData.OrderBy(static value => value)); } [Fact] @@ -84,7 +84,7 @@ namespace Recyclable.CollectionsTests queue.AddRange((ReadOnlySpan)_testData); queue.AddRange((ReadOnlySpan)_testData); - var expected = _testData.Concat(_testData).Order().ToArray(); + var expected = _testData.Concat(_testData).OrderBy(static value => value).ToArray(); var result = new List(); while (queue.LongCount > 0) { @@ -106,7 +106,7 @@ namespace Recyclable.CollectionsTests result.Add(queue.Dequeue()); } - _ = result.Should().Equal(_testData.Order()); + _ = result.Should().Equal(_testData.OrderBy(static value => value)); } [Fact] @@ -121,7 +121,7 @@ namespace Recyclable.CollectionsTests result.Add(queue.Dequeue()); } - _ = result.Should().Equal(_testData.Order()); + _ = result.Should().Equal(_testData.OrderBy(static value => value)); } [Fact] @@ -136,7 +136,7 @@ namespace Recyclable.CollectionsTests result.Add(queue.Dequeue()); } - _ = result.Should().Equal(_testData.Order()); + _ = result.Should().Equal(_testData.OrderBy(static value => value)); } [Fact] @@ -146,7 +146,7 @@ namespace Recyclable.CollectionsTests queue.AddRange(_testData.AsMemory()); queue.AddRange(_testData.AsMemory()); - var expected = _testData.Concat(_testData).Order().ToArray(); + var expected = _testData.Concat(_testData).OrderBy(static value => value).ToArray(); var result = new List(); while (queue.LongCount > 0) { @@ -163,7 +163,7 @@ namespace Recyclable.CollectionsTests queue.AddRange(_testData); queue.AddRange(_testData); - var expected = _testData.Concat(_testData).Order().ToArray(); + var expected = _testData.Concat(_testData).OrderBy(static value => value).ToArray(); var result = new List(); while (queue.LongCount > 0) { @@ -195,7 +195,7 @@ namespace Recyclable.CollectionsTests result.Add(queue.Dequeue()); } - _ = result.Should().Equal(_testData.Order()); + _ = result.Should().Equal(_testData.OrderBy(static value => value)); } [Fact] @@ -207,7 +207,7 @@ namespace Recyclable.CollectionsTests queue.AddRange(source); queue.AddRange(source); - var expected = _testData.Concat(_testData).Order().ToArray(); + var expected = _testData.Concat(_testData).OrderBy(static value => value).ToArray(); var result = new List(); while (queue.LongCount > 0) { @@ -231,7 +231,7 @@ namespace Recyclable.CollectionsTests result.Add(queue.Dequeue()); } - _ = result.Should().Equal(_testData.Order()); + _ = result.Should().Equal(_testData.OrderBy(static value => value)); } [Fact] @@ -253,7 +253,7 @@ namespace Recyclable.CollectionsTests result.Add(queue.Dequeue()); } - _ = result.Should().Equal(_testData.Order()); + _ = result.Should().Equal(_testData.OrderBy(static value => value)); } [Fact] @@ -453,7 +453,7 @@ namespace Recyclable.CollectionsTests result.Add(queue.Dequeue()); } - _ = result.Should().Equal(_testData.Order()); + _ = result.Should().Equal(_testData.OrderBy(static value => value)); } [Fact] @@ -465,7 +465,7 @@ namespace Recyclable.CollectionsTests queue.AddRange(source); queue.AddRange(source); - var expected = _testData.Concat(_testData).Order().ToArray(); + var expected = _testData.Concat(_testData).OrderBy(static value => value).ToArray(); var result = new List(); while (queue.LongCount > 0) {