From 2546d6bf3ffa9cbbbaebaa8874c6620ea30422cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Thu, 17 Jul 2025 00:21:30 +0200 Subject: [PATCH] Optimize hash set and stack AddRange for RecyclableStack --- .../RecyclableStack.AddRange.cs | 81 +++++++++++++++--- .../RecyclableStackTests.cs | 84 +++++++++++++++++++ 2 files changed, 155 insertions(+), 10 deletions(-) diff --git a/Recyclable.Collections/RecyclableStack.AddRange.cs b/Recyclable.Collections/RecyclableStack.AddRange.cs index 33326fb..e2b010c 100644 --- a/Recyclable.Collections/RecyclableStack.AddRange.cs +++ b/Recyclable.Collections/RecyclableStack.AddRange.cs @@ -400,37 +400,98 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclableStack stack, RecyclableHashSet items) { + int count = items._count; + if (count == 0) + { + return; + } + + EnsureCapacity(stack, stack._count + count); + var entries = items._entries; int shift = items._blockShift, mask = items._blockSizeMinus1; - for (int i = 0; i < items._count; i++) + + RecyclableArrayPoolChunk chunk = stack._current; + int index = chunk.Index; + int chunkLength = chunk.Value.Length; + int copied = 0; + + while (copied < count) { - stack.Push(entries[i >> shift][i & mask].Value); + if (index == chunkLength) + { + chunk.Index = index; + Grow(stack); + chunk = stack._current; + index = chunk.Index; + chunkLength = chunk.Value.Length; + } + + int toCopy = Math.Min(chunkLength - index, count - copied); + var destination = chunk.Value; + for (int i = 0; i < toCopy; i++) + { + destination[index + i] = entries[(copied + i) >> shift][(copied + i) & mask].Value; + } + + copied += toCopy; + index += toCopy; } + + chunk.Index = index; + stack._count += count; } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclableStack stack, RecyclableStack items) { - if (items._count == 0) + long longCount = items._count; + if (longCount == 0) { return; } - RecyclableArrayPoolChunk chunk = items._current; - while (chunk.Previous != null) + EnsureCapacity(stack, stack._count + longCount); + + RecyclableArrayPoolChunk destChunk = stack._current; + int destIndex = destChunk.Index; + int destLength = destChunk.Value.Length; + + RecyclableArrayPoolChunk sourceChunk = items._current; + while (sourceChunk.Previous != null) { - chunk = chunk.Previous; + sourceChunk = sourceChunk.Previous; } - while (chunk != null) + while (sourceChunk != null) { - for (int i = 0; i < chunk.Index; i++) + T[] source = sourceChunk.Value; + int sourceCount = sourceChunk.Index; + int sourceIndex = 0; + + while (sourceIndex < sourceCount) { - stack.Push(chunk.Value[i]); + if (destIndex == destLength) + { + destChunk.Index = destIndex; + Grow(stack); + destChunk = stack._current; + destIndex = destChunk.Index; + destLength = destChunk.Value.Length; + } + + int toCopy = Math.Min(destLength - destIndex, sourceCount - sourceIndex); + Array.Copy(source, sourceIndex, destChunk.Value, destIndex, toCopy); + sourceIndex += toCopy; + destIndex += toCopy; } - chunk = chunk.Next!; + + sourceChunk = sourceChunk.Next!; } + + destChunk.Index = destIndex; + stack._count += longCount; } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] diff --git a/Recyclable.CollectionsTests/RecyclableStackTests.cs b/Recyclable.CollectionsTests/RecyclableStackTests.cs index 60d5ffd..0d878bf 100644 --- a/Recyclable.CollectionsTests/RecyclableStackTests.cs +++ b/Recyclable.CollectionsTests/RecyclableStackTests.cs @@ -442,5 +442,89 @@ namespace Recyclable.CollectionsTests _ = stack.Should().Equal(expected); } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddRangeShouldAddHashSetItemsInCorrectOrder(int itemsCount) + { + long[] data = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var source = new RecyclableHashSet(); + foreach (long item in data) + { + source.Add(item); + } + 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 AddRangeShouldNotOverrideItemsFromHashSet(int itemsCount) + { + long[] data = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var source = new RecyclableHashSet(); + foreach (long item in data) + { + source.Add(item); + } + 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); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddRangeShouldAddStackItemsInCorrectOrder(int itemsCount) + { + long[] data = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var source = new RecyclableStack(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 AddRangeShouldNotOverrideItemsFromStack(int itemsCount) + { + long[] data = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var source = new RecyclableStack(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); + } + } }