From 78e9373491a102e9ed88578ffddee819037acb9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Thu, 17 Jul 2025 00:21:51 +0200 Subject: [PATCH] Optimize stack AddRange for sorted dictionary --- .../RecyclableStack.AddRange.cs | 95 ++++++++++++++++--- .../RecyclableStackTests.cs | 55 +++++++++++ 2 files changed, 139 insertions(+), 11 deletions(-) diff --git a/Recyclable.Collections/RecyclableStack.AddRange.cs b/Recyclable.Collections/RecyclableStack.AddRange.cs index 435ecff..88ae72f 100644 --- a/Recyclable.Collections/RecyclableStack.AddRange.cs +++ b/Recyclable.Collections/RecyclableStack.AddRange.cs @@ -14,8 +14,10 @@ namespace Recyclable.Collections { 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() { @@ -24,6 +26,7 @@ namespace Recyclable.Collections { Type[] args = elementType.GetGenericArguments(); _dictionaryType = typeof(RecyclableDictionary<,>).MakeGenericType(args); + _sortedDictionaryType = typeof(RecyclableSortedDictionary<,>).MakeGenericType(args); MethodInfo? method = typeof(zRecyclableStackAddRange).GetMethod( nameof(AddRange), BindingFlags.NonPublic | BindingFlags.Static, @@ -37,6 +40,20 @@ namespace Recyclable.Collections var call = Expression.Call(method, stackParam, Expression.Convert(objParam, _dictionaryType)); _dictionaryAdder = Expression.Lambda, object>>(call, stackParam, objParam).Compile(); } + + method = typeof(zRecyclableStackAddRange).GetMethod( + nameof(AddRange), + BindingFlags.NonPublic | BindingFlags.Static, + binder: null, + types: new[] { typeof(RecyclableStack<>).MakeGenericType(elementType), _sortedDictionaryType }, + modifiers: null); + if (method != null) + { + var stackParam = Expression.Parameter(typeof(RecyclableStack)); + var objParam = Expression.Parameter(typeof(object)); + var call = Expression.Call(method, stackParam, Expression.Convert(objParam, _sortedDictionaryType)); + _sortedDictionaryAdder = Expression.Lambda, object>>(call, stackParam, objParam).Compile(); + } } else if (elementType.IsGenericType && elementType.GetGenericTypeDefinition() == typeof(ValueTuple<,>)) { @@ -66,6 +83,12 @@ namespace Recyclable.Collections return true; } + if (_sortedDictionaryAdder != null && _sortedDictionaryType!.IsInstanceOfType(items)) + { + _sortedDictionaryAdder(stack, items); + return true; + } + if (_sortedListAdder != null && _sortedListType!.IsInstanceOfType(items)) { _sortedListAdder(stack, items); @@ -275,7 +298,7 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclableStack stack, RecyclableList items) { - int count = items._count; + int count = items.Count; if (count == 0) { return; @@ -492,7 +515,7 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclableStack stack, RecyclableHashSet items) { - int count = items._count; + int count = items.Count; if (count == 0) { return; @@ -542,7 +565,7 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclableStack stack, RecyclableStack items) { - long longCount = items._count; + long longCount = items.Count; if (longCount == 0) { return; @@ -597,7 +620,7 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclableStack stack, RecyclableSortedSet items) { - int count = items._count; + int count = items.Count; if (count == 0) { return; @@ -638,7 +661,7 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclableStack stack, RecyclableLinkedList items) { - long longCount = items._count; + long longCount = items.Count; if (longCount == 0) { return; @@ -694,7 +717,7 @@ namespace Recyclable.Collections [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclableStack stack, RecyclableQueue items) { - long longCount = items._count; + long longCount = items.Count; if (longCount == 0) { return; @@ -745,7 +768,7 @@ namespace Recyclable.Collections internal static void AddRange(this RecyclableStack> stack, RecyclableDictionary items) where TKey : notnull { - int count = items._count; + int count = items.Count; if (count == 0) { return; @@ -795,10 +818,10 @@ namespace Recyclable.Collections } [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] - internal static void AddRange(this RecyclableStack<(TKey Key, TValue Value)> stack, RecyclableSortedList items) + internal static void AddRange(this RecyclableStack> stack, RecyclableSortedDictionary items) where TKey : notnull { - int count = items._count; + int count = items.Count; if (count == 0) { return; @@ -806,6 +829,58 @@ namespace Recyclable.Collections 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] = new KeyValuePair(items.GetKey(local + i), items.GetValue(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<(TKey Key, TValue Value)> stack, RecyclableSortedList items) + where TKey : notnull + { + int count = items.Count; + if (count == 0) + { + return; + } + + EnsureCapacity(stack, stack._count + count); + + TKey[] keys = items._keys; + TValue[] values = items._values; + RecyclableArrayPoolChunk<(TKey Key, TValue Value)> chunk = stack._current; while (chunk.Previous != null && chunk.Previous.Index < chunk.Previous.Value.Length) { @@ -815,8 +890,6 @@ namespace Recyclable.Collections int index = chunk.Index; int chunkLength = chunk.Value.Length; int copied = 0; - TKey[] keys = items._keys; - TValue[] values = items._values; while (copied < count) { diff --git a/Recyclable.CollectionsTests/RecyclableStackTests.cs b/Recyclable.CollectionsTests/RecyclableStackTests.cs index 276ef10..96cd5ee 100644 --- a/Recyclable.CollectionsTests/RecyclableStackTests.cs +++ b/Recyclable.CollectionsTests/RecyclableStackTests.cs @@ -758,5 +758,60 @@ namespace Recyclable.CollectionsTests _ = stack.Should().Equal(expected); } + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddRangeShouldAddSortedDictionaryItemsInCorrectOrder(int itemsCount) + { + long[] data = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var source = new RecyclableSortedDictionary(); + for (int i = 0; i < data.Length; i++) + { + source.Add(i, data[i]); + } + using var stack = new RecyclableStack>(); + + stack.AddRange(source); + + List> expected = new(itemsCount); + for (int i = 0; i < data.Length; i++) + { + expected.Add(new KeyValuePair(i, data[i])); + } + expected.Reverse(); + + _ = stack.Count.Should().Be(itemsCount); + _ = stack.Should().Equal(expected); + } + + [Theory] + [MemberData(nameof(RecyclableLongListTestData.ItemsCountTestCases), MemberType = typeof(RecyclableLongListTestData))] + public void AddRangeShouldNotOverrideItemsFromSortedDictionary(int itemsCount) + { + long[] data = RecyclableLongListTestData.CreateTestData(itemsCount).ToArray(); + using var source = new RecyclableSortedDictionary(); + for (int i = 0; i < data.Length; i++) + { + source.Add(i, data[i]); + } + using var stack = new RecyclableStack>(); + + stack.AddRange(source); + stack.AddRange(source); + + List> expected = new(itemsCount * 2); + for (int i = 0; i < data.Length; i++) + { + expected.Add(new KeyValuePair(i, data[i])); + } + for (int i = 0; i < data.Length; i++) + { + expected.Add(new KeyValuePair(i, data[i])); + } + expected.Reverse(); + + _ = stack.Count.Should().Be(itemsCount * 2); + _ = stack.Should().Equal(expected); + } + } }