From 3dea5099581cad62032e955b7f4c3006d61e36d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Thu, 17 Jul 2025 15:36:54 +0200 Subject: [PATCH] Add additional AddRange overloads for hash set --- .../RecyclableHashSet.AddRange.cs | 233 ++++++++++++++++++ .../RecyclableHashSetTests.cs | 36 +++ 2 files changed, 269 insertions(+) diff --git a/Recyclable.Collections/RecyclableHashSet.AddRange.cs b/Recyclable.Collections/RecyclableHashSet.AddRange.cs index 3e2da0e..04de693 100644 --- a/Recyclable.Collections/RecyclableHashSet.AddRange.cs +++ b/Recyclable.Collections/RecyclableHashSet.AddRange.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -262,6 +263,90 @@ namespace Recyclable.Collections set._count = insertIndex; } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableHashSet set, in Array items) + where T : notnull + { + int count = items.Length; + if (count == 0) + { + return; + } + + T[] buffer; + int lower = items.GetLowerBound(0); + + if (items is T[] array && lower == 0) + { + buffer = array; + } + else + { + buffer = count >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(checked((int)BitOperations.RoundUpToPowerOf2((uint)count))) + : new T[count]; + + Array.Copy(items, lower, buffer, 0, count); + } + + int requiredCount = set._count + count; + EnsureCapacity(set, requiredCount); + + int bucketsLength = set._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != set._buckets.Length) + { + ResizeBuckets(set, bucketsLength); + } + + int bucketMask = set._buckets.Length - 1; + int insertIndex = set._count; + + for (int i = 0; i < count; i++) + { + T value = buffer[i]; + int hash = value?.GetHashCode() & int.MaxValue ?? 0; + int bucket = hash & bucketMask; + + int index = set._buckets[bucket]; + bool exists = false; + while (index >= 0) + { + ref var check = ref GetEntry(set, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Value, value)) + { + exists = true; + break; + } + + index = check.Next; + } + + if (exists) + { + continue; + } + + ref var entry = ref GetEntry(set, insertIndex); + entry.HashCode = hash; + entry.Value = value!; + entry.Next = set._buckets[bucket]; + set._buckets[bucket] = insertIndex; + insertIndex++; + } + + set._count = insertIndex; + + if (!ReferenceEquals(buffer, items) && count >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(buffer, RecyclableHashSet._needsClearing); + } + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclableHashSet set, List items) where T : notnull @@ -326,6 +411,154 @@ namespace Recyclable.Collections set._count = insertIndex; } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableHashSet set, ICollection items) + where T : notnull + { + 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 requiredCount = set._count + count; + EnsureCapacity(set, requiredCount); + + int bucketsLength = set._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != set._buckets.Length) + { + ResizeBuckets(set, bucketsLength); + } + + int bucketMask = set._buckets.Length - 1; + int insertIndex = set._count; + + for (int i = 0; i < count; i++) + { + T value = buffer[i]; + int hash = value?.GetHashCode() & int.MaxValue ?? 0; + int bucket = hash & bucketMask; + + int index = set._buckets[bucket]; + bool exists = false; + while (index >= 0) + { + ref var check = ref GetEntry(set, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Value, value)) + { + exists = true; + break; + } + + index = check.Next; + } + + if (exists) + { + continue; + } + + ref var entry = ref GetEntry(set, insertIndex); + entry.HashCode = hash; + entry.Value = value!; + entry.Next = set._buckets[bucket]; + set._buckets[bucket] = insertIndex; + insertIndex++; + } + + set._count = insertIndex; + + if (count >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(buffer, RecyclableHashSet._needsClearing); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableHashSet set, ICollection items) + where T : notnull + { + 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 requiredCount = set._count + count; + EnsureCapacity(set, requiredCount); + + int bucketsLength = set._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != set._buckets.Length) + { + ResizeBuckets(set, bucketsLength); + } + + int bucketMask = set._buckets.Length - 1; + int insertIndex = set._count; + + for (int i = 0; i < count; i++) + { + T value = buffer[i]; + int hash = value?.GetHashCode() & int.MaxValue ?? 0; + int bucket = hash & bucketMask; + + int index = set._buckets[bucket]; + bool exists = false; + while (index >= 0) + { + ref var check = ref GetEntry(set, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Value, value)) + { + exists = true; + break; + } + + index = check.Next; + } + + if (exists) + { + continue; + } + + ref var entry = ref GetEntry(set, insertIndex); + entry.HashCode = hash; + entry.Value = value!; + entry.Next = set._buckets[bucket]; + set._buckets[bucket] = insertIndex; + insertIndex++; + } + + set._count = insertIndex; + + if (count >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(buffer, RecyclableHashSet._needsClearing); + } + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] internal static void AddRange(this RecyclableHashSet set, RecyclableList items) where T : notnull diff --git a/Recyclable.CollectionsTests/RecyclableHashSetTests.cs b/Recyclable.CollectionsTests/RecyclableHashSetTests.cs index 88298f5..7768edb 100644 --- a/Recyclable.CollectionsTests/RecyclableHashSetTests.cs +++ b/Recyclable.CollectionsTests/RecyclableHashSetTests.cs @@ -161,5 +161,41 @@ namespace Recyclable.CollectionsTests _ = set.Should().Contain(new[] { 1, 2, 3 }); } + [Fact] + public void AddRangeShouldAddItemsFromArray() + { + using var set = new RecyclableHashSet(); + Array array = new int[] { 1, 2, 3 }; + + set.AddRange(array); + + _ = set.Count.Should().Be(3); + _ = set.Should().Contain(new[] { 1, 2, 3 }); + } + + [Fact] + public void AddRangeShouldAddItemsFromICollection() + { + using var set = new RecyclableHashSet(); + System.Collections.ArrayList list = new System.Collections.ArrayList { 1, 2, 3 }; + + set.AddRange(list); + + _ = set.Count.Should().Be(3); + _ = set.Should().Contain(new[] { 1, 2, 3 }); + } + + [Fact] + public void AddRangeShouldAddItemsFromGenericICollection() + { + using var set = new RecyclableHashSet(); + ICollection list = new List { 1, 2, 3 }; + + set.AddRange(list); + + _ = set.Count.Should().Be(3); + _ = set.Should().Contain(new[] { 1, 2, 3 }); + } + } }