From 56a5c3375a25a42a519998f644d79c58706b2605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Thu, 17 Jul 2025 00:21:45 +0200 Subject: [PATCH] Optimize sorted-list and read-only list AddRange --- .../RecyclableStack.AddRange.cs | 86 ++++++++++++++++++- .../RecyclableStackTests.cs | 55 ++++++++++++ 2 files changed, 137 insertions(+), 4 deletions(-) diff --git a/Recyclable.Collections/RecyclableStack.AddRange.cs b/Recyclable.Collections/RecyclableStack.AddRange.cs index 0053375..435ecff 100644 --- a/Recyclable.Collections/RecyclableStack.AddRange.cs +++ b/Recyclable.Collections/RecyclableStack.AddRange.cs @@ -798,19 +798,97 @@ namespace Recyclable.Collections internal static void AddRange(this RecyclableStack<(TKey Key, TValue Value)> stack, RecyclableSortedList items) where TKey : notnull { - for (int i = 0; i < items._count; i++) + int count = items._count; + if (count == 0) { - stack.Push((items._keys[i], items._values[i])); + return; } + + EnsureCapacity(stack, stack._count + count); + + RecyclableArrayPoolChunk<(TKey Key, TValue Value)> chunk = stack._current; + while (chunk.Previous != null && chunk.Previous.Index < chunk.Previous.Value.Length) + { + chunk = chunk.Previous; + } + + int index = chunk.Index; + int chunkLength = chunk.Value.Length; + int copied = 0; + TKey[] keys = items._keys; + TValue[] values = items._values; + + while (copied < count) + { + int toCopy = Math.Min(chunkLength - index, count - copied); + var destination = chunk.Value; + int local = copied; + for (int i = 0; i < toCopy; i++) + { + destination[index + i] = (keys[local + i], values[local + i]); + } + + copied += toCopy; + index += toCopy; + + if (index == chunkLength && copied < count) + { + chunk.Index = index; + chunk = chunk.Next!; + index = 0; + chunkLength = chunk.Value.Length; + } + } + + chunk.Index = index; + stack._count += count; } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclableStack stack, IReadOnlyList items) { - for (int i = 0; i < items.Count; i++) + int count = items.Count; + if (count == 0) { - stack.Push(items[i]); + return; } + + EnsureCapacity(stack, stack._count + count); + + RecyclableArrayPoolChunk chunk = stack._current; + while (chunk.Previous != null && chunk.Previous.Index < chunk.Previous.Value.Length) + { + chunk = chunk.Previous; + } + + int index = chunk.Index; + int chunkLength = chunk.Value.Length; + int copied = 0; + + while (copied < count) + { + int toCopy = Math.Min(chunkLength - index, count - copied); + var destination = chunk.Value; + int local = copied; + for (int i = 0; i < toCopy; i++) + { + destination[index + i] = items[local + i]; + } + + copied += toCopy; + index += toCopy; + + if (index == chunkLength && copied < count) + { + chunk.Index = index; + chunk = chunk.Next!; + index = 0; + chunkLength = chunk.Value.Length; + } + } + + chunk.Index = index; + stack._count += count; } internal static IEnumerator AddRange(this RecyclableStack stack, IEnumerable source) diff --git a/Recyclable.CollectionsTests/RecyclableStackTests.cs b/Recyclable.CollectionsTests/RecyclableStackTests.cs index 24e6553..276ef10 100644 --- a/Recyclable.CollectionsTests/RecyclableStackTests.cs +++ b/Recyclable.CollectionsTests/RecyclableStackTests.cs @@ -703,5 +703,60 @@ namespace Recyclable.CollectionsTests _ = stack.Should().Equal(expected); } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddRangeShouldAddSortedListItemsInCorrectOrder(int itemsCount) + { + long[] data = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var source = new RecyclableSortedList(); + for (int i = 0; i < data.Length; i++) + { + source.Add(i, data[i]); + } + using var stack = new RecyclableStack<(int Key, long Value)>(); + + stack.AddRange(source); + + List<(int Key, long Value)> expected = new(itemsCount); + for (int i = 0; i < data.Length; i++) + { + expected.Add((i, data[i])); + } + expected.Reverse(); + + _ = stack.Count.Should().Be(itemsCount); + _ = stack.Should().Equal(expected); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddRangeShouldNotOverrideItemsFromSortedList(int itemsCount) + { + long[] data = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var source = new RecyclableSortedList(); + for (int i = 0; i < data.Length; i++) + { + source.Add(i, data[i]); + } + using var stack = new RecyclableStack<(int Key, long Value)>(); + + stack.AddRange(source); + stack.AddRange(source); + + List<(int Key, long Value)> expected = new(itemsCount * 2); + for (int i = 0; i < data.Length; i++) + { + expected.Add((i, data[i])); + } + for (int i = 0; i < data.Length; i++) + { + expected.Add((i, data[i])); + } + expected.Reverse(); + + _ = stack.Count.Should().Be(itemsCount * 2); + _ = stack.Should().Equal(expected); + } + } }