From 1aedfb2cbcae090cb2ce81edad4ffb985ed674e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Thu, 17 Jul 2025 00:21:15 +0200 Subject: [PATCH] Optimize long-list AddRange for stack --- .../RecyclableStack.AddRange.cs | 43 +++++++++++++++++-- .../RecyclableStackTests.cs | 38 ++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/Recyclable.Collections/RecyclableStack.AddRange.cs b/Recyclable.Collections/RecyclableStack.AddRange.cs index c6afdab..62057dc 100644 --- a/Recyclable.Collections/RecyclableStack.AddRange.cs +++ b/Recyclable.Collections/RecyclableStack.AddRange.cs @@ -281,27 +281,62 @@ namespace Recyclable.Collections return; } + EnsureCapacity(stack, stack._count + longCount); + int blockSize = items._blockSize; int fullBlocks = (int)(longCount >> items._blockSizePow2BitShift); int lastBlockLength = (int)(longCount & items._blockSizeMinus1); + RecyclableArrayPoolChunk chunk = stack._current; + int index = chunk.Index; + int chunkLength = chunk.Value.Length; + for (int blockIndex = 0; blockIndex < fullBlocks; blockIndex++) { T[] block = items._memoryBlocks[blockIndex]; - for (int i = 0; i < blockSize; i++) + int sourceIndex = 0; + while (sourceIndex < blockSize) { - stack.Push(block[i]); + if (index == chunkLength) + { + chunk.Index = index; + Grow(stack); + chunk = stack._current; + index = chunk.Index; + chunkLength = chunk.Value.Length; + } + + int toCopy = Math.Min(chunkLength - index, blockSize - sourceIndex); + Array.Copy(block, sourceIndex, chunk.Value, index, toCopy); + sourceIndex += toCopy; + index += toCopy; } } if (lastBlockLength > 0) { T[] block = items._memoryBlocks[fullBlocks]; - for (int i = 0; i < lastBlockLength; i++) + int sourceIndex = 0; + while (sourceIndex < lastBlockLength) { - stack.Push(block[i]); + if (index == chunkLength) + { + chunk.Index = index; + Grow(stack); + chunk = stack._current; + index = chunk.Index; + chunkLength = chunk.Value.Length; + } + + int toCopy = Math.Min(chunkLength - index, lastBlockLength - sourceIndex); + Array.Copy(block, sourceIndex, chunk.Value, index, toCopy); + sourceIndex += toCopy; + index += toCopy; } } + + chunk.Index = index; + stack._count += longCount; } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] diff --git a/Recyclable.CollectionsTests/RecyclableStackTests.cs b/Recyclable.CollectionsTests/RecyclableStackTests.cs index 4d91bfc..60d5ffd 100644 --- a/Recyclable.CollectionsTests/RecyclableStackTests.cs +++ b/Recyclable.CollectionsTests/RecyclableStackTests.cs @@ -404,5 +404,43 @@ namespace Recyclable.CollectionsTests _ = stack.Should().Equal(expected); } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddRangeShouldAddLongListItemsInCorrectOrder(int itemsCount) + { + long[] data = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var source = new RecyclableLongList(data); + using var stack = new RecyclableStack(); + + stack.AddRange(source); + + List expected = new(itemsCount); + expected.AddRange(data); + expected.Reverse(); + + _ = stack.Count.Should().Be(itemsCount); + _ = stack.Should().Equal(expected); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddRangeShouldNotOverrideItemsFromLongList(int itemsCount) + { + long[] data = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var source = new RecyclableLongList(data); + using var stack = new RecyclableStack(); + + stack.AddRange(source); + stack.AddRange(source); + + List expected = new(itemsCount * 2); + expected.AddRange(data); + expected.AddRange(data); + expected.Reverse(); + + _ = stack.Count.Should().Be(itemsCount * 2); + _ = stack.Should().Equal(expected); + } + } }