More frozen collection perf, test, and cleanup improvements (#81389)
authorStephen Toub <stoub@microsoft.com>
Tue, 31 Jan 2023 17:49:27 +0000 (12:49 -0500)
committerGitHub <noreply@github.com>
Tue, 31 Jan 2023 17:49:27 +0000 (12:49 -0500)
* More frozen collection perf, test, and cleanup improvements

- Changed SmallIntegerFrozenDictionary/Set implementations to be based on `IComparable<>` rather than `IBinaryInteger<>`.  This enables them to be used with comparable types other than integers and also be used in pre-.NET 7 builds, as well as to simplify consuming code. Codegen isn't quite as good for the types it did work with, but it's good enough (and will hopefully improve in the future.
- Removed SmallInt32FrozenDictionary/Set implementations, which as a result of the above changes are no longer needed.
- Rewrote FrozenHashtable.Create to avoid creating so many temporary allocations as part of setting up the hashtable.  It was creating a list per bucket, which on average would be a list per element.  This was not only a lot of allocation but also a significant time sink, with benchmarks showing this can up to halve construction time.
- Sorted some .csproj includes
- A few more tests to improve code coverage. Line coverage for the Frozen namespace is up to 99.1%.

* Address PR feedback

* Apply suggestions from code review

Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>
---------

Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>
17 files changed:
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/FrozenDictionary.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenHashTable.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32/Int32FrozenDictionary.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32/Int32FrozenSet.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32/SmallInt32FrozenDictionary.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32/SmallInt32FrozenSet.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Integer/SmallIntegerFrozenDictionary.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Integer/SmallIntegerFrozenSet.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallComparableValueTypeFrozenDictionary.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallComparableValueTypeFrozenSet.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/WrappedDictionaryFrozenDictionary.cs
src/libraries/System.Collections.Immutable/tests/Frozen/FrozenDictionaryTests.cs
src/libraries/System.Collections.Immutable/tests/Frozen/FrozenFromKnownValuesTests.cs
src/libraries/System.Collections.Immutable/tests/Frozen/FrozenSetTests.cs

index 4ac92f2..eba6ba5 100644 (file)
@@ -11,7 +11,35 @@ The System.Collections.Immutable library is built-in as part of the shared frame
   <ItemGroup>
     <Compile Include="Properties\InternalsVisibleTo.cs" />
     
+    <Compile Include="System\Polyfills.cs" />
+    <Compile Include="System\Collections\ThrowHelper.cs" />
+    <Compile Include="$(CoreLibSharedDir)System\Collections\HashHelpers.cs" Link="System\Collections\HashHelpers.cs" />
+  
+    <Compile Include="System\Collections\Frozen\Constants.cs" />
+    <Compile Include="System\Collections\Frozen\DefaultFrozenDictionary.cs" />
+    <Compile Include="System\Collections\Frozen\DefaultFrozenSet.cs" />
+    <Compile Include="System\Collections\Frozen\EmptyFrozenDictionary.cs" />
+    <Compile Include="System\Collections\Frozen\EmptyFrozenSet.cs" />
+    <Compile Include="System\Collections\Frozen\FrozenDictionary.cs" />
+    <Compile Include="System\Collections\Frozen\FrozenHashTable.cs" />
+    <Compile Include="System\Collections\Frozen\FrozenSet.cs" />
+    <Compile Include="System\Collections\Frozen\FrozenSetInternalBase.cs" />
+    <Compile Include="System\Collections\Frozen\ImmutableArrayFactory.cs" />
+    <Compile Include="System\Collections\Frozen\ItemsFrozenSet.cs" />
+    <Compile Include="System\Collections\Frozen\KeysAndValuesFrozenDictionary.cs" />
+    <Compile Include="System\Collections\Frozen\SmallFrozenDictionary.cs" />
+    <Compile Include="System\Collections\Frozen\SmallFrozenSet.cs" />
+    <Compile Include="System\Collections\Frozen\SmallComparableValueTypeFrozenDictionary.cs" />
+    <Compile Include="System\Collections\Frozen\SmallComparableValueTypeFrozenSet.cs" />
+    <Compile Include="System\Collections\Frozen\ValueTypeDefaultComparerFrozenDictionary.cs" />
+    <Compile Include="System\Collections\Frozen\ValueTypeDefaultComparerFrozenSet.cs" />
+    <Compile Include="System\Collections\Frozen\Int32\Int32FrozenDictionary.cs" />
+    <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\LengthBucketsFrozenDictionary.cs" />
+    <Compile Include="System\Collections\Frozen\String\LengthBucketsFrozenSet.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary.cs" />
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii.cs" />
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_FullCaseInsensitive.cs" />
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_Full.cs" />
@@ -23,8 +51,7 @@ The System.Collections.Immutable library is built-in as part of the shared frame
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_RightJustifiedSingleChar.cs" />
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_RightJustifiedSubstring.cs" />
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_LeftJustifiedSubstring.cs" />
-    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\String\Hashing.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet.cs" />
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_FullCaseInsensitiveAscii.cs" />
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_FullCaseInsensitive.cs" />
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_Full.cs" />
@@ -36,33 +63,7 @@ The System.Collections.Immutable library is built-in as part of the shared frame
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring.cs" />
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_RightJustifiedSubstring.cs" />
     <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_RightJustifiedSingleChar.cs" />
-    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet.cs" />
-
-    <Compile Include="System\Polyfills.cs" />
-    <Compile Include="System\Collections\ThrowHelper.cs" />
-    <Compile Include="$(CoreLibSharedDir)System\Collections\HashHelpers.cs" Link="System\Collections\HashHelpers.cs" />
-  
-    <Compile Include="System\Collections\Frozen\Constants.cs" />
-    <Compile Include="System\Collections\Frozen\DefaultFrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\DefaultFrozenSet.cs" />
-    <Compile Include="System\Collections\Frozen\EmptyFrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\EmptyFrozenSet.cs" />
-    <Compile Include="System\Collections\Frozen\FrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\FrozenHashTable.cs" />
-    <Compile Include="System\Collections\Frozen\FrozenSet.cs" />
-    <Compile Include="System\Collections\Frozen\FrozenSetInternalBase.cs" />
-    <Compile Include="System\Collections\Frozen\ImmutableArrayFactory.cs" />
-    <Compile Include="System\Collections\Frozen\ItemsFrozenSet.cs" />
-    <Compile Include="System\Collections\Frozen\KeysAndValuesFrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\String\LengthBucketsFrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\String\LengthBucketsFrozenSet.cs" />
-    <Compile Include="System\Collections\Frozen\SmallFrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\SmallFrozenSet.cs" />
-    <Compile Include="System\Collections\Frozen\ValueTypeDefaultComparerFrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\ValueTypeDefaultComparerFrozenSet.cs" />
-    <Compile Include="System\Collections\Frozen\Int32\Int32FrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\Int32\Int32FrozenSet.cs" />
-
+    
     <Compile Include="System\Collections\Generic\IHashKeyCollection.cs" />
     <Compile Include="System\Collections\Generic\ISortKeyCollection.cs" />
 
@@ -147,16 +148,6 @@ The System.Collections.Immutable library is built-in as part of the shared frame
     <Compile Include="System\Collections\Frozen\WrappedDictionaryFrozenDictionary.cs" />
   </ItemGroup>
 
-  <ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
-    <Compile Include="System\Collections\Frozen\Integer\SmallIntegerFrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\Integer\SmallIntegerFrozenSet.cs" />
-  </ItemGroup>
-
-  <ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
-    <Compile Include="System\Collections\Frozen\Int32\SmallInt32FrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\Int32\SmallInt32FrozenSet.cs" />
-  </ItemGroup>
-  
   <ItemGroup Condition="'$(TargetFramework)' == '$(NetCoreAppCurrent)'">
     <Reference Include="System.Collections" />
     <Reference Include="System.Linq" />
index f4566fa..7e1a953 100644 (file)
@@ -9,13 +9,11 @@ namespace System.Collections.Frozen
     /// <remarks>
     /// These numbers were arrived through simple benchmarks conducted against .NET 7.
     /// It's worth potentially tweaking these values if the implementation of the
-    /// collections changes in a substantial way, or if the JIT got smarter over time.
+    /// collections changes in a substantial way, or if the JIT improves related code gen over time.
     /// </remarks>
     internal static class Constants
     {
-        /// <summary>
-        /// Threshold when we switch from scanning to hashing for non-integer collections.
-        /// </summary>
+        /// <summary>Threshold when we switch from scanning to hashing for non-value-type or non-default-comparer collections.</summary>
         /// <remarks>
         /// This determines the threshold where we switch from
         /// the scanning-based SmallFrozenDictionary/Set to the hashing-based
@@ -23,25 +21,12 @@ namespace System.Collections.Frozen
         /// </remarks>
         public const int MaxItemsInSmallFrozenCollection = 4;
 
-        /// <summary>
-        /// Threshold when we switch from scanning to hashing integer collections.
-        /// </summary>
+        /// <summary>Threshold when we switch from scanning to hashing value type collections using a default comparer.</summary>
         /// <remarks>
         /// This determines the threshold when we switch from the scanning
-        /// SmallIntegerFrozenDictionary/Set to the
-        /// hashing IntegerFrozenDictionary/Set.
+        /// SmallValueTypeDefaultComparerFrozenDictionary/Set to the
+        /// hashing ValueTypeDefaultComparerFrozenDictionary/Set.
         /// </remarks>
-        public const int MaxItemsInSmallIntegerFrozenCollection = 10;
-
-        /// <summary>
-        /// How much free space is allowed in a sparse integer set
-        /// </summary>
-        /// <remarks>
-        /// This determines how much free space is allowed in a sparse integer set.
-        /// This is a space/perf trade off. The sparse sets just use a bit vector to
-        /// hold the state, so lookup is always fast. But there's a point where you're
-        /// too much heap space.
-        /// </remarks>
-        public const int MaxSparsenessFactorInSparseRangeIntegerSet = 8;
+        public const int MaxItemsInSmallComparableValueTypeFrozenCollection = 10;
     }
 }
index 65de1ad..b0fee67 100644 (file)
@@ -196,34 +196,21 @@ namespace System.Collections.Frozen
             if (typeof(TKey).IsValueType)
             {
                 // Optimize for value types when the default comparer is being used. In such a case, the implementation
-                // may use EqualityComparer<TKey>.Default.Equals/GetHashCode directly, with generic specialization enabling
+                // may use {Equality}Comparer<TKey>.Default.Compare/Equals/GetHashCode directly, with generic specialization enabling
                 // the Equals/GetHashCode methods to be devirtualized and possibly inlined.
                 if (ReferenceEquals(comparer, EqualityComparer<TKey>.Default))
                 {
-#if NET7_0_OR_GREATER
-                    if (typeof(TKey) == typeof(sbyte)) return PickIntegerDictionary<sbyte>(source);
-                    if (typeof(TKey) == typeof(byte)) return PickIntegerDictionary<byte>(source);
-                    if (typeof(TKey) == typeof(short)) return PickIntegerDictionary<short>(source);
-                    if (typeof(TKey) == typeof(ushort)) return PickIntegerDictionary<ushort>(source);
-                    if (typeof(TKey) == typeof(int)) return PickIntegerDictionary<int>(source);
-                    if (typeof(TKey) == typeof(uint)) return PickIntegerDictionary<uint>(source);
-                    if (typeof(TKey) == typeof(long)) return PickIntegerDictionary<long>(source);
-                    if (typeof(TKey) == typeof(ulong)) return PickIntegerDictionary<ulong>(source);
-
-                    static FrozenDictionary<TKey, TValue> PickIntegerDictionary<TInt>(Dictionary<TKey, TValue> source) where TInt : struct, IBinaryInteger<TInt> =>
-                        (FrozenDictionary<TKey, TValue>)(object)
-                        (source.Count <= Constants.MaxItemsInSmallIntegerFrozenCollection ? new SmallIntegerFrozenDictionary<TInt, TValue>((Dictionary<TInt, TValue>)(object)source) :
-                         typeof(TInt) == typeof(int) ? new Int32FrozenDictionary<TValue>((Dictionary<int, TValue>)(object)source) :
-                         new ValueTypeDefaultComparerFrozenDictionary<TInt, TValue>((Dictionary<TInt, TValue>)(object)source));
-#else
+                    if (default(TKey) is IComparable<TKey> &&
+                        source.Count <= Constants.MaxItemsInSmallComparableValueTypeFrozenCollection)
+                    {
+                        return (FrozenDictionary<TKey, TValue>)(object)new SmallComparableValueTypeFrozenDictionary<TKey, TValue>(source);
+                    }
+
                     if (typeof(TKey) == typeof(int))
                     {
-                        return (FrozenDictionary<TKey, TValue>)(object)
-                            (source.Count <= Constants.MaxItemsInSmallIntegerFrozenCollection ?
-                             new SmallInt32FrozenDictionary<TValue>((Dictionary<int, TValue>)(object)source) :
-                             new Int32FrozenDictionary<TValue>((Dictionary<int, TValue>)(object)source));
+                        return (FrozenDictionary<TKey, TValue>)(object)new Int32FrozenDictionary<TValue>((Dictionary<int, TValue>)(object)source);
                     }
-#endif
+
                     return new ValueTypeDefaultComparerFrozenDictionary<TKey, TValue>(source);
                 }
             }
index 11a52cb..5ad2bde 100644 (file)
@@ -5,7 +5,6 @@ using System.Buffers;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
 
 namespace System.Collections.Frozen
 {
@@ -19,6 +18,13 @@ namespace System.Collections.Frozen
         private readonly Bucket[] _buckets;
         private readonly ulong _fastModMultiplier;
 
+        /// <summary>Initializes the hashtable with the computed hashcodes and bucket information.</summary>
+        /// <param name="hashCodes">The array of hashcodes grouped into contiguous regions by bucket. Each bucket is one and only one region of the array.</param>
+        /// <param name="buckets">
+        /// The array of buckets, indexed by hashCodes % buckets.Length, where each bucket is
+        /// the start/end index into <paramref name="hashCodes"/> for all items in that bucket.
+        /// </param>
+        /// <param name="fastModMultiplier">The multiplier to use as part of a FastMod method call.</param>
         private FrozenHashTable(int[] hashCodes, Bucket[] buckets, ulong fastModMultiplier)
         {
             Debug.Assert(hashCodes.Length != 0);
@@ -47,50 +53,81 @@ namespace System.Collections.Frozen
         {
             Debug.Assert(entries.Length != 0);
 
-            int[] hashCodes = new int[entries.Length];
+            // Calculate the hashcodes for every entry.
+            int[] arrayPoolHashCodes = ArrayPool<int>.Shared.Rent(entries.Length);
+            Span<int> hashCodes = arrayPoolHashCodes.AsSpan(0, entries.Length);
             for (int i = 0; i < entries.Length; i++)
             {
                 hashCodes[i] = hasher(entries[i]);
             }
 
+            // Determine how many buckets to use.  This might be fewer than the number of entries
+            // if any entries have identical hashcodes (not just different hashcodes that might
+            // map to the same bucket).
             int numBuckets = CalcNumBuckets(hashCodes, optimizeForReading);
             ulong fastModMultiplier = HashHelpers.GetFastModMultiplier((uint)numBuckets);
 
-            var chainBuddies = new Dictionary<uint, List<ChainBuddy>>();
+            // Create two spans:
+            // - bucketStarts: initially filled with all -1s, the ith element stores the index
+            //   into hashCodes of the head element of that bucket's chain.
+            // - nexts: the ith element stores the index of the next item in the chain.
+            int[] arrayPoolBuckets = ArrayPool<int>.Shared.Rent(numBuckets + hashCodes.Length);
+            Span<int> bucketStarts = arrayPoolBuckets.AsSpan(0, numBuckets);
+            Span<int> nexts = arrayPoolBuckets.AsSpan(numBuckets, hashCodes.Length);
+            bucketStarts.Fill(-1);
+
+            // Populate the bucket entries and starts.  For each hash code, compute its bucket,
+            // and store at the bucket entry corresponding to the hashcode item the entry for that
+            // item, which includes a copy of the hash code and the current bucket start, which
+            // is then replaced by this entry as it's pushed into the bucket list.
             for (int index = 0; index < hashCodes.Length; index++)
             {
                 int hashCode = hashCodes[index];
-                uint bucket = HashHelpers.FastMod((uint)hashCode, (uint)numBuckets, fastModMultiplier);
+                int bucketNum = (int)HashHelpers.FastMod((uint)hashCode, (uint)bucketStarts.Length, fastModMultiplier);
 
-#if NET6_0_OR_GREATER
-                ref List<ChainBuddy>? list = ref CollectionsMarshal.GetValueRefOrAddDefault(chainBuddies, bucket, out _);
-                list ??= new List<ChainBuddy>();
-#else
-                if (!chainBuddies.TryGetValue(bucket, out List<ChainBuddy>? list))
-                {
-                    chainBuddies[bucket] = list = new List<ChainBuddy>();
-                }
-#endif
-
-                list.Add(new ChainBuddy(hashCode, index));
+                ref int bucketStart = ref bucketStarts[bucketNum];
+                nexts[index] = bucketStart;
+                bucketStart = index;
             }
 
-            var buckets = new Bucket[numBuckets]; // buckets left uninitialized will by default have an endIndex < startIndex
-
+            // Write out the hashcodes and buckets arrays to be used by the FrozenHashtable instance.
+            // We iterate through each bucket start, and from each, each item in that chain, writing
+            // out all of the items in each chain next to each other in the hashcodes list (and
+            // calling the setter to allow the consumer to reorder its entries appropriately).
+            // Along the way we could how many items are in each chain, and use that along with
+            // the starting index to write out the bucket information for indexing into hashcodes.
+            var hashtableHashcodes = new int[hashCodes.Length];
+            var hashtableBuckets = new Bucket[bucketStarts.Length];
             int count = 0;
-            foreach (KeyValuePair<uint, List<ChainBuddy>> chain in chainBuddies)
+            for (int bucketNum = 0; bucketNum < hashtableBuckets.Length; bucketNum++)
             {
-                List<ChainBuddy> list = chain.Value;
-                buckets[chain.Key] = new Bucket(count, list.Count);
-                for (int i = 0; i < list.Count; i++)
+                int bucketStart = bucketStarts[bucketNum];
+                if (bucketStart < 0)
                 {
-                    hashCodes[count] = list[i].HashCode;
-                    setter(count, entries[list[i].Index]);
+                    continue;
+                }
+
+                int bucketCount = 0;
+                int index = bucketStart;
+                bucketStart = count;
+                while (index >= 0)
+                {
+                    hashtableHashcodes[count] = hashCodes[index];
+                    setter(count, entries[index]);
                     count++;
+                    bucketCount++;
+
+                    index = nexts[index];
                 }
+
+                hashtableBuckets[bucketNum] = new Bucket(bucketStart, bucketCount);
             }
+            Debug.Assert(count == hashtableHashcodes.Length);
 
-            return new FrozenHashTable(hashCodes, buckets, fastModMultiplier);
+            ArrayPool<int>.Shared.Return(arrayPoolBuckets);
+            ArrayPool<int>.Shared.Return(arrayPoolHashCodes);
+
+            return new FrozenHashTable(hashtableHashcodes, hashtableBuckets, fastModMultiplier);
         }
 
         /// <summary>
@@ -113,16 +150,15 @@ namespace System.Collections.Frozen
         internal int[] HashCodes { get; }
 
         /// <summary>
-        /// Given an array of hash codes, figure out the best number of hash buckets to use.
+        /// Given a span of hash codes, figure out the best number of hash buckets to use.
         /// </summary>
         /// <remarks>
         /// This tries to select a prime number of buckets. Rather than iterating through all possible bucket
         /// sizes, starting at the exact number of hash codes and incrementing the bucket count by 1 per trial,
         /// this is a trade-off between speed of determining a good number of buckets and maximal density.
         /// </remarks>
-        private static int CalcNumBuckets(int[] hashCodes, bool optimizeForReading)
+        private static int CalcNumBuckets(ReadOnlySpan<int> hashCodes, bool optimizeForReading)
         {
-            Debug.Assert(hashCodes is not null);
             Debug.Assert(hashCodes.Length != 0);
 
             const double AcceptableCollisionRate = 0.05;  // What is a satisfactory rate of hash collisions?
@@ -136,7 +172,16 @@ namespace System.Collections.Frozen
             }
 
             // Filter out duplicate codes, since no increase in buckets will avoid collisions from duplicate input hash codes.
-            var codes = new HashSet<int>(hashCodes);
+            var codes =
+#if NETCOREAPP2_0_OR_GREATER
+                new HashSet<int>(hashCodes.Length);
+#else
+                new HashSet<int>();
+#endif
+            foreach (int hashCode in hashCodes)
+            {
+                codes.Add(hashCode);
+            }
             Debug.Assert(codes.Count != 0);
 
             // In our precomputed primes table, find the index of the smallest prime that's at least as large as our number of
@@ -228,18 +273,6 @@ namespace System.Collections.Frozen
             return bestNumBuckets;
         }
 
-        private readonly struct ChainBuddy
-        {
-            public readonly int HashCode;
-            public readonly int Index;
-
-            public ChainBuddy(int hashCode, int index)
-            {
-                HashCode = hashCode;
-                Index = index;
-            }
-        }
-
         private readonly struct Bucket
         {
             public readonly int StartIndex;
index d057036..e267afb 100644 (file)
@@ -127,33 +127,21 @@ namespace System.Collections.Frozen
             if (typeof(T).IsValueType)
             {
                 // Optimize for value types when the default comparer is being used. In such a case, the implementation
-                // may use EqualityComparer<T>.Default.Equals/GetHashCode directly, with generic specialization enabling
+                // may use {Equality}Comparer<T>.Default.Compare/Equals/GetHashCode directly, with generic specialization enabling
                 // the Equals/GetHashCode methods to be devirtualized and possibly inlined.
                 if (ReferenceEquals(comparer, EqualityComparer<T>.Default))
                 {
-#if NET7_0_OR_GREATER
-                    if (typeof(T) == typeof(sbyte)) return PickIntegerSet<sbyte>(source);
-                    if (typeof(T) == typeof(byte)) return PickIntegerSet<byte>(source);
-                    if (typeof(T) == typeof(short)) return PickIntegerSet<short>(source);
-                    if (typeof(T) == typeof(ushort)) return PickIntegerSet<ushort>(source);
-                    if (typeof(T) == typeof(int)) return PickIntegerSet<int>(source);
-                    if (typeof(T) == typeof(uint)) return PickIntegerSet<uint>(source);
-                    if (typeof(T) == typeof(long)) return PickIntegerSet<long>(source);
-                    if (typeof(T) == typeof(ulong)) return PickIntegerSet<ulong>(source);
-
-                    static FrozenSet<T> PickIntegerSet<TInt>(HashSet<T> source)
-                        where TInt : struct, IBinaryInteger<TInt> => (FrozenSet<T>)(object)
-                        (source.Count <= Constants.MaxItemsInSmallIntegerFrozenCollection ? new SmallIntegerFrozenSet<TInt>((HashSet<TInt>)(object)source) :
-                        typeof(T) == typeof(int) ? new Int32FrozenSet((HashSet<int>)(object)source) :
-                        new ValueTypeDefaultComparerFrozenSet<T>(source));
-#else
+                    if (default(T) is IComparable<T> &&
+                        source.Count <= Constants.MaxItemsInSmallComparableValueTypeFrozenCollection)
+                    {
+                        return (FrozenSet<T>)(object)new SmallComparableValueTypeFrozenSet<T>(source);
+                    }
+
                     if (typeof(T) == typeof(int))
                     {
-                        return (FrozenSet<T>)(object)(source.Count <= Constants.MaxItemsInSmallIntegerFrozenCollection ?
-                            new SmallInt32FrozenSet((HashSet<int>)(object)source) :
-                            new Int32FrozenSet((HashSet<int>)(object)source));
+                        return (FrozenSet<T>)(object)new Int32FrozenSet((HashSet<int>)(object)source);
                     }
-#endif
+
                     return new ValueTypeDefaultComparerFrozenSet<T>(source);
                 }
             }
index d5e6415..43214af 100644 (file)
@@ -9,7 +9,7 @@ namespace System.Collections.Frozen
 {
     /// <summary>Provides a frozen dictionary to use when the key is an <see cref="int"/> and the default comparer is used.</summary>
     /// <remarks>
-    /// This key type is specialized as a memory optimization, as the frozen hash table already contains the array of all
+    /// This dictionary type is specialized as a memory optimization, as the frozen hash table already contains the array of all
     /// int values, and we can thus use its array as the keys rather than maintaining a duplicate copy.
     /// </remarks>
     internal sealed class Int32FrozenDictionary<TValue> : FrozenDictionary<int, TValue>
index 669b80a..7e47486 100644 (file)
@@ -8,6 +8,10 @@ using System.Linq;
 namespace System.Collections.Frozen
 {
     /// <summary>Provides a frozen set to use when the value is an <see cref="int"/> and the default comparer is used.</summary>
+    /// <remarks>
+    /// This set type is specialized as a memory optimization, as the frozen hash table already contains the array of all
+    /// int values, and we can thus use its array as the items rather than maintaining a duplicate copy.
+    /// </remarks>
     internal sealed class Int32FrozenSet : FrozenSetInternalBase<int, Int32FrozenSet.GSW>
     {
         private readonly FrozenHashTable _hashTable;
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32/SmallInt32FrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32/SmallInt32FrozenDictionary.cs
deleted file mode 100644 (file)
index 4797f87..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Runtime.CompilerServices;
-
-namespace System.Collections.Frozen
-{
-    /// <summary>Provides a frozen dictionary to use when the key is an <see cref="int"/>, the default comparer is used, and the item count is small.</summary>
-    /// <remarks>
-    /// No hashing here, just a straight-up linear scan through the items.
-    /// </remarks>
-    internal sealed class SmallInt32FrozenDictionary<TValue> : FrozenDictionary<int, TValue>
-    {
-        private readonly int[] _keys;
-        private readonly TValue[] _values;
-        private readonly int _max;
-
-        internal SmallInt32FrozenDictionary(Dictionary<int, TValue> source) : base(EqualityComparer<int>.Default)
-        {
-            Debug.Assert(source.Count != 0);
-            Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<int>.Default));
-
-            _keys = source.Keys.ToArray();
-            _values = source.Values.ToArray();
-            Array.Sort(_keys, _values);
-
-            _max = _keys[_keys.Length - 1];
-        }
-
-        private protected override int[] KeysCore => _keys;
-        private protected override TValue[] ValuesCore => _values;
-        private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values);
-        private protected override int CountCore => _keys.Length;
-
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private protected override ref readonly TValue GetValueRefOrNullRefCore(int key)
-        {
-            if (key <= _max)
-            {
-                int[] keys = _keys;
-                for (int i = 0; i < keys.Length; i++)
-                {
-                    if (key <= keys[i])
-                    {
-                        if (key < keys[i])
-                        {
-                            break;
-                        }
-
-                        return ref _values[i];
-                    }
-                }
-            }
-
-            return ref Unsafe.NullRef<TValue>();
-        }
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32/SmallInt32FrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Int32/SmallInt32FrozenSet.cs
deleted file mode 100644 (file)
index 4f38d75..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-
-namespace System.Collections.Frozen
-{
-    /// <summary>Provides a frozen set to use when the value is an <see cref="int"/>, the default comparer is used, and the item count is small.</summary>
-    /// <remarks>
-    /// No hashing here, just a straight-up linear scan through the items.
-    /// </remarks>
-    internal sealed class SmallInt32FrozenSet : FrozenSetInternalBase<int, SmallInt32FrozenSet.GSW>
-    {
-        private readonly int[] _items;
-        private readonly int _max;
-
-        internal SmallInt32FrozenSet(HashSet<int> source) : base(EqualityComparer<int>.Default)
-        {
-            Debug.Assert(source.Count != 0);
-            Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<int>.Default));
-
-            int[] items = source.ToArray();
-            Array.Sort(items);
-
-            _items = items;
-            _max = _items[_items.Length - 1];
-        }
-
-        private protected override int[] ItemsCore => _items;
-        private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items);
-        private protected override int CountCore => _items.Length;
-
-        private protected override int FindItemIndex(int item)
-        {
-            if (item <= _max)
-            {
-                int[] items = _items;
-                for (int i = 0; i < items.Length; i++)
-                {
-                    if (item <= items[i])
-                    {
-                        if (item < items[i])
-                        {
-                            break;
-                        }
-
-                        return i;
-                    }
-                }
-            }
-
-            return -1;
-        }
-
-        internal struct GSW : IGenericSpecializedWrapper
-        {
-            private SmallInt32FrozenSet _set;
-            public void Store(FrozenSet<int> set) => _set = (SmallInt32FrozenSet)set;
-
-            public int Count => _set.Count;
-            public IEqualityComparer<int> Comparer => _set.Comparer;
-            public int FindItemIndex(int item) => _set.FindItemIndex(item);
-            public Enumerator GetEnumerator() => _set.GetEnumerator();
-        }
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Integer/SmallIntegerFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Integer/SmallIntegerFrozenDictionary.cs
deleted file mode 100644 (file)
index 51d352d..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Numerics;
-using System.Runtime.CompilerServices;
-
-namespace System.Collections.Frozen
-{
-    /// <summary>Provides a frozen dictionary to use when the key is an integer, the default comparer is used, and the item count is small.</summary>
-    /// <remarks>
-    /// No hashing here, just a straight-up linear scan through the items.
-    /// </remarks>
-    internal sealed class SmallIntegerFrozenDictionary<TKey, TValue> : FrozenDictionary<TKey, TValue>
-        where TKey : struct, IBinaryInteger<TKey>
-    {
-        private readonly TKey[] _keys;
-        private readonly TValue[] _values;
-        private readonly TKey _max;
-
-        internal SmallIntegerFrozenDictionary(Dictionary<TKey, TValue> source) : base(EqualityComparer<TKey>.Default)
-        {
-            Debug.Assert(source.Count != 0);
-            Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<TKey>.Default));
-
-            _keys = source.Keys.ToArray();
-            _values = source.Values.ToArray();
-            Array.Sort(_keys, _values);
-
-            _max = _keys[^1];
-        }
-
-        private protected override TKey[] KeysCore => _keys;
-        private protected override TValue[] ValuesCore => _values;
-        private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values);
-        private protected override int CountCore => _keys.Length;
-
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key)
-        {
-            if (key <= _max)
-            {
-                TKey[] keys = _keys;
-                for (int i = 0; i < keys.Length; i++)
-                {
-                    if (key <= keys[i])
-                    {
-                        if (key < keys[i])
-                        {
-                            break;
-                        }
-
-                        return ref _values[i];
-                    }
-                }
-            }
-
-            return ref Unsafe.NullRef<TValue>();
-        }
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Integer/SmallIntegerFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/Integer/SmallIntegerFrozenSet.cs
deleted file mode 100644 (file)
index c057312..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Collections.Generic;
-using System.Collections.Immutable;
-using System.Diagnostics;
-using System.Linq;
-using System.Numerics;
-
-namespace System.Collections.Frozen
-{
-    /// <summary>Provides a frozen set to use when the value is an integer, the default comparer is used, and the item count is small.</summary>
-    /// <remarks>
-    /// No hashing here, just a straight-up linear scan through the items.
-    /// </remarks>
-    internal sealed class SmallIntegerFrozenSet<T> : FrozenSetInternalBase<T, SmallIntegerFrozenSet<T>.GSW>
-        where T : struct, IBinaryInteger<T>
-    {
-        private readonly T[] _items;
-        private readonly T _max;
-
-        internal SmallIntegerFrozenSet(HashSet<T> source) : base(EqualityComparer<T>.Default)
-        {
-            Debug.Assert(source.Count != 0);
-            Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<T>.Default));
-
-            T[] items = source.ToArray();
-            Array.Sort(items);
-
-            _items = items;
-            _max = _items[^1];
-        }
-
-        private protected override T[] ItemsCore => _items;
-        private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items);
-        private protected override int CountCore => _items.Length;
-
-        private protected override int FindItemIndex(T item)
-        {
-            if (item <= _max)
-            {
-                T[] items = _items;
-                for (int i = 0; i < items.Length; i++)
-                {
-                    if (item <= items[i])
-                    {
-                        if (item < items[i])
-                        {
-                            break;
-                        }
-
-                        return i;
-                    }
-                }
-            }
-
-            return -1;
-        }
-
-        internal struct GSW : IGenericSpecializedWrapper
-        {
-            private SmallIntegerFrozenSet<T> _set;
-            public void Store(FrozenSet<T> set) => _set = (SmallIntegerFrozenSet<T>)set;
-
-            public int Count => _set.Count;
-            public IEqualityComparer<T> Comparer => _set.Comparer;
-            public int FindItemIndex(T item) => _set.FindItemIndex(item);
-            public Enumerator GetEnumerator() => _set.GetEnumerator();
-        }
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallComparableValueTypeFrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallComparableValueTypeFrozenDictionary.cs
new file mode 100644 (file)
index 0000000..6b55e99
--- /dev/null
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+namespace System.Collections.Frozen
+{
+    /// <summary>Provides a frozen dictionary to use when the key is a comparable value type, the default comparer is used, and the item count is small.</summary>
+    /// <remarks>
+    /// No hashing involved, just a linear scan through the keys.  This implementation is close in nature to that of <see cref="SmallComparableValueTypeFrozenDictionary{TKey, TValue}"/>,
+    /// except that this implementation sorts the keys in order to a) extract a max that it can compare against at the beginning of each match in order to
+    /// immediately rule out keys too large to be contained, and b) early-exits from the linear scan when a comparison determines the key is too
+    /// small to be contained.
+    /// </remarks>
+    internal sealed class SmallComparableValueTypeFrozenDictionary<TKey, TValue> : FrozenDictionary<TKey, TValue>
+        where TKey : notnull
+    {
+        private readonly TKey[] _keys;
+        private readonly TValue[] _values;
+        private readonly TKey _max;
+
+        internal SmallComparableValueTypeFrozenDictionary(Dictionary<TKey, TValue> source) : base(EqualityComparer<TKey>.Default)
+        {
+            // TKey is logically constrained to `where TKey : struct, IComparable<TKey>`, but we can't actually write that
+            // constraint currently and still have this be used from the calling context that has an unconstrained TKey.
+            // So, we assert it here instead. The implementation relies on {Equality}Comparer<TKey>.Default to sort things out.
+            Debug.Assert(default(TKey) is IComparable<TKey>);
+            Debug.Assert(default(TKey) is not null);
+            Debug.Assert(typeof(TKey).IsValueType);
+
+            Debug.Assert(source.Count != 0);
+            Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<TKey>.Default));
+
+            _keys = source.Keys.ToArray();
+            _values = source.Values.ToArray();
+            Array.Sort(_keys, _values);
+
+            _max = _keys[_keys.Length - 1];
+        }
+
+        private protected override TKey[] KeysCore => _keys;
+        private protected override TValue[] ValuesCore => _values;
+        private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values);
+        private protected override int CountCore => _keys.Length;
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(TKey key)
+        {
+            if (Comparer<TKey>.Default.Compare(key, _max) <= 0)
+            {
+                TKey[] keys = _keys;
+                for (int i = 0; i < keys.Length; i++)
+                {
+                    int c = Comparer<TKey>.Default.Compare(key, keys[i]);
+                    if (c <= 0)
+                    {
+                        if (c == 0)
+                        {
+                            return ref _values[i];
+                        }
+
+                        break;
+                    }
+                }
+            }
+
+            return ref Unsafe.NullRef<TValue>();
+        }
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallComparableValueTypeFrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/SmallComparableValueTypeFrozenSet.cs
new file mode 100644 (file)
index 0000000..99a30dc
--- /dev/null
@@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Linq;
+
+namespace System.Collections.Frozen
+{
+    /// <summary>Provides a frozen set to use when the item is a comparable value type, the default comparer is used, and the item count is small.</summary>
+    /// <remarks>
+    /// No hashing involved, just a linear scan through the items.  This implementation is close in nature to that of <see cref="ValueTypeDefaultComparerFrozenSet{T}"/>,
+    /// except that this implementation sorts the values in order to a) extract a max that it can compare against at the beginning of each match in order to
+    /// immediately rule out values too large to be contained, and b) early-exits from the linear scan when a comparison determines the value is too
+    /// small to be contained.
+    /// </remarks>
+    internal sealed class SmallComparableValueTypeFrozenSet<T> : FrozenSetInternalBase<T, SmallComparableValueTypeFrozenSet<T>.GSW>
+    {
+        private readonly T[] _items;
+        private readonly T _max;
+
+        internal SmallComparableValueTypeFrozenSet(HashSet<T> source) : base(EqualityComparer<T>.Default)
+        {
+            // T is logically constrained to `where T : struct, IComparable<T>`, but we can't actually write that
+            // constraint currently and still have this be used from the calling context that has an unconstrained T.
+            // So, we assert it here instead. The implementation relies on {Equality}Comparer<T>.Default to sort things out.
+            Debug.Assert(default(T) is IComparable<T>);
+            Debug.Assert(default(T) is not null);
+            Debug.Assert(typeof(T).IsValueType);
+
+            Debug.Assert(source.Count != 0);
+            Debug.Assert(ReferenceEquals(source.Comparer, EqualityComparer<T>.Default));
+
+            _items = source.ToArray();
+            Array.Sort(_items);
+
+            _max = _items[_items.Length - 1];
+        }
+
+        private protected override T[] ItemsCore => _items;
+        private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items);
+        private protected override int CountCore => _items.Length;
+
+        private protected override int FindItemIndex(T item)
+        {
+            if (Comparer<T>.Default.Compare(item, _max) <= 0)
+            {
+                T[] items = _items;
+                for (int i = 0; i < items.Length; i++)
+                {
+                    int c = Comparer<T>.Default.Compare(item, items[i]);
+                    if (c <= 0)
+                    {
+                        if (c == 0)
+                        {
+                            return i;
+                        }
+
+                        break;
+                    }
+                }
+            }
+
+            return -1;
+        }
+
+        internal struct GSW : IGenericSpecializedWrapper
+        {
+            private SmallComparableValueTypeFrozenSet<T> _set;
+            public void Store(FrozenSet<T> set) => _set = (SmallComparableValueTypeFrozenSet<T>)set;
+
+            public int Count => _set.Count;
+            public IEqualityComparer<T> Comparer => _set.Comparer;
+            public int FindItemIndex(T item) => _set.FindItemIndex(item);
+            public Enumerator GetEnumerator() => _set.GetEnumerator();
+        }
+    }
+}
index 38f1c2d..5374e8c 100644 (file)
@@ -9,10 +9,15 @@ using System.Threading;
 
 namespace System.Collections.Frozen
 {
+    /// <summary><see cref="FrozenDictionary{TKey, TValue}"/> implementation that just wraps a <see cref="Dictionary{TKey, TValue}"/>.</summary>
     internal sealed class WrappedDictionaryFrozenDictionary<TKey, TValue> :
         FrozenDictionary<TKey, TValue>, IDictionary<TKey, TValue>, IEnumerable<KeyValuePair<TKey, TValue>>
         where TKey : notnull
     {
+        // Note that while most of the FrozenDictionary implementations have an equivalent FrozenSet implementation,
+        // there's no corresponding WrappedHashSetFrozenSet<T> because HashSet<T> doesn't provide a way to implement
+        // FrozenSet<T>.FindItemIndex.
+
         private readonly Dictionary<TKey, TValue> _source;
         private TKey[]? _keys;
         private TValue[]? _values;
index 12e218f..a059ea7 100644 (file)
@@ -20,6 +20,8 @@ namespace System.Collections.Frozen.Tests
         protected override IDictionary<TKey, TValue> GenericIDictionaryFactory(int count) =>
             GenericIDictionaryFactory(count, optimizeForReading: true);
 
+        protected virtual bool AllowVeryLargeSizes => true;
+
         protected virtual IDictionary<TKey, TValue> GenericIDictionaryFactory(int count, bool optimizeForReading)
         {
             var d = new Dictionary<TKey, TValue>();
@@ -46,9 +48,12 @@ namespace System.Collections.Frozen.Tests
         [Theory]
         [InlineData(100_000, false)]
         [InlineData(100_000, true)]
-        public void CreateVeryLargeDictionary_Success(int largeCount, bool optimizeForReading)
+        public virtual void CreateVeryLargeDictionary_Success(int largeCount, bool optimizeForReading)
         {
-            GenericIDictionaryFactory(largeCount, optimizeForReading);
+            if (AllowVeryLargeSizes)
+            {
+                GenericIDictionaryFactory(largeCount, optimizeForReading);
+            }
         }
 
         [Fact]
@@ -406,7 +411,10 @@ namespace System.Collections.Frozen.Tests
         [InlineData(8_000_000, true)]
         public void CreateHugeDictionary_Success(int largeCount, bool optimizeForReading)
         {
-            GenericIDictionaryFactory(largeCount, optimizeForReading);
+            if (AllowVeryLargeSizes)
+            {
+                GenericIDictionaryFactory(largeCount, optimizeForReading);
+            }
         }
     }
 
@@ -429,7 +437,7 @@ namespace System.Collections.Frozen.Tests
     {
         protected override KeyValuePair<SimpleClass, SimpleClass> CreateT(int seed)
         {
-            return new KeyValuePair<SimpleClass, SimpleClass>(CreateTKey(seed), CreateTKey(seed + 500));
+            return new KeyValuePair<SimpleClass, SimpleClass>(CreateTKey(seed), CreateTValue(seed + 500));
         }
 
         protected override SimpleClass CreateTKey(int seed)
@@ -453,6 +461,36 @@ namespace System.Collections.Frozen.Tests
             Value.CompareTo(other.Value);
     }
 
+    public class FrozenDictionary_Generic_Tests_SimpleStruct_int : FrozenDictionary_Generic_Tests<SimpleStruct, int>
+    {
+        protected override KeyValuePair<SimpleStruct, int> CreateT(int seed)
+        {
+            return new KeyValuePair<SimpleStruct, int>(CreateTKey(seed), CreateTValue(seed + 500));
+        }
+
+        protected override SimpleStruct CreateTKey(int seed) => new SimpleStruct { Value = seed + 1 };
+
+        protected override int CreateTValue(int seed) => seed;
+
+        protected override bool DefaultValueAllowed => true;
+
+        protected override bool AllowVeryLargeSizes => false; // hash code contention leads to longer running times
+    }
+
+    public struct SimpleStruct : IEquatable<SimpleStruct>, IComparable<SimpleStruct>
+    {
+        public int Value { get; set; }
+
+        public int CompareTo(SimpleStruct other) => Value.CompareTo(other.Value);
+
+        public bool Equals(SimpleStruct other) => Value == other.Value;
+
+        public override int GetHashCode() => 0; // to force hashcode contention in implementation
+
+        public override bool Equals([NotNullWhen(true)] object? obj) =>
+            obj is SimpleStruct other && Equals(other);
+    }
+
     public sealed class NonDefaultEqualityComparer<TKey> : IEqualityComparer<TKey>
     {
         public static NonDefaultEqualityComparer<TKey> Instance { get; } = new();
@@ -525,16 +563,18 @@ namespace System.Collections.Frozen.Tests
             var kvpArray = new KeyValuePair<string, int>[4];
             ((ICollection)frozen).CopyTo(kvpArray, 1);
             Assert.Equal(new KeyValuePair<string, int>(null, 0), kvpArray[0]);
-            Assert.Equal(new KeyValuePair<string, int>("hello", 123), kvpArray[1]);
-            Assert.Equal(new KeyValuePair<string, int>("world", 456), kvpArray[2]);
+            Assert.True(
+                (kvpArray[1].Equals(new KeyValuePair<string, int>("hello", 123)) && kvpArray[2].Equals(new KeyValuePair<string, int>("world", 456))) ||
+                (kvpArray[2].Equals(new KeyValuePair<string, int>("hello", 123)) && kvpArray[1].Equals(new KeyValuePair<string, int>("world", 456))));
             Assert.Equal(new KeyValuePair<string, int>(null, 0), kvpArray[3]);
 
             var deArray = new DictionaryEntry[4];
             ((ICollection)frozen).CopyTo(deArray, 2);
             Assert.Equal(new DictionaryEntry(null, null), deArray[0]);
             Assert.Equal(new DictionaryEntry(null, null), deArray[1]);
-            Assert.Equal(new DictionaryEntry("hello", 123), deArray[2]);
-            Assert.Equal(new DictionaryEntry("world", 456), deArray[3]);
+            Assert.True(
+                (deArray[2].Equals(new DictionaryEntry("hello", 123)) && deArray[3].Equals(new DictionaryEntry("world", 456))) ||
+                (deArray[3].Equals(new DictionaryEntry("hello", 123)) && deArray[2].Equals(new DictionaryEntry("world", 456))));
         }
 
         [Fact]
index e0358a2..7c67f33 100644 (file)
@@ -114,6 +114,7 @@ namespace System.Collections.Frozen.Tests
 
                 // from https://raw.githubusercontent.com/dotnet/roslyn/0456b4adc6939e366e7c509318b3ac6a85cda496/src/Compilers/CSharp/Test/Emit2/CodeGen/CodeGenLengthBasedSwitchTests.cs
                 new[] { "", "a", "b", "c", "no", "yes", "four", "alice", "blurb", "hello", "lamps", "lambs", "lower", "names", "slurp", "towed", "words" },
+                new[] { "", "a", "b", "c", "no", "yes", "four", "alice", "blurb", "hello", "lamps", "lambs", "lower", "names", "slurp", "towed", "words", "\u03BB" }, // plus a non-ASCII char
                 new[] { "", "a", "b", "c", "no", "yes", "four", "alice", "blurb", "hello" },
                 new[] { "abcdefgh", "abcdefg", "abcdef", "abcde", "abcd", "abc", "ab", "a" },
                 Enumerable.Range(0, 100).Select(i => i.ToString("D2")).ToArray(),
@@ -142,6 +143,7 @@ namespace System.Collections.Frozen.Tests
                 Enumerable.Range(0, 10).Select(i => $"ABCDEFGH\U0001F600{i}").ToArray(), // right justified single char non-ascii
                 Enumerable.Range(0, 100).Select(i => $"{i:D2}ABCDEFGH\U0001F600").ToArray(), // left justified substring non-ascii
                 Enumerable.Range(0, 100).Select(i => $"ABCDEFGH\U0001F600{i:D2}").ToArray(), // right justified substring non-ascii
+                Enumerable.Range(0, 20).Select(i => i.ToString("D2")).Select(s => (char)(s[0] + 128) + "" + (char)(s[1] + 128)).ToArray(), // left-justified non-ascii
             }
             select new object[] { optimizeForReading, keys.ToDictionary(i => i, i => i, comparer) };
 
index a1190c0..638309c 100644 (file)
@@ -31,6 +31,8 @@ namespace System.Collections.Frozen.Tests
         protected override ISet<T> GenericISetFactory(int count) =>
             GenericISetFactory(count, optimizeForReading: true);
 
+        protected virtual bool TestLargeSizes => true;
+
         protected virtual ISet<T> GenericISetFactory(int count, bool optimizeForReading)
         {
             var s = new HashSet<T>();
@@ -48,7 +50,10 @@ namespace System.Collections.Frozen.Tests
         [InlineData(100_000, true)]
         public void CreateVeryLargeSet_Success(int largeCount, bool optimizeForReading)
         {
-            GenericISetFactory(largeCount, optimizeForReading);
+            if (TestLargeSizes)
+            {
+                GenericISetFactory(largeCount, optimizeForReading);
+            }
         }
 
         [Fact]
@@ -221,6 +226,11 @@ namespace System.Collections.Frozen.Tests
         [InlineData(5000, false)]
         public void ComparingWithOtherSets(int size, bool optimizeForReading)
         {
+            if (size > 10 && !TestLargeSizes)
+            {
+                return;
+            }
+
             foreach (IEqualityComparer<T> comparer in new IEqualityComparer<T>[] { EqualityComparer<T>.Default })//, NonDefaultEqualityComparer<T>.Instance })
             {
                 IEqualityComparer<T> otherComparer = ReferenceEquals(comparer, EqualityComparer<T>.Default) ? NonDefaultEqualityComparer<T>.Instance : EqualityComparer<T>.Default;
@@ -423,6 +433,13 @@ namespace System.Collections.Frozen.Tests
         }
     }
 
+    public class FrozenSet_Generic_Tests_SimpleStruct : FrozenSet_Generic_Tests<SimpleStruct>
+    {
+        protected override SimpleStruct CreateT(int seed) => new SimpleStruct { Value = seed + 1 };
+
+        protected override bool TestLargeSizes => false; // hash code contention leads to longer running times
+    }
+
     public class FrozenSet_NonGeneric_Tests : ICollection_NonGeneric_Tests
     {
         protected override ICollection NonGenericICollectionFactory() =>