Faster optimized frozen dictionary creation (6/6) (#88093)
authorAdam Sitnik <adam.sitnik@gmail.com>
Wed, 5 Jul 2023 07:45:54 +0000 (09:45 +0200)
committerGitHub <noreply@github.com>
Wed, 5 Jul 2023 07:45:54 +0000 (09:45 +0200)
* don't use custom ToArray for small frozen collections, up to 50% gain for creation time for collections with <= 4 items

* for these types GetHashCode returns their value casted to int, so when we receive a Dictionary/HashSet where there are key we know that all hash codes are unique and we can avoid some work later

10-15% CPU time gain and 15-20% allocation reduction for FrozenDictionary and FrozenHashSet where TKey is uint, short, ushort, byte, sbyte

* move Length Buckets code to a dedicated helper type to reduce code duplication and decrease code size

* add tests for Frozen Dictionaries with key being uint, short, ushort, byte, sbyte, nint and nuint

* fix discovered bug: IntPtr started implementing IComparable<IntPtr> in .NET 5

14 files changed:
src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs
src/libraries/Common/tests/System/Collections/IDictionary.Generic.Tests.cs
src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Constants.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ItemsFrozenSet.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/KeysAndValuesFrozenDictionary.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallFrozenDictionary.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallFrozenSet.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBuckets.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBucketsFrozenDictionary.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBucketsFrozenSet.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenDictionary.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/ValueTypeDefaultComparerFrozenSet.cs
src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTests.cs

index d01bb5c..a94a930 100644 (file)
@@ -406,8 +406,10 @@ namespace System.Collections.Tests
         public void ICollection_Generic_Contains_DefaultValueOnCollectionNotContainingDefaultValue(int count)
         {
             ICollection<T> collection = GenericICollectionFactory(count);
-            if (DefaultValueAllowed)
+            if (DefaultValueAllowed && default(T) is null) // it's true only for reference types and for Nullable<T>
+            {
                 Assert.False(collection.Contains(default(T)));
+            }
         }
 
         [Theory]
index 22f3912..df92ab2 100644 (file)
@@ -289,7 +289,7 @@ namespace System.Collections.Tests
         [MemberData(nameof(ValidCollectionSizes))]
         public void IDictionary_Generic_ItemGet_MissingDefaultKey_ThrowsKeyNotFoundException(int count)
         {
-            if (DefaultValueAllowed)
+            if (DefaultValueAllowed && !IsReadOnly)
             {
                 IDictionary<TKey, TValue> dictionary = GenericIDictionaryFactory(count);
                 TKey missingKey = default(TKey);
@@ -733,11 +733,14 @@ namespace System.Collections.Tests
             IDictionary<TKey, TValue> dictionary = GenericIDictionaryFactory(count);
             if (DefaultValueAllowed)
             {
-                // returns false
-                TKey missingKey = default(TKey);
-                while (dictionary.ContainsKey(missingKey))
-                    dictionary.Remove(missingKey);
-                Assert.False(dictionary.ContainsKey(missingKey));
+                if (!IsReadOnly)
+                {
+                    // returns false
+                    TKey missingKey = default(TKey);
+                    while (dictionary.ContainsKey(missingKey))
+                        dictionary.Remove(missingKey);
+                    Assert.False(dictionary.ContainsKey(missingKey));
+                }
             }
             else
             {
@@ -934,10 +937,13 @@ namespace System.Collections.Tests
             TValue outValue;
             if (DefaultValueAllowed)
             {
-                TKey missingKey = default(TKey);
-                while (dictionary.ContainsKey(missingKey))
-                    dictionary.Remove(missingKey);
-                Assert.False(dictionary.TryGetValue(missingKey, out outValue));
+                if (!IsReadOnly)
+                {
+                    TKey missingKey = default(TKey);
+                    while (dictionary.ContainsKey(missingKey))
+                        dictionary.Remove(missingKey);
+                    Assert.False(dictionary.TryGetValue(missingKey, out outValue));
+                }
             }
             else
             {
index 0e0d453..62da301 100644 (file)
@@ -38,6 +38,7 @@ The System.Collections.Immutable library is built-in as part of the shared frame
     <Compile Include="System\Collections\Frozen\Int32\Int32FrozenSet.cs" />
     <Compile Include="System\Collections\Frozen\String\Hashing.cs" />
     <Compile Include="System\Collections\Frozen\String\KeyAnalyzer.cs" />
+    <Compile Include="System\Collections\Frozen\String\LengthBuckets.cs" />
     <Compile Include="System\Collections\Frozen\String\LengthBucketsFrozenDictionary.cs" />
     <Compile Include="System\Collections\Frozen\String\LengthBucketsFrozenSet.cs" />
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary.cs" />
index 55e654a..5371a03 100644 (file)
@@ -53,8 +53,6 @@ namespace System.Collections.Frozen
             typeof(T) == typeof(uint) ||
             typeof(T) == typeof(long) ||
             typeof(T) == typeof(ulong) ||
-            typeof(T) == typeof(nint) ||
-            typeof(T) == typeof(nuint) ||
             typeof(T) == typeof(decimal) ||
             typeof(T) == typeof(float) ||
             typeof(T) == typeof(double) ||
@@ -68,6 +66,8 @@ namespace System.Collections.Frozen
 #endif
 #if NET5_0_OR_GREATER
             typeof(T) == typeof(Half) ||
+            typeof(T) == typeof(nint) ||
+            typeof(T) == typeof(nuint) ||
 #endif
 #if NET6_0_OR_GREATER
             typeof(T) == typeof(DateOnly) ||
@@ -78,5 +78,13 @@ namespace System.Collections.Frozen
             typeof(T) == typeof(UInt128) ||
 #endif
             typeof(T).IsEnum;
+
+        // for these types GetHashCode returns their value casted to int, so when we receive a Dictionary/HashSet where there are key
+        // we know that all hash codes are unique and we can avoid some work later
+        internal static bool KeysAreHashCodes<T>()
+            => typeof(T) == typeof(int) || typeof(T) == typeof(uint)
+            || typeof(T) == typeof(short) || typeof(T) == typeof(ushort)
+            || typeof(T) == typeof(byte) || typeof(T) == typeof(sbyte)
+            || ((typeof(T) == typeof(nint) || typeof(T) == typeof(nuint)) && IntPtr.Size == 4);
     }
 }
index 787899b..f94cbd4 100644 (file)
@@ -14,7 +14,7 @@ namespace System.Collections.Frozen
         private protected readonly FrozenHashTable _hashTable;
         private protected readonly T[] _items;
 
-        protected ItemsFrozenSet(HashSet<T> source) : base(source.Comparer)
+        protected ItemsFrozenSet(HashSet<T> source, bool keysAreHashCodes = false) : base(source.Comparer)
         {
             Debug.Assert(source.Count != 0);
 
@@ -30,7 +30,7 @@ namespace System.Collections.Frozen
                 hashCodes[i] = entries[i] is T t ? Comparer.GetHashCode(t) : 0;
             }
 
-            _hashTable = FrozenHashTable.Create(hashCodes);
+            _hashTable = FrozenHashTable.Create(hashCodes, keysAreHashCodes);
 
             for (int srcIndex = 0; srcIndex < hashCodes.Length; srcIndex++)
             {
index 6435eb1..5e76b58 100644 (file)
@@ -15,7 +15,7 @@ namespace System.Collections.Frozen
         private protected readonly TKey[] _keys;
         private protected readonly TValue[] _values;
 
-        protected KeysAndValuesFrozenDictionary(Dictionary<TKey, TValue> source) : base(source.Comparer)
+        protected KeysAndValuesFrozenDictionary(Dictionary<TKey, TValue> source, bool keysAreHashCodes = false) : base(source.Comparer)
         {
             Debug.Assert(source.Count != 0);
 
@@ -32,7 +32,7 @@ namespace System.Collections.Frozen
                 hashCodes[i] = Comparer.GetHashCode(entries[i].Key);
             }
 
-            _hashTable = FrozenHashTable.Create(hashCodes);
+            _hashTable = FrozenHashTable.Create(hashCodes, keysAreHashCodes);
 
             for (int srcIndex = 0; srcIndex < hashCodes.Length; srcIndex++)
             {
index 733e028..317b7c2 100644 (file)
@@ -4,6 +4,7 @@
 using System.Collections.Generic;
 using System.Collections.Immutable;
 using System.Diagnostics;
+using System.Linq;
 using System.Runtime.CompilerServices;
 
 namespace System.Collections.Frozen
@@ -24,8 +25,8 @@ namespace System.Collections.Frozen
         {
             Debug.Assert(source.Count != 0);
 
-            _keys = source.Keys.ToArray(source.Count);
-            _values = source.Values.ToArray(source.Count);
+            _keys = source.Keys.ToArray();
+            _values = source.Values.ToArray();
         }
 
         private protected override TKey[] KeysCore => _keys;
index d5a2c86..53b89d7 100644 (file)
@@ -18,7 +18,7 @@ namespace System.Collections.Frozen
 
         internal SmallFrozenSet(HashSet<T> source) : base(source.Comparer)
         {
-            _items = source.ToArray(source.Count);
+            _items = source.ToArray();
         }
 
         private protected override T[] ItemsCore => _items;
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBuckets.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/LengthBuckets.cs
new file mode 100644 (file)
index 0000000..97659c5
--- /dev/null
@@ -0,0 +1,102 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace System.Collections.Frozen
+{
+    internal static class LengthBuckets
+    {
+        /// <summary>The maximum number of items allowed per bucket.  The larger the value, the longer it can take to search a bucket, which is sequentially examined.</summary>
+        internal const int MaxPerLength = 5;
+        /// <summary>Allowed ratio between buckets with values and total buckets.  Under this ratio, this implementation won't be used due to too much wasted space.</summary>
+        private const double EmptyLengthsRatio = 0.2;
+
+        internal static int[]? CreateLengthBucketsArrayIfAppropriate(string[] keys, IEqualityComparer<string> comparer, int minLength, int maxLength)
+        {
+            Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
+            Debug.Assert(minLength >= 0 && maxLength >= minLength);
+
+            // If without even looking at the keys we know that some bucket will exceed the max per-bucket
+            // limit (pigeon hole principle), we can early-exit out without doing any further work.
+            int spread = maxLength - minLength + 1;
+            if (keys.Length / spread > MaxPerLength)
+            {
+                return null;
+            }
+
+            int arraySize = spread * MaxPerLength;
+#if NET6_0_OR_GREATER
+            if (arraySize > Array.MaxLength)
+#else
+            if (arraySize > 0X7FFFFFC7)
+#endif
+            {
+                // In the future we may lower the value, as it may be quite unlikely
+                // to have a LOT of strings of different sizes.
+                return null;
+            }
+
+            // Instead of creating a dictionary of lists or a multi-dimensional array
+            // we rent a single dimension array, where every bucket has five slots.
+            // The bucket starts at (key.Length - minLength) * 5.
+            // Each value is an index of the key from _keys array
+            // or just -1, which represents "null".
+            int[] buckets = ArrayPool<int>.Shared.Rent(arraySize);
+            buckets.AsSpan(0, arraySize).Fill(-1);
+
+            int nonEmptyCount = 0;
+            for (int i = 0; i < keys.Length; i++)
+            {
+                string key = keys[i];
+                int startIndex = (key.Length - minLength) * MaxPerLength;
+                int endIndex = startIndex + MaxPerLength;
+                int index = startIndex;
+
+                while (index < endIndex)
+                {
+                    ref int bucket = ref buckets[index];
+                    if (bucket < 0)
+                    {
+                        if (index == startIndex)
+                        {
+                            nonEmptyCount++;
+                        }
+
+                        bucket = i;
+                        break;
+                    }
+
+                    index++;
+                }
+
+                if (index == endIndex)
+                {
+                    // If we've already hit the max per-bucket limit, bail.
+                    ArrayPool<int>.Shared.Return(buckets);
+                    return null;
+                }
+            }
+
+            // If there would be too much empty space in the lookup array, bail.
+            if (nonEmptyCount / (double)spread < EmptyLengthsRatio)
+            {
+                ArrayPool<int>.Shared.Return(buckets);
+                return null;
+            }
+
+#if NET6_0_OR_GREATER
+            // We don't need an array with every value initialized to zero if we are just about to overwrite every value anyway.
+            int[] copy = GC.AllocateUninitializedArray<int>(arraySize);
+            Array.Copy(buckets, copy, arraySize);
+#else
+            int[] copy = buckets.AsSpan(0, arraySize).ToArray();
+#endif
+            ArrayPool<int>.Shared.Return(buckets);
+
+            return copy;
+        }
+    }
+}
index 3e335bd..6942fac 100644 (file)
@@ -1,7 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Buffers;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
@@ -11,11 +10,6 @@ namespace System.Collections.Frozen
     /// <summary>Provides a frozen dictionary implementation where strings are grouped by their lengths.</summary>
     internal sealed class LengthBucketsFrozenDictionary<TValue> : FrozenDictionary<string, TValue>
     {
-        /// <summary>Allowed ratio between buckets with values and total buckets.  Under this ratio, this implementation won't be used due to too much wasted space.</summary>
-        private const double EmptyLengthsRatio = 0.2;
-        /// <summary>The maximum number of items allowed per bucket.  The larger the value, the longer it can take to search a bucket, which is sequentially examined.</summary>
-        private const int MaxPerLength = 5;
-
         private readonly int[] _lengthBuckets;
         private readonly int _minLength;
         private readonly string[] _keys;
@@ -39,87 +33,14 @@ namespace System.Collections.Frozen
             string[] keys, TValue[] values, IEqualityComparer<string> comparer, int minLength, int maxLength)
         {
             Debug.Assert(keys.Length != 0 && keys.Length == values.Length);
-            Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
-            Debug.Assert(minLength >= 0 && maxLength >= minLength);
 
-            // If without even looking at the keys we know that some bucket will exceed the max per-bucket
-            // limit (pigeon hole principle), we can early-exit out without doing any further work.
-            int spread = maxLength - minLength + 1;
-            if (keys.Length / spread > MaxPerLength)
+            int[]? lengthBuckets = LengthBuckets.CreateLengthBucketsArrayIfAppropriate(keys, comparer, minLength, maxLength);
+            if (lengthBuckets is null)
             {
                 return null;
             }
 
-            int arraySize = spread * MaxPerLength;
-#if NET6_0_OR_GREATER
-            if (arraySize > Array.MaxLength)
-#else
-            if (arraySize > 0X7FFFFFC7)
-#endif
-            {
-                // In the future we may lower the value, as it may be quite unlikely
-                // to have a LOT of strings of different sizes.
-                return null;
-            }
-
-            // Instead of creating a dictionary of lists or a multi-dimensional array
-            // we rent a single dimension array, where every bucket has five slots.
-            // The bucket starts at (key.Length - minLength) * 5.
-            // Each value is an index of the key from _keys array
-            // or just -1, which represents "null".
-            int[] buckets = ArrayPool<int>.Shared.Rent(arraySize);
-            buckets.AsSpan(0, arraySize).Fill(-1);
-
-            int nonEmptyCount = 0;
-            for (int i = 0; i < keys.Length; i++)
-            {
-                string key = keys[i];
-                int startIndex = (key.Length - minLength) * MaxPerLength;
-                int endIndex = startIndex + MaxPerLength;
-                int index = startIndex;
-
-                while (index < endIndex)
-                {
-                    ref int bucket = ref buckets[index];
-                    if (bucket < 0)
-                    {
-                        if (index == startIndex)
-                        {
-                            nonEmptyCount++;
-                        }
-
-                        bucket = i;
-                        break;
-                    }
-
-                    index++;
-                }
-
-                if (index == endIndex)
-                {
-                    // If we've already hit the max per-bucket limit, bail.
-                    ArrayPool<int>.Shared.Return(buckets);
-                    return null;
-                }
-            }
-
-            // If there would be too much empty space in the lookup array, bail.
-            if (nonEmptyCount / (double)spread < EmptyLengthsRatio)
-            {
-                ArrayPool<int>.Shared.Return(buckets);
-                return null;
-            }
-
-#if NET6_0_OR_GREATER
-            // We don't need an array with every value initialized to zero if we are just about to overwrite every value anyway.
-            int[] copy = GC.AllocateUninitializedArray<int>(arraySize);
-            Array.Copy(buckets, copy, arraySize);
-#else
-            int[] copy = buckets.AsSpan(0, arraySize).ToArray();
-#endif
-            ArrayPool<int>.Shared.Return(buckets);
-
-            return new LengthBucketsFrozenDictionary<TValue>(keys, values, copy, minLength, comparer);
+            return new LengthBucketsFrozenDictionary<TValue>(keys, values, lengthBuckets, minLength, comparer);
         }
 
         /// <inheritdoc />
@@ -138,8 +59,8 @@ namespace System.Collections.Frozen
         private protected override ref readonly TValue GetValueRefOrNullRefCore(string key)
         {
             // If the length doesn't have an associated bucket, the key isn't in the dictionary.
-            int bucketIndex = (key.Length - _minLength) * MaxPerLength;
-            int bucketEndIndex = bucketIndex + MaxPerLength;
+            int bucketIndex = (key.Length - _minLength) * LengthBuckets.MaxPerLength;
+            int bucketEndIndex = bucketIndex + LengthBuckets.MaxPerLength;
             int[] lengthBuckets = _lengthBuckets;
             if (bucketIndex >= 0 && bucketEndIndex <= lengthBuckets.Length)
             {
index b5e7cd3..000eec5 100644 (file)
@@ -1,7 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Buffers;
 using System.Collections.Generic;
 using System.Diagnostics;
 
@@ -10,11 +9,6 @@ namespace System.Collections.Frozen
     /// <summary>Provides a frozen set implementation where strings are grouped by their lengths.</summary>
     internal sealed class LengthBucketsFrozenSet : FrozenSetInternalBase<string, LengthBucketsFrozenSet.GSW>
     {
-        /// <summary>Allowed ratio between buckets with values and total buckets.  Under this ratio, this implementation won't be used due to too much wasted space.</summary>
-        private const double EmptyLengthsRatio = 0.2;
-        /// <summary>The maximum number of items allowed per bucket.  The larger the value, the longer it can take to search a bucket, which is sequentially examined.</summary>
-        private const int MaxPerLength = 5;
-
         private readonly int[] _lengthBuckets;
         private readonly int _minLength;
         private readonly string[] _items;
@@ -36,87 +30,14 @@ namespace System.Collections.Frozen
             string[] items, IEqualityComparer<string> comparer, int minLength, int maxLength)
         {
             Debug.Assert(items.Length != 0);
-            Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
-            Debug.Assert(minLength >= 0 && maxLength >= minLength);
 
-            // If without even looking at the keys we know that some bucket will exceed the max per-bucket
-            // limit (pigeon hole principle), we can early-exit out without doing any further work.
-            int spread = maxLength - minLength + 1;
-            if (items.Length / spread > MaxPerLength)
+            int[]? lengthBuckets = LengthBuckets.CreateLengthBucketsArrayIfAppropriate(items, comparer, minLength, maxLength);
+            if (lengthBuckets is null)
             {
                 return null;
             }
 
-            int arraySize = spread * MaxPerLength;
-#if NET6_0_OR_GREATER
-            if (arraySize > Array.MaxLength)
-#else
-            if (arraySize > 0X7FFFFFC7)
-#endif
-            {
-                // In the future we may lower the value, as it may be quite unlikely
-                // to have a LOT of strings of different sizes.
-                return null;
-            }
-
-            // Instead of creating a dictionary of lists or a multi-dimensional array
-            // we rent a single dimension array, where every bucket has five slots.
-            // The bucket starts at (key.Length - minLength) * 5.
-            // Each value is an index of the key from _keys array
-            // or just -1, which represents "null".
-            int[] buckets = ArrayPool<int>.Shared.Rent(arraySize);
-            buckets.AsSpan(0, arraySize).Fill(-1);
-
-            int nonEmptyCount = 0;
-            for (int i = 0; i < items.Length; i++)
-            {
-                string key = items[i];
-                int startIndex = (key.Length - minLength) * MaxPerLength;
-                int endIndex = startIndex + MaxPerLength;
-                int index = startIndex;
-
-                while (index < endIndex)
-                {
-                    ref int bucket = ref buckets[index];
-                    if (bucket < 0)
-                    {
-                        if (index == startIndex)
-                        {
-                            nonEmptyCount++;
-                        }
-
-                        bucket = i;
-                        break;
-                    }
-
-                    index++;
-                }
-
-                if (index == endIndex)
-                {
-                    // If we've already hit the max per-bucket limit, bail.
-                    ArrayPool<int>.Shared.Return(buckets);
-                    return null;
-                }
-            }
-
-            // If there would be too much empty space in the lookup array, bail.
-            if (nonEmptyCount / (double)spread < EmptyLengthsRatio)
-            {
-                ArrayPool<int>.Shared.Return(buckets);
-                return null;
-            }
-
-#if NET6_0_OR_GREATER
-            // We don't need an array with every value initialized to zero if we are just about to overwrite every value anyway.
-            int[] copy = GC.AllocateUninitializedArray<int>(arraySize);
-            Array.Copy(buckets, copy, arraySize);
-#else
-            int[] copy = buckets.AsSpan(0, arraySize).ToArray();
-#endif
-            ArrayPool<int>.Shared.Return(buckets);
-
-            return new LengthBucketsFrozenSet(items, copy, minLength, comparer);
+            return new LengthBucketsFrozenSet(items, lengthBuckets, minLength, comparer);
         }
 
         /// <inheritdoc />
@@ -134,8 +55,8 @@ namespace System.Collections.Frozen
             if (item is not null) // this implementation won't be constructed from null values, but Contains may still be called with one
             {
                 // If the length doesn't have an associated bucket, the key isn't in the dictionary.
-                int bucketIndex = (item.Length - _minLength) * MaxPerLength;
-                int bucketEndIndex = bucketIndex + MaxPerLength;
+                int bucketIndex = (item.Length - _minLength) * LengthBuckets.MaxPerLength;
+                int bucketEndIndex = bucketIndex + LengthBuckets.MaxPerLength;
                 int[] lengthBuckets = _lengthBuckets;
                 if (bucketIndex >= 0 && bucketEndIndex <= lengthBuckets.Length)
                 {
@@ -155,7 +76,7 @@ namespace System.Collections.Frozen
                             }
                             else
                             {
-                                // -1 is used to indicate a null, when it's casted to uint it becomes > keys.Length
+                                // -1 is used to indicate a null, when it's casted to uint it becomes > items.Length
                                 break;
                             }
                         }
@@ -174,7 +95,7 @@ namespace System.Collections.Frozen
                             }
                             else
                             {
-                                // -1 is used to indicate a null, when it's casted to unit it becomes > keys.Length
+                                // -1 is used to indicate a null, when it's casted to unit it becomes > items.Length
                                 break;
                             }
                         }
index d5630d7..661aec6 100644 (file)
@@ -13,7 +13,7 @@ namespace System.Collections.Frozen
     internal sealed class ValueTypeDefaultComparerFrozenDictionary<TKey, TValue> : KeysAndValuesFrozenDictionary<TKey, TValue>, IDictionary<TKey, TValue>
         where TKey : notnull
     {
-        internal ValueTypeDefaultComparerFrozenDictionary(Dictionary<TKey, TValue> source) : base(source)
+        internal ValueTypeDefaultComparerFrozenDictionary(Dictionary<TKey, TValue> source) : base(source, Constants.KeysAreHashCodes<TKey>())
         {
             Debug.Assert(typeof(TKey).IsValueType);
             Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<TKey>.Default));
index d54dffd..8f63226 100644 (file)
@@ -10,7 +10,7 @@ namespace System.Collections.Frozen
     /// <typeparam name="T">The type of values in the set.</typeparam>
     internal sealed class ValueTypeDefaultComparerFrozenSet<T> : ItemsFrozenSet<T, ValueTypeDefaultComparerFrozenSet<T>.GSW>
     {
-        internal ValueTypeDefaultComparerFrozenSet(HashSet<T> source) : base(source)
+        internal ValueTypeDefaultComparerFrozenSet(HashSet<T> source) : base(source, Constants.KeysAreHashCodes<T>())
         {
             Debug.Assert(typeof(T).IsValueType);
             Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<T>.Default));
index c6455a1..a1f6adc 100644 (file)
@@ -18,18 +18,33 @@ namespace System.Collections.Frozen.Tests
         protected override Type ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentOutOfRangeException);
 
         protected virtual bool AllowVeryLargeSizes => true;
+        protected virtual int MaxUniqueValueCount => int.MaxValue;
 
         public virtual TKey GetEqualKey(TKey key) => key;
 
         protected override IDictionary<TKey, TValue> GenericIDictionaryFactory(int count)
+            => GenerateUniqueKeyValuePairs(count).ToFrozenDictionary(GetKeyIEqualityComparer());
+
+        protected KeyValuePair<TKey, TValue>[] GenerateUniqueKeyValuePairs(int count)
         {
-            var d = new Dictionary<TKey, TValue>();
-            for (int i = 0; i < count; i++)
+            if (count > MaxUniqueValueCount)
             {
-                d.Add(CreateTKey(i), CreateTValue(i));
+                throw new NotSupportedException($"It's impossible to create {count} unique values for {typeof(TKey)} keys.");
+            }
+
+            Dictionary<TKey, TValue> dictionary = new();
+            int seed = 0;
+            while (dictionary.Count != count)
+            {
+                TKey key = CreateTKey(seed);
+                if (!dictionary.ContainsKey(key))
+                {
+                    dictionary.Add(key, CreateTValue(seed));
+                }
+                seed++;
             }
 
-            return d.ToFrozenDictionary(GetKeyIEqualityComparer());
+            return dictionary.ToArray();
         }
 
         protected override IDictionary<TKey, TValue> GenericIDictionaryFactory() => Enumerable.Empty<KeyValuePair<TKey, TValue>>().ToFrozenDictionary();
@@ -146,7 +161,7 @@ namespace System.Collections.Frozen.Tests
         [Fact]
         public void ToFrozenDictionary_KeySelector_ResultsAreUsed()
         {
-            TKey[] keys = Enumerable.Range(0, 10).Select(CreateTKey).ToArray();
+            TKey[] keys = GenerateUniqueKeyValuePairs(10).Select(pair => pair.Key).ToArray();
 
             FrozenDictionary<TKey, int> frozen = Enumerable.Range(0, 10).ToFrozenDictionary(i => keys[i], NonDefaultEqualityComparer<TKey>.Instance);
             Assert.Same(NonDefaultEqualityComparer<TKey>.Instance, frozen.Comparer);
@@ -160,8 +175,9 @@ namespace System.Collections.Frozen.Tests
         [Fact]
         public void ToFrozenDictionary_KeySelectorAndValueSelector_ResultsAreUsed()
         {
-            TKey[] keys = Enumerable.Range(0, 10).Select(CreateTKey).ToArray();
-            TValue[] values = Enumerable.Range(0, 10).Select(CreateTValue).ToArray();
+            KeyValuePair<TKey, TValue>[] uniquePairs = GenerateUniqueKeyValuePairs(10);
+            TKey[] keys = uniquePairs.Select(pair => pair.Key).ToArray();
+            TValue[] values = uniquePairs.Select(pair => pair.Value).ToArray();
 
             FrozenDictionary<TKey, TValue> frozen = Enumerable.Range(0, 10).ToFrozenDictionary(i => keys[i], i => values[i], NonDefaultEqualityComparer<TKey>.Instance);
             Assert.Same(NonDefaultEqualityComparer<TKey>.Instance, frozen.Comparer);
@@ -183,8 +199,7 @@ namespace System.Collections.Frozen.Tests
         public void LookupItems_AllItemsFoundAsExpected(int size, IEqualityComparer<TKey> comparer, bool specifySameComparer)
         {
             Dictionary<TKey, TValue> original =
-                Enumerable.Range(0, size)
-                .Select(i => new KeyValuePair<TKey, TValue>(CreateTKey(i), CreateTValue(i)))
+                GenerateUniqueKeyValuePairs(size)
                 .ToDictionary(p => p.Key, p => p.Value, comparer);
             KeyValuePair<TKey, TValue>[] originalPairs = original.ToArray();
 
@@ -237,8 +252,7 @@ namespace System.Collections.Frozen.Tests
         public void EqualButPossiblyDifferentKeys_Found(bool fromDictionary)
         {
             Dictionary<TKey, TValue> original =
-                Enumerable.Range(0, 50)
-                .Select(i => new KeyValuePair<TKey, TValue>(CreateTKey(i), CreateTValue(i)))
+                GenerateUniqueKeyValuePairs(50)
                 .ToDictionary(p => p.Key, p => p.Value, GetKeyIEqualityComparer());
 
             FrozenDictionary<TKey, TValue> frozen = fromDictionary ?
@@ -259,7 +273,7 @@ namespace System.Collections.Frozen.Tests
         [Fact]
         public void MultipleValuesSameKey_LastInSourceWins()
         {
-            TKey[] keys = Enumerable.Range(0, 2).Select(CreateTKey).ToArray();
+            TKey[] keys = GenerateUniqueKeyValuePairs(2).Select(pair => pair.Key).ToArray();
             TValue[] values = Enumerable.Range(0, 10).Select(CreateTValue).ToArray();
 
             foreach (bool reverse in new[] { false, true })
@@ -392,19 +406,89 @@ namespace System.Collections.Frozen.Tests
         }
     }
 
-    public class FrozenDictionary_Generic_Tests_int_int : FrozenDictionary_Generic_Tests<int, int>
+    public abstract class FrozenDictionary_Generic_Tests_base_for_numbers<T> : FrozenDictionary_Generic_Tests<T, T>
     {
         protected override bool DefaultValueAllowed => true;
 
-        protected override KeyValuePair<int, int> CreateT(int seed)
+        protected override KeyValuePair<T, T> CreateT(int seed)
         {
             Random rand = new Random(seed);
-            return new KeyValuePair<int, int>(rand.Next(), rand.Next());
+            return new KeyValuePair<T, T>(Next(rand), Next(rand));
         }
 
-        protected override int CreateTKey(int seed) => new Random(seed).Next();
+        protected override T CreateTKey(int seed) => Next(new Random(seed));
+
+        protected override T CreateTValue(int seed) => CreateTKey(seed);
+
+        protected abstract T Next(Random random);
+
+        protected static long NextLong(Random random)
+        {
+            byte[] bytes = new byte[8];
+            random.NextBytes(bytes);
+            return BitConverter.ToInt64(bytes, 0);
+        }
+    }
+
+
+    public class FrozenDictionary_Generic_Tests_int_int : FrozenDictionary_Generic_Tests_base_for_numbers<int>
+    {
+        protected override int Next(Random random) => random.Next(); 
+    }
+
+    public class FrozenDictionary_Generic_Tests_uint_uint : FrozenDictionary_Generic_Tests_base_for_numbers<uint>
+    {
+        protected override uint Next(Random random) => (uint)random.Next(int.MinValue, int.MaxValue);
+    }
+
+    public class FrozenDictionary_Generic_Tests_nint_nint : FrozenDictionary_Generic_Tests_base_for_numbers<nint>
+    {
+        protected override nint Next(Random random) => IntPtr.Size == sizeof(int)
+            ? random.Next(int.MinValue, int.MaxValue)
+            : (nint)NextLong(random);
+    }
+
+    public class FrozenDictionary_Generic_Tests_nuint_nuint : FrozenDictionary_Generic_Tests_base_for_numbers<nuint>
+    {
+        protected override nuint Next(Random random) => IntPtr.Size == sizeof(int)
+            ? (nuint)random.Next(int.MinValue, int.MaxValue)
+            : (nuint)NextLong(random);
+    }
+
+    public class FrozenDictionary_Generic_Tests_short_short : FrozenDictionary_Generic_Tests_base_for_numbers<short>
+    {
+        protected override bool AllowVeryLargeSizes => false;
+
+        protected override int MaxUniqueValueCount => short.MaxValue - short.MinValue;
+
+        protected override short Next(Random random) => (short)random.Next(short.MinValue, short.MaxValue);
+    }
+
+    public class FrozenDictionary_Generic_Tests_ushort_ushort : FrozenDictionary_Generic_Tests_base_for_numbers<ushort>
+    {
+        protected override bool AllowVeryLargeSizes => false;
+
+        protected override int MaxUniqueValueCount => ushort.MaxValue;
+
+        protected override ushort Next(Random random) => (ushort)random.Next(ushort.MinValue, ushort.MaxValue);
+    }
+
+    public class FrozenDictionary_Generic_Tests_byte_byte : FrozenDictionary_Generic_Tests_base_for_numbers<byte>
+    {
+        protected override bool AllowVeryLargeSizes => false;
+
+        protected override int MaxUniqueValueCount => byte.MaxValue;
+
+        protected override byte Next(Random random) => (byte)random.Next(byte.MinValue, byte.MaxValue);
+    }
+
+    public class FrozenDictionary_Generic_Tests_sbyte_sbyte : FrozenDictionary_Generic_Tests_base_for_numbers<sbyte>
+    {
+        protected override bool AllowVeryLargeSizes => false;
+
+        protected override int MaxUniqueValueCount => sbyte.MaxValue - sbyte.MinValue;
 
-        protected override int CreateTValue(int seed) => CreateTKey(seed);
+        protected override sbyte Next(Random random) => (sbyte)random.Next(sbyte.MinValue, sbyte.MaxValue);
     }
 
     public class FrozenDictionary_Generic_Tests_SimpleClass_SimpleClass : FrozenDictionary_Generic_Tests<SimpleClass, SimpleClass>