From c24d8d54ea89d6fbfe4c847f88e4f8fbee8119cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Lema=C5=84czyk?= Date: Thu, 17 Jul 2025 08:34:21 +0200 Subject: [PATCH] Add span AddRange for hash set --- .../RecyclableDictionary.AddRange.cs | 1142 +++++++++++++++++ .../RecyclableHashSet.AddRange.cs | 149 +++ .../RecyclableDictionaryTests.cs | 224 ++++ .../RecyclableHashSetTests.cs | 25 + 4 files changed, 1540 insertions(+) create mode 100644 Recyclable.Collections/RecyclableDictionary.AddRange.cs create mode 100644 Recyclable.Collections/RecyclableHashSet.AddRange.cs diff --git a/Recyclable.Collections/RecyclableDictionary.AddRange.cs b/Recyclable.Collections/RecyclableDictionary.AddRange.cs new file mode 100644 index 0000000..74c685f --- /dev/null +++ b/Recyclable.Collections/RecyclableDictionary.AddRange.cs @@ -0,0 +1,1142 @@ +using System; +using System.Collections.Generic; +using System.Collections; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Numerics; +using Recyclable.Collections.Pools; + +namespace Recyclable.Collections +{ + internal static class zRecyclableDictionaryAddRange + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ref RecyclableDictionary.Entry GetEntry(RecyclableDictionary dictionary, int index) + where TKey : notnull + { + return ref dictionary._entries[index >> dictionary._blockShift][index & dictionary._blockSizeMinus1]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EnsureCapacity(RecyclableDictionary dictionary, int min) + where TKey : notnull + { + int requiredBlocks = (min + dictionary._blockSizeMinus1) >> dictionary._blockShift; + if (dictionary._entries.Length < requiredBlocks) + { + var newBlocks = requiredBlocks >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.Entry[]>.RentShared(requiredBlocks) + : new RecyclableDictionary.Entry[requiredBlocks][]; + + Array.Copy(dictionary._entries, newBlocks, dictionary._entries.Length); + + if (dictionary._entries.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.Entry[]>.ReturnShared(dictionary._entries, false); + } + + dictionary._entries = newBlocks; + } + + for (int i = 0; i < requiredBlocks; i++) + { + if (dictionary._entries[i] == null) + { + dictionary._entries[i] = dictionary._blockSize >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.Entry>.RentShared(dictionary._blockSize) + : new RecyclableDictionary.Entry[dictionary._blockSize]; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ResizeBuckets(RecyclableDictionary dictionary, int newSize) + where TKey : notnull + { + var newBuckets = new int[newSize]; + Array.Fill(newBuckets, -1); + for (int i = 0; i < dictionary._count; i++) + { + ref var entry = ref GetEntry(dictionary, i); + int bucket = entry.HashCode & (newSize - 1); + entry.Next = newBuckets[bucket]; + newBuckets[bucket] = i; + } + + dictionary._buckets = newBuckets; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, ReadOnlySpan> items) + where TKey : notnull + { + int count = items.Length; + if (count == 0) + { + return; + } + + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int startIndex = dictionary._count; + + for (int i = 0; i < count; i++) + { + var kvp = items[i]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, startIndex + i); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = startIndex + i; + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, Span> items) + where TKey : notnull + => AddRange(dictionary, (ReadOnlySpan>)items); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, KeyValuePair[] items) + where TKey : notnull + => AddRange(dictionary, (ReadOnlySpan>)items); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, RecyclableDictionary items) + where TKey : notnull + { + int count = items._count; + if (count == 0) + { + return; + } + + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int shift = items._blockShift; + int mask = items._blockSizeMinus1; + var sourceBlocks = items._entries; + int startIndex = dictionary._count; + + for (int i = 0; i < count; i++) + { + ref var sourceEntry = ref sourceBlocks[i >> shift][i & mask]; + TKey key = sourceEntry.Key; + TValue value = sourceEntry.Value; + int hash = sourceEntry.HashCode; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, startIndex + i); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = startIndex + i; + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, in Array items) + where TKey : notnull + { + int count = items.Length; + if (count == 0) + { + return; + } + + KeyValuePair[] buffer; + int lower = items.GetLowerBound(0); + + if (items is KeyValuePair[] array && lower == 0) + { + buffer = array; + } + else + { + buffer = count >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool>.RentShared(checked((int)BitOperations.RoundUpToPowerOf2((uint)count))) + : new KeyValuePair[count]; + + Array.Copy(items, lower, buffer, 0, count); + } + + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int startIndex = dictionary._count; + + for (int i = 0; i < count; i++) + { + var kvp = buffer[i]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, startIndex + i); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = startIndex + i; + } + + dictionary._count = requiredCount; + + if (!ReferenceEquals(buffer, items) && count >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool>.ReturnShared(buffer, RecyclableDictionary._needsClearing); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, List> items) + where TKey : notnull + { + int count = items.Count; + if (count == 0) + { + return; + } + + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int startIndex = dictionary._count; + var span = CollectionsMarshal.AsSpan(items); + + for (int i = 0; i < count; i++) + { + var kvp = span[i]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, startIndex + i); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = startIndex + i; + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, ICollection items) + where TKey : notnull + { + int count = items.Count; + if (count == 0) + { + return; + } + + KeyValuePair[] buffer = count >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool>.RentShared(checked((int)BitOperations.RoundUpToPowerOf2((uint)count))) + : new KeyValuePair[count]; + + items.CopyTo(buffer, 0); + + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int startIndex = dictionary._count; + + for (int i = 0; i < count; i++) + { + var kvp = buffer[i]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, startIndex + i); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = startIndex + i; + } + + dictionary._count = requiredCount; + + if (count >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool>.ReturnShared(buffer, RecyclableDictionary._needsClearing); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, ICollection> items) + where TKey : notnull + { + int count = items.Count; + if (count == 0) + { + return; + } + + KeyValuePair[] buffer = count >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool>.RentShared(checked((int)BitOperations.RoundUpToPowerOf2((uint)count))) + : new KeyValuePair[count]; + + items.CopyTo(buffer, 0); + + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int startIndex = dictionary._count; + + for (int i = 0; i < count; i++) + { + var kvp = buffer[i]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, startIndex + i); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = startIndex + i; + } + + dictionary._count = requiredCount; + + if (count >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool>.ReturnShared(buffer, RecyclableDictionary._needsClearing); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, RecyclableList> items) + where TKey : notnull + { + int count = items._count; + if (count == 0) + { + return; + } + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int startIndex = dictionary._count; + var source = items._memoryBlock; + + for (int i = 0; i < count; i++) + { + var kvp = source[i]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, startIndex + i); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = startIndex + i; + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, RecyclableLongList> items) + where TKey : notnull + { + long longCount = items._longCount; + if (longCount == 0) + { + return; + } + + if (longCount > int.MaxValue - dictionary._count) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(items), $"The total number of items exceeds {int.MaxValue}"); + } + + int count = (int)longCount; + int blockSize = items._blockSize; + int fullBlocks = (int)(longCount >> items._blockSizePow2BitShift); + int lastBlockLength = (int)(longCount & items._blockSizeMinus1); + + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int destIndex = dictionary._count; + var blocks = items._memoryBlocks; + + for (int block = 0; block < fullBlocks; block++) + { + var source = blocks[block]; + for (int j = 0; j < blockSize; j++) + { + var kvp = source[j]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, destIndex); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = destIndex++; + } + } + + if (lastBlockLength > 0) + { + var source = blocks[fullBlocks]; + for (int j = 0; j < lastBlockLength; j++) + { + var kvp = source[j]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, destIndex); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = destIndex++; + } + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, RecyclableSortedDictionary items) + where TKey : notnull + { + int count = items.Count; + if (count == 0) + { + return; + } + + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int startIndex = dictionary._count; + + for (int i = 0; i < count; i++) + { + TKey key = items.GetKey(i); + TValue value = items.GetValue(i); + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, startIndex + i); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = startIndex + i; + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, RecyclableSortedList items) + where TKey : notnull + { + int count = items._count; + if (count == 0) + { + return; + } + + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int startIndex = dictionary._count; + TKey[] keys = items._keys; + TValue[] values = items._values; + + for (int i = 0; i < count; i++) + { + TKey key = keys[i]; + TValue value = values[i]; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, startIndex + i); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = startIndex + i; + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, RecyclableQueue> items) + where TKey : notnull + { + long longCount = items._count; + if (longCount == 0) + { + return; + } + + if (longCount > int.MaxValue - dictionary._count) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(items), $"The total number of items exceeds {int.MaxValue}"); + } + + int count = (int)longCount; + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int destIndex = dictionary._count; + + var chunk = items._head; + while (chunk != null) + { + int sourceIndex = chunk.Bottom; + int sourceTop = chunk.Top; + var source = chunk.Value; + + while (sourceIndex < sourceTop) + { + var kvp = source[sourceIndex++]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, destIndex); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = destIndex++; + } + + chunk = chunk.Next; + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, RecyclableStack> items) + where TKey : notnull + { + long longCount = items._count; + if (longCount == 0) + { + return; + } + + if (longCount > int.MaxValue - dictionary._count) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(items), $"The total number of items exceeds {int.MaxValue}"); + } + + int count = (int)longCount; + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int destIndex = dictionary._count; + + var sourceChunk = items._current; + while (sourceChunk.Previous != null) + { + sourceChunk = sourceChunk.Previous; + } + + while (sourceChunk != null) + { + var source = sourceChunk.Value; + int sourceCount = sourceChunk.Index; + int sourceIndex = 0; + + while (sourceIndex < sourceCount) + { + var kvp = source[sourceIndex++]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, destIndex); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = destIndex++; + } + + sourceChunk = sourceChunk.Next; + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, RecyclableLinkedList> items) + where TKey : notnull + { + long longCount = items._count; + if (longCount == 0) + { + return; + } + + if (longCount > int.MaxValue - dictionary._count) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(items), $"The total number of items exceeds {int.MaxValue}"); + } + + int count = (int)longCount; + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int destIndex = dictionary._count; + + var chunk = items._head; + while (chunk != null) + { + int sourceIndex = chunk.Bottom; + int sourceTop = chunk.Top; + var source = chunk.Value; + + while (sourceIndex < sourceTop) + { + var kvp = source[sourceIndex++]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, destIndex); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = destIndex++; + } + + chunk = chunk.Next; + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, RecyclablePriorityQueue> items) + where TKey : notnull + { + int count = items._size; + if (count == 0) + { + return; + } + + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int startIndex = dictionary._count; + var source = items._heap; + + for (int i = 0; i < count; i++) + { + var kvp = source[i]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, startIndex + i); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = startIndex + i; + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, IReadOnlyList> items) + where TKey : notnull + { + int count = items.Count; + if (count == 0) + { + return; + } + + int requiredCount = dictionary._count + count; + EnsureCapacity(dictionary, requiredCount); + + int bucketsLength = dictionary._buckets.Length; + while (requiredCount >= (bucketsLength * 3) / 4) + { + bucketsLength <<= 1; + } + + if (bucketsLength != dictionary._buckets.Length) + { + ResizeBuckets(dictionary, bucketsLength); + } + + int bucketMask = dictionary._buckets.Length - 1; + int startIndex = dictionary._count; + + for (int i = 0; i < count; i++) + { + var kvp = items[i]; + TKey key = kvp.Key; + TValue value = kvp.Value; + int hash = key.GetHashCode() & int.MaxValue; + int bucket = hash & bucketMask; + + int index = dictionary._buckets[bucket]; + while (index >= 0) + { + ref var check = ref GetEntry(dictionary, index); + if (check.HashCode == hash && EqualityComparer.Default.Equals(check.Key, key)) + { + throw new ArgumentException("An element with the same key already exists.", nameof(items)); + } + + index = check.Next; + } + + ref var entry = ref GetEntry(dictionary, startIndex + i); + entry.HashCode = hash; + entry.Key = key; + entry.Value = value; + entry.Next = dictionary._buckets[bucket]; + dictionary._buckets[bucket] = startIndex + i; + } + + dictionary._count = requiredCount; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableDictionary dictionary, IEnumerable> items) + where TKey : notnull + { + if (items is RecyclableDictionary recyclableDictionary) + { + AddRange(dictionary, recyclableDictionary); + } + else if (items is RecyclableList> recyclableList) + { + AddRange(dictionary, recyclableList); + } + else if (items is RecyclableLongList> recyclableLongList) + { + AddRange(dictionary, recyclableLongList); + } + else if (items is KeyValuePair[] array) + { + AddRange(dictionary, array); + } + else if (items is List> list) + { + AddRange(dictionary, list); + } + else if (items is ICollection> iCollection) + { + AddRange(dictionary, iCollection); + } + else if (items is ICollection collection) + { + AddRange(dictionary, collection); + } + else if (items is IReadOnlyList> readOnlyList) + { + AddRange(dictionary, readOnlyList); + } + else + { + foreach (var kvp in items) + { + dictionary.Add(kvp.Key, kvp.Value); + } + } + } + } +} diff --git a/Recyclable.Collections/RecyclableHashSet.AddRange.cs b/Recyclable.Collections/RecyclableHashSet.AddRange.cs new file mode 100644 index 0000000..e963aac --- /dev/null +++ b/Recyclable.Collections/RecyclableHashSet.AddRange.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Numerics; +using Recyclable.Collections.Pools; + +namespace Recyclable.Collections +{ + internal static class zRecyclableHashSetAddRange + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ref RecyclableHashSet.Entry GetEntry(RecyclableHashSet set, int index) + where T : notnull + { + return ref set._entries[index >> set._blockShift][index & set._blockSizeMinus1]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void EnsureCapacity(RecyclableHashSet set, int min) + where T : notnull + { + int requiredBlocks = (min + set._blockSizeMinus1) >> set._blockShift; + if (set._entries.Length < requiredBlocks) + { + var newBlocks = requiredBlocks >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.Entry[]>.RentShared(requiredBlocks) + : new RecyclableHashSet.Entry[requiredBlocks][]; + + Array.Copy(set._entries, newBlocks, set._entries.Length); + + if (set._entries.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.Entry[]>.ReturnShared(set._entries, false); + } + + set._entries = newBlocks; + } + + for (int i = 0; i < requiredBlocks; i++) + { + if (set._entries[i] == null) + { + set._entries[i] = set._blockSize >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.Entry>.RentShared(set._blockSize) + : new RecyclableHashSet.Entry[set._blockSize]; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ResizeBuckets(RecyclableHashSet set, int newSize) + where T : notnull + { + int[] newBuckets = newSize >= RecyclableDefaults.MinPooledArrayLength + ? RecyclableArrayPool.RentShared(newSize) + : new int[newSize]; + Array.Fill(newBuckets, -1); + + for (int i = 0; i < set._count; i++) + { + ref var entry = ref GetEntry(set, i); + int bucket = entry.HashCode & (newSize - 1); + entry.Next = newBuckets[bucket]; + newBuckets[bucket] = i; + } + + if (set._buckets.Length >= RecyclableDefaults.MinPooledArrayLength) + { + RecyclableArrayPool.ReturnShared(set._buckets, false); + } + + set._buckets = newBuckets; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableHashSet set, ReadOnlySpan items) + where T : notnull + { + int count = items.Length; + if (count == 0) + { + return; + } + + 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 = items[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; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableHashSet set, Span items) + where T : notnull + => AddRange(set, (ReadOnlySpan)items); + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + internal static void AddRange(this RecyclableHashSet set, T[] items) + where T : notnull + => AddRange(set, (ReadOnlySpan)items); + } +} diff --git a/Recyclable.CollectionsTests/RecyclableDictionaryTests.cs b/Recyclable.CollectionsTests/RecyclableDictionaryTests.cs index 3aebdf3..5f9399e 100644 --- a/Recyclable.CollectionsTests/RecyclableDictionaryTests.cs +++ b/Recyclable.CollectionsTests/RecyclableDictionaryTests.cs @@ -95,5 +95,229 @@ namespace Recyclable.CollectionsTests var expected = RecyclableLongListTestData.CreateTestData(itemsCount).ToList(); _ = actual.Should().ContainInConsecutiveOrder(expected); } + + [Fact] + public void AddRangeShouldAddItemsFromSpan() + { + using var dict = new RecyclableDictionary(); + var data = new[] + { + new KeyValuePair(1, "a"), + new KeyValuePair(2, "b"), + new KeyValuePair(3, "c"), + }; + + dict.AddRange(data); + + _ = dict.Count.Should().Be(3); + for (int i = 0; i < 3; i++) + { + dict.GetKey(i).Should().Be(i + 1); + dict.GetValue(i).Should().Be(data[i].Value); + } + } + + [Fact] + public void AddRangeShouldAddItemsFromDictionary() + { + using var source = new RecyclableDictionary(); + source.Add(1, "a"); + source.Add(2, "b"); + using var dict = new RecyclableDictionary(); + + dict.AddRange(source); + + _ = dict.Count.Should().Be(2); + dict.GetKey(0).Should().Be(1); + dict.GetValue(0).Should().Be("a"); + dict.GetKey(1).Should().Be(2); + dict.GetValue(1).Should().Be("b"); + } + + [Fact] + public void AddRangeShouldAddListItemsInCorrectOrder() + { + var data = new[] + { + new KeyValuePair(1, "a"), + new KeyValuePair(2, "b"), + new KeyValuePair(3, "c"), + }; + + using var source = new RecyclableList>(data); + using var dict = new RecyclableDictionary(); + + dict.AddRange(source); + + _ = dict.Count.Should().Be(3); + for (int i = 0; i < 3; i++) + { + dict.GetKey(i).Should().Be(data[i].Key); + dict.GetValue(i).Should().Be(data[i].Value); + } + } + + [Fact] + public void AddRangeShouldAddLongListItemsInCorrectOrder() + { + var data = new[] + { + new KeyValuePair(1, "a"), + new KeyValuePair(2, "b"), + new KeyValuePair(3, "c"), + }; + + using var source = new RecyclableLongList>(data); + using var dict = new RecyclableDictionary(); + + dict.AddRange(source); + + _ = dict.Count.Should().Be(3); + for (int i = 0; i < 3; i++) + { + dict.GetKey(i).Should().Be(data[i].Key); + dict.GetValue(i).Should().Be(data[i].Value); + } + } + + [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 dict = new RecyclableDictionary(); + + dict.AddRange(source); + + _ = dict.Count.Should().Be(itemsCount); + for (int i = 0; i < data.Length; i++) + { + dict.GetKey(i).Should().Be(i); + dict.GetValue(i).Should().Be(data[i]); + } + } + + [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 dict = new RecyclableDictionary(); + + dict.AddRange(source); + + _ = dict.Count.Should().Be(itemsCount); + for (int i = 0; i < data.Length; i++) + { + dict.GetKey(i).Should().Be(i); + dict.GetValue(i).Should().Be(data[i]); + } + } + + [Fact] + public void AddRangeShouldAddQueueItemsInCorrectOrder() + { + var data = new[] + { + new KeyValuePair(1, "a"), + new KeyValuePair(2, "b"), + new KeyValuePair(3, "c"), + }; + + using var source = new RecyclableQueue>(data); + using var dict = new RecyclableDictionary(); + + dict.AddRange(source); + + _ = dict.Count.Should().Be(3); + for (int i = 0; i < 3; i++) + { + dict.GetKey(i).Should().Be(data[i].Key); + dict.GetValue(i).Should().Be(data[i].Value); + } + } + + [Fact] + public void AddRangeShouldAddStackItemsInCorrectOrder() + { + var data = new[] + { + new KeyValuePair(1, "a"), + new KeyValuePair(2, "b"), + new KeyValuePair(3, "c"), + }; + + using var source = new RecyclableStack>(data); + using var dict = new RecyclableDictionary(); + + dict.AddRange(source); + + _ = dict.Count.Should().Be(3); + for (int i = 0; i < 3; i++) + { + dict.GetKey(i).Should().Be(data[i].Key); + dict.GetValue(i).Should().Be(data[i].Value); + } + } + + [Fact] + public void AddRangeShouldAddLinkedListItemsInCorrectOrder() + { + var data = new[] + { + new KeyValuePair(1, "a"), + new KeyValuePair(2, "b"), + new KeyValuePair(3, "c"), + }; + + using var source = new RecyclableLinkedList>(data); + using var dict = new RecyclableDictionary(); + + dict.AddRange(source); + + _ = dict.Count.Should().Be(3); + for (int i = 0; i < 3; i++) + { + dict.GetKey(i).Should().Be(data[i].Key); + dict.GetValue(i).Should().Be(data[i].Value); + } + } + + [Fact] + public void AddRangeShouldAddPriorityQueueItemsInCorrectOrder() + { + var data = new[] + { + new KeyValuePair(1, "a"), + new KeyValuePair(2, "b"), + new KeyValuePair(3, "c"), + }; + + using var source = new RecyclablePriorityQueue>(comparer: Comparer>.Create((x, y) => x.Key.CompareTo(y.Key))); + foreach (var kvp in data) + { + source.Enqueue(kvp); + } + using var dict = new RecyclableDictionary(); + + dict.AddRange(source); + + _ = dict.Count.Should().Be(3); + for (int i = 0; i < 3; i++) + { + dict.GetKey(i).Should().Be(data[i].Key); + dict.GetValue(i).Should().Be(data[i].Value); + } + } } } diff --git a/Recyclable.CollectionsTests/RecyclableHashSetTests.cs b/Recyclable.CollectionsTests/RecyclableHashSetTests.cs index 9574c9e..ec8b76b 100644 --- a/Recyclable.CollectionsTests/RecyclableHashSetTests.cs +++ b/Recyclable.CollectionsTests/RecyclableHashSetTests.cs @@ -76,5 +76,30 @@ namespace Recyclable.CollectionsTests _ = actual.Should().Contain(expected).And.HaveCount(itemsCount); } } + + [Fact] + public void AddRangeShouldAddItemsFromSpan() + { + using var set = new RecyclableHashSet(); + var data = new[] { 1, 2, 3 }; + + set.AddRange(data); + + _ = set.Count.Should().Be(3); + _ = set.Should().ContainInOrder(data); + } + + [Fact] + public void AddRangeShouldIgnoreDuplicates() + { + using var set = new RecyclableHashSet(); + var data = new[] { 1, 2, 3 }; + + set.AddRange(data); + set.AddRange(data); + + _ = set.Count.Should().Be(3); + _ = set.Should().ContainInOrder(data); + } } }