Speed up frozen string dictionaries and sets by ~25-30% (#81021)
authorMartin Taillefer <geeknoid@users.noreply.github.com>
Tue, 24 Jan 2023 11:54:43 +0000 (03:54 -0800)
committerGitHub <noreply@github.com>
Tue, 24 Jan 2023 11:54:43 +0000 (03:54 -0800)
- Replace the specialized comparer types with specialized dictionary
and set types, to avoid virtual dispatch overhead and enable better
inlining.

Co-authored-by: Martin Taillefer <mataille@microsoft.com>
49 files changed:
src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSetInternalBase.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenDictionary.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/LengthBucketsFrozenSet.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/Hashing.cs [moved from src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/StringComparerBase.cs with 89% similarity]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/KeyAnalyzer.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_Full.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_FullCaseInsensitive.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedSingleChar.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedSubstring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedSingleChar.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedSubstring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet.cs
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_Full.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_FullCaseInsensitive.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_FullCaseInsensitiveAscii.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstrring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstrring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedSingleChar.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedSubstrring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedSingleChar.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedSubstring.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/ComparerPicker.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveAsciiStringComparer.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveStringComparer.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullStringComparer.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveSubstringComparer.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSingleCharComparer.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSubstringComparer.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveSubstringComparer.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSingleCharComparer.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSubstringComparer.cs [deleted file]
src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/SubstringComparerBase.cs [deleted file]
src/libraries/System.Collections.Immutable/tests/Frozen/ComparerPickerTests.cs [deleted file]
src/libraries/System.Collections.Immutable/tests/Frozen/ComparerTests.cs [deleted file]
src/libraries/System.Collections.Immutable/tests/Frozen/KeyAnalyzerTests.cs [new file with mode: 0644]
src/libraries/System.Collections.Immutable/tests/System.Collections.Immutable.Tests.csproj

index bfa1b92..3e61e24 100644 (file)
@@ -10,6 +10,32 @@ The System.Collections.Immutable library is built-in as part of the shared frame
 
   <ItemGroup>
     <Compile Include="Properties\InternalsVisibleTo.cs" />
+    <Compile Include="System\Collections\Frozen\String\KeyAnalyzer.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" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_LeftJustifiedSingleChar.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring.cs" />
+    <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_FullCaseInsensitiveAscii.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_FullCaseInsensitive.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_Full.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstrring.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstrring.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_LeftJustifiedSubstrring.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_LeftJustifiedSingleChar.cs" />
+    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring.cs" />
+    <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" />
@@ -29,28 +55,12 @@ The System.Collections.Immutable library is built-in as part of the shared frame
     <Compile Include="System\Collections\Frozen\KeysAndValuesFrozenDictionary.cs" />
     <Compile Include="System\Collections\Frozen\LengthBucketsFrozenDictionary.cs" />
     <Compile Include="System\Collections\Frozen\LengthBucketsFrozenSet.cs" />
-    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenDictionary.cs" />
-    <Compile Include="System\Collections\Frozen\String\OrdinalStringFrozenSet.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\Frozen\StringComparers\ComparerPicker.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\FullCaseInsensitiveAsciiStringComparer.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\FullCaseInsensitiveStringComparer.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\FullStringComparer.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\LeftJustifiedCaseInsensitiveSubstringComparer.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\LeftJustifiedSingleCharComparer.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\LeftJustifiedSubstringComparer.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\RightJustifiedCaseInsensitiveSubstringComparer.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\RightJustifiedSingleCharComparer.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\RightJustifiedSubstringComparer.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\StringComparerBase.cs" />
-    <Compile Include="System\Collections\Frozen\StringComparers\SubstringComparerBase.cs" />
 
     <Compile Include="System\Collections\Generic\IHashKeyCollection.cs" />
     <Compile Include="System\Collections\Generic\ISortKeyCollection.cs" />
@@ -132,7 +142,7 @@ The System.Collections.Immutable library is built-in as part of the shared frame
     <None Include="Interfaces.cd" />
   </ItemGroup>
 
-  <ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))" >
+  <ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
     <Compile Include="System\Collections\Frozen\Integer\IntegerFrozenDictionary.cs" />
     <Compile Include="System\Collections\Frozen\Integer\IntegerFrozenSet.cs" />
     <Compile Include="System\Collections\Frozen\Integer\SmallIntegerFrozenDictionary.cs" />
@@ -140,7 +150,7 @@ The System.Collections.Immutable library is built-in as part of the shared frame
     <Compile Include="System\Collections\Frozen\Integer\SparseRangeIntegerFrozenSet.cs" />
   </ItemGroup>
 
-  <ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))" >
+  <ItemGroup Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
     <Compile Include="System\Collections\Frozen\Int32\SmallInt32FrozenDictionary.cs" />
     <Compile Include="System\Collections\Frozen\Int32\SmallInt32FrozenSet.cs" />
     <Compile Include="System\Collections\Frozen\Int32\SparseRangeInt32FrozenSet.cs" />
index 55ff5de..6a468bd 100644 (file)
@@ -206,9 +206,61 @@ namespace System.Collections.Frozen
                     Dictionary<string, TValue> stringEntries = (Dictionary<string, TValue>)(object)source;
                     IEqualityComparer<string> stringComparer = (IEqualityComparer<string>)(object)comparer;
 
-                    FrozenDictionary<string, TValue> frozenDictionary =
-                        LengthBucketsFrozenDictionary<TValue>.TryCreateLengthBucketsFrozenSet(stringEntries, stringComparer) ??
-                        (FrozenDictionary<string, TValue>)new OrdinalStringFrozenDictionary<TValue>(stringEntries, stringComparer);
+                    FrozenDictionary<string, TValue>? frozenDictionary = LengthBucketsFrozenDictionary<TValue>.CreateLengthBucketsFrozenDictionaryIfAppropriate(stringEntries, stringComparer);
+                    if (frozenDictionary is not null)
+                    {
+                        return (FrozenDictionary<TKey, TValue>)(object)frozenDictionary;
+                    }
+
+                    var entries = (string[])(object)source.Keys.ToArray();
+
+                    KeyAnalyzer.Analyze(entries, ReferenceEquals(stringComparer, StringComparer.OrdinalIgnoreCase), out KeyAnalyzer.AnalysisResults results);
+                    if (results.SubstringHashing)
+                    {
+                        if (results.RightJustifiedSubstring)
+                        {
+                            if (results.IgnoreCase)
+                            {
+                                frozenDictionary = results.AllAscii
+                                    ? new OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount)
+                                    : new OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+                            }
+                            else
+                            {
+                                frozenDictionary = results.HashCount == 1
+                                    ? new OrdinalStringFrozenDictionary_RightJustifiedSingleChar<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex)
+                                    : new OrdinalStringFrozenDictionary_RightJustifiedSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+                            }
+                        }
+                        else
+                        {
+                            if (results.IgnoreCase)
+                            {
+                                frozenDictionary = results.AllAscii
+                                    ? new OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount)
+                                    : new OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+                            }
+                            else
+                            {
+                                    frozenDictionary = results.HashCount == 1
+                                        ? new OrdinalStringFrozenDictionary_LeftJustifiedSingleChar<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex)
+                                        : new OrdinalStringFrozenDictionary_LeftJustifiedSubstring<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+                            }
+                        }
+                    }
+                    else
+                    {
+                        if (results.IgnoreCase)
+                        {
+                            frozenDictionary = results.AllAscii
+                                ? new OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff)
+                                : new OrdinalStringFrozenDictionary_FullCaseInsensitive<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff);
+                        }
+                        else
+                        {
+                            frozenDictionary = new OrdinalStringFrozenDictionary_Full<TValue>(stringEntries, entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff);
+                        }
+                    }
 
                     return (FrozenDictionary<TKey, TValue>)(object)frozenDictionary;
                 }
index dcc7ea8..c519278 100644 (file)
@@ -168,11 +168,64 @@ namespace System.Collections.Frozen
                         ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase))
                     {
                         HashSet<string> stringValues = (HashSet<string>)(object)uniqueValues;
+                        string[] entries = new string[stringValues.Count];
+                        stringValues.CopyTo(entries);
+
                         IEqualityComparer<string> stringComparer = (IEqualityComparer<string>)(object)comparer;
 
-                        FrozenSet<string> frozenSet =
-                            LengthBucketsFrozenSet.TryCreateLengthBucketsFrozenSet(stringValues, stringComparer) ??
-                            (FrozenSet<string>)new OrdinalStringFrozenSet(stringValues, stringComparer);
+                        FrozenSet<string>? frozenSet = LengthBucketsFrozenSet.CreateLengthBucketsFrozenSetIfAppropriate(entries, stringComparer);
+                        if (frozenSet is not null)
+                        {
+                            return (FrozenSet<T>)(object)frozenSet;
+                        }
+
+                        KeyAnalyzer.Analyze(entries, ReferenceEquals(stringComparer, StringComparer.OrdinalIgnoreCase), out KeyAnalyzer.AnalysisResults results);
+                        if (results.SubstringHashing)
+                        {
+                            if (results.RightJustifiedSubstring)
+                            {
+                                if (results.IgnoreCase)
+                                {
+                                    frozenSet = results.AllAscii
+                                        ? new OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount)
+                                        : new OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+                                }
+                                else
+                                {
+                                    frozenSet = results.HashCount == 1
+                                        ? new OrdinalStringFrozenSet_RightJustifiedSingleChar(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex)
+                                        : new OrdinalStringFrozenSet_RightJustifiedSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+                                }
+                            }
+                            else
+                            {
+                                if (results.IgnoreCase)
+                                {
+                                    frozenSet = results.AllAscii
+                                        ? new OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount)
+                                        : new OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+                                }
+                                else
+                                {
+                                    frozenSet = results.HashCount == 1
+                                        ? new OrdinalStringFrozenSet_LeftJustifiedSingleChar(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex)
+                                        : new OrdinalStringFrozenSet_LeftJustifiedSubstring(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff, results.HashIndex, results.HashCount);
+                                }
+                            }
+                        }
+                        else
+                        {
+                            if (results.IgnoreCase)
+                            {
+                                frozenSet = results.AllAscii
+                                    ? new OrdinalStringFrozenSet_FullCaseInsensitiveAscii(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff)
+                                    : new OrdinalStringFrozenSet_FullCaseInsensitive(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff);
+                            }
+                            else
+                            {
+                                frozenSet = new OrdinalStringFrozenSet_Full(entries, stringComparer, results.MinimumLength, results.MaximumLengthDiff);
+                            }
+                        }
 
                         return (FrozenSet<T>)(object)frozenSet;
                     }
index eae06ec..088c21b 100644 (file)
@@ -161,7 +161,7 @@ namespace System.Collections.Frozen
             other switch
             {
                 HashSet<T> hs => _thisSet.Comparer.Equals(hs.Comparer),
-                SortedSet<T> ss => _thisSet.Comparer.Equals(ss.Comparer),
+                SortedSet<T> sortedSet => _thisSet.Comparer.Equals(sortedSet.Comparer),
                 ImmutableHashSet<T> ihs => _thisSet.Comparer.Equals(ihs.KeyComparer),
                 ImmutableSortedSet<T> iss => _thisSet.Comparer.Equals(iss.KeyComparer),
                 FrozenSet<T> fs => _thisSet.Comparer.Equals(fs.Comparer),
index d824cc8..c5c4d35 100644 (file)
@@ -35,7 +35,7 @@ namespace System.Collections.Frozen
             _ignoreCase = ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase);
         }
 
-        internal static LengthBucketsFrozenDictionary<TValue>? TryCreateLengthBucketsFrozenSet(Dictionary<string, TValue> source, IEqualityComparer<string> comparer)
+        internal static LengthBucketsFrozenDictionary<TValue>? CreateLengthBucketsFrozenDictionaryIfAppropriate(Dictionary<string, TValue> source, IEqualityComparer<string> comparer)
         {
             Debug.Assert(source.Count != 0);
             Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
index d561db2..71dc38d 100644 (file)
@@ -31,15 +31,15 @@ namespace System.Collections.Frozen
             _ignoreCase = ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase);
         }
 
-        internal static LengthBucketsFrozenSet? TryCreateLengthBucketsFrozenSet(HashSet<string> source, IEqualityComparer<string> comparer)
+        internal static LengthBucketsFrozenSet? CreateLengthBucketsFrozenSetIfAppropriate(string[] entries, IEqualityComparer<string> comparer)
         {
-            Debug.Assert(source.Count != 0);
+            Debug.Assert(entries.Length != 0);
             Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
 
             // Iterate through all of the inputs, bucketing them based on the length of the string.
             var groupedByLength = new Dictionary<int, List<string>>();
             int minLength = int.MaxValue, maxLength = int.MinValue;
-            foreach (string s in source)
+            foreach (string s in entries)
             {
                 Debug.Assert(s is not null, "This implementation should not be used with null source values.");
 
@@ -67,7 +67,6 @@ namespace System.Collections.Frozen
                 return null;
             }
 
-            string[] items = new string[source.Count];
             var lengthBuckets = new KeyValuePair<string, int>[maxLength - minLength + 1][];
 
             // Iterate through each bucket, filling the items array, and creating a lookup array such that
@@ -81,14 +80,14 @@ namespace System.Collections.Frozen
                 foreach (string value in group.Value)
                 {
                     length[i] = new KeyValuePair<string, int>(value, index);
-                    items[index] = value;
+                    entries[index] = value;
 
                     i++;
                     index++;
                 }
             }
 
-            return new LengthBucketsFrozenSet(items, lengthBuckets, minLength, comparer);
+            return new LengthBucketsFrozenSet(entries, lengthBuckets, minLength, comparer);
         }
 
         /// <inheritdoc />
@@ -2,19 +2,17 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System.Buffers;
-using System.Collections.Generic;
 using System.Numerics;
 using System.Runtime.InteropServices;
 
 namespace System.Collections.Frozen
 {
-    // We define this rather than using IEqualityComparer<string>, since virtual dispatch is faster than interface dispatch
-    internal abstract class StringComparerBase : EqualityComparer<string>
+    internal static class Hashing
     {
         // TODO https://github.com/dotnet/runtime/issues/77679:
         // Replace these once non-randomized implementations are available.
 
-        protected static unsafe int GetHashCodeOrdinal(ReadOnlySpan<char> s)
+        public static unsafe int GetHashCodeOrdinal(ReadOnlySpan<char> s)
         {
             int length = s.Length;
             fixed (char* src = &MemoryMarshal.GetReference(s))
@@ -42,7 +40,7 @@ namespace System.Collections.Frozen
         }
 
         // useful if the string only contains ASCII characters
-        protected static unsafe int GetHashCodeOrdinalIgnoreCaseAscii(ReadOnlySpan<char> s)
+        public static unsafe int GetHashCodeOrdinalIgnoreCaseAscii(ReadOnlySpan<char> s)
         {
             int length = s.Length;
             fixed (char* src = &MemoryMarshal.GetReference(s))
@@ -75,7 +73,7 @@ namespace System.Collections.Frozen
             }
         }
 
-        protected static unsafe int GetHashCodeOrdinalIgnoreCase(ReadOnlySpan<char> s)
+        public static unsafe int GetHashCodeOrdinalIgnoreCase(ReadOnlySpan<char> s)
         {
             int length = s.Length;
 
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/KeyAnalyzer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/KeyAnalyzer.cs
new file mode 100644 (file)
index 0000000..f554b01
--- /dev/null
@@ -0,0 +1,249 @@
+// 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.Runtime.CompilerServices;
+
+namespace System.Collections.Frozen
+{
+    internal static class KeyAnalyzer
+    {
+        /// <summary>
+        /// Look for well-known patterns we can optimize for in a set of dictionary or set keys.
+        /// </summary>
+        /// <remarks>
+        /// The idea here is to find the shortest substring slice across all the input strings which yields a set of
+        /// strings which are maximally unique. The optimal slice is then applied to incoming strings being hashed to
+        /// perform dictionary/set lookups. Keeping the slices as small as possible minimizes the number of characters
+        /// involved in hashing, speeding up the whole process.
+        ///
+        /// What we do here is pretty simple. We loop over the input strings, looking for the shortest slice with a good
+        /// enough uniqueness factor. We look at all the strings both left-justified and right-justified as this maximizes
+        /// the opportunities to find unique slices, especially in the case of many strings with the same prefix or suffix.
+        ///
+        /// In whatever slice we end up with, if all the characters involved in the slice are ASCII and we're doing case-insensitive
+        /// operations, then we can select an ASCII-specific case-insensitive comparer which yields faster overall performance.
+        /// </remarks>
+        public static void Analyze(ReadOnlySpan<string> uniqueStrings, bool ignoreCase, out AnalysisResults results)
+        {
+            // First, try to pick a substring comparer.
+            // if we can't find a good substring comparer, fallback to a full string comparer.
+            if (!UseSubstring(uniqueStrings, ignoreCase, out results))
+            {
+                UseFullString(uniqueStrings, ignoreCase, out results);
+            }
+
+            // Calculate the trivial rejection boundaries.
+            int min = int.MaxValue, max = 0;
+            foreach (string s in uniqueStrings)
+            {
+                if (s.Length < min)
+                {
+                    min = s.Length;
+                }
+
+                if (s.Length > max)
+                {
+                    max = s.Length;
+                }
+            }
+
+            results.MinimumLength = min;
+            results.MaximumLengthDiff = max - min;
+        }
+
+        private static bool UseSubstring(ReadOnlySpan<string> uniqueStrings, bool ignoreCase, out AnalysisResults results)
+        {
+            const double SufficientUniquenessFactor = 0.95; // 95% is good enough
+
+            // What is the shortest string? This represents the maximum substring length we consider
+            int maxSubstringLength = int.MaxValue;
+            foreach (string s in uniqueStrings)
+            {
+                if (s.Length < maxSubstringLength)
+                {
+                    maxSubstringLength = s.Length;
+                }
+            }
+
+            SubstringComparer leftComparer = ignoreCase ? new LeftJustifiedCaseInsensitiveSubstringComparer() : new LeftJustifiedSubstringComparer();
+            SubstringComparer rightComparer = ignoreCase ? new RightJustifiedCaseInsensitiveSubstringComparer() : new RightJustifiedSubstringComparer();
+
+            // try to find the minimal unique substring to use for comparisons
+            var leftSet = new HashSet<string>(leftComparer);
+            var rightSet = new HashSet<string>(rightComparer);
+            for (int count = 1; count <= maxSubstringLength; count++)
+            {
+                for (int index = 0; index <= maxSubstringLength - count; index++)
+                {
+                    leftComparer.Index = index;
+                    leftComparer.Count = count;
+
+                    double factor = GetUniquenessFactor(leftSet, uniqueStrings);
+                    if (factor >= SufficientUniquenessFactor)
+                    {
+                        bool allAscii = true;
+                        foreach (string s in uniqueStrings)
+                        {
+                            if (!IsAllAscii(s.AsSpan(leftComparer.Index, leftComparer.Count)))
+                            {
+                                allAscii = false;
+                                break;
+                            }
+                        }
+
+                        results = new(allAscii, ignoreCase, 0, 0, leftComparer.Index, leftComparer.Count);
+                        return true;
+                    }
+
+                    rightComparer.Index = -index - count;
+                    rightComparer.Count = count;
+
+                    factor = GetUniquenessFactor(rightSet, uniqueStrings);
+                    if (factor >= SufficientUniquenessFactor)
+                    {
+                        bool allAscii = true;
+                        foreach (string s in uniqueStrings)
+                        {
+                            if (!IsAllAscii(s.AsSpan(s.Length + rightComparer.Index, rightComparer.Count)))
+                            {
+                                allAscii = false;
+                                break;
+                            }
+                        }
+
+                        results = new(allAscii, ignoreCase, 0, 0, rightComparer.Index, rightComparer.Count);
+                        return true;
+                    }
+                }
+            }
+
+            results = default;
+            return false;
+        }
+
+        private static void UseFullString(ReadOnlySpan<string> uniqueStrings, bool ignoreCase, out AnalysisResults results)
+        {
+            bool allAscii = true;
+            foreach (string s in uniqueStrings)
+            {
+                if (!IsAllAscii(s.AsSpan()))
+                {
+                    allAscii = false;
+                    break;
+                }
+            }
+
+            results = new(allAscii, ignoreCase, 0, 0, 0, 0);
+        }
+
+        // TODO https://github.com/dotnet/runtime/issues/28230:
+        // Replace this once Ascii.IsValid exists.
+        internal static unsafe bool IsAllAscii(ReadOnlySpan<char> s)
+        {
+            fixed (char* src = s)
+            {
+                uint* ptrUInt32 = (uint*)src;
+                int length = s.Length;
+
+                while (length > 3)
+                {
+                    if (!AllCharsInUInt32AreAscii(ptrUInt32[0] | ptrUInt32[1]))
+                    {
+                        return false;
+                    }
+
+                    ptrUInt32 += 2;
+                    length -= 4;
+                }
+
+                char* ptrChar = (char*)ptrUInt32;
+                while (length-- > 0)
+                {
+                    char ch = *ptrChar++;
+                    if (ch >= 0x7f)
+                    {
+                        return false;
+                    }
+                }
+            }
+
+            return true;
+
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            static bool AllCharsInUInt32AreAscii(uint value) => (value & ~0x007F_007Fu) == 0;
+        }
+
+        private static double GetUniquenessFactor(HashSet<string> set, ReadOnlySpan<string> uniqueStrings)
+        {
+            set.Clear();
+            foreach (string s in uniqueStrings)
+            {
+                set.Add(s);
+            }
+
+            return set.Count / (double)uniqueStrings.Length;
+        }
+
+        internal struct AnalysisResults
+        {
+            public AnalysisResults(
+                bool allAscii,
+                bool ignoreCase,
+                int minimumLength,
+                int maximumLengthDiff,
+                int hashIndex,
+                int hashCount)
+            {
+                AllAscii = allAscii;
+                IgnoreCase = ignoreCase;
+                MinimumLength = minimumLength;
+                MaximumLengthDiff = maximumLengthDiff;
+                HashIndex = hashIndex;
+                HashCount = hashCount;
+            }
+
+            public bool AllAscii { get; }
+            public bool IgnoreCase { get; }
+            public int MinimumLength { get; set; }
+            public int MaximumLengthDiff { get; set; }
+            public int HashIndex { get; }
+            public int HashCount { get; }
+
+            public bool SubstringHashing => HashCount != 0;
+            public bool RightJustifiedSubstring => HashIndex < 0;
+        }
+
+        private abstract class SubstringComparer : IEqualityComparer<string>
+        {
+            public int Index;
+            public int Count;
+            public abstract bool Equals(string? x, string? y);
+            public abstract int GetHashCode(string s);
+        }
+
+        private sealed class LeftJustifiedSubstringComparer : SubstringComparer
+        {
+            public override bool Equals(string? x, string? y) => x.AsSpan(Index, Count).SequenceEqual(y.AsSpan(Index, Count));
+            public override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(Index, Count));
+        }
+
+        private sealed class LeftJustifiedCaseInsensitiveSubstringComparer : SubstringComparer
+        {
+            public override bool Equals(string? x, string? y) => x.AsSpan(Index, Count).Equals(y.AsSpan(Index, Count), StringComparison.OrdinalIgnoreCase);
+            public override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(Index, Count));
+        }
+
+        private sealed class RightJustifiedSubstringComparer : SubstringComparer
+        {
+            public override bool Equals(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).SequenceEqual(y.AsSpan(y!.Length + Index, Count));
+            public override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(s.Length + Index, Count));
+        }
+
+        private sealed class RightJustifiedCaseInsensitiveSubstringComparer : SubstringComparer
+        {
+            public override bool Equals(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).Equals(y.AsSpan(y!.Length + Index, Count), StringComparison.OrdinalIgnoreCase);
+            public override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(s.Length + Index, Count));
+        }
+    }
+}
index 5d5b71e..4ff5c90 100644 (file)
@@ -2,24 +2,28 @@
 // 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.Runtime.CompilerServices;
 
 namespace System.Collections.Frozen
 {
-    /// <summary>Provides a frozen dictionary optimized for ordinal (case-sensitive or case-insensitive) lookup of strings.</summary>
-    /// <typeparam name="TValue">The type of values in the dictionary.</typeparam>
-    internal sealed class OrdinalStringFrozenDictionary<TValue> : FrozenDictionary<string, TValue>
+    /// <summary>The base class for the specialized frozen string dictionaries.</summary>
+    internal abstract class OrdinalStringFrozenDictionary<TValue> : FrozenDictionary<string, TValue>
     {
         private readonly FrozenHashTable _hashTable;
         private readonly string[] _keys;
         private readonly TValue[] _values;
-        private readonly StringComparerBase _partialComparer;
         private readonly int _minimumLength;
         private readonly int _maximumLengthDiff;
 
-        internal OrdinalStringFrozenDictionary(Dictionary<string, TValue> source, IEqualityComparer<string> comparer) :
+        internal OrdinalStringFrozenDictionary(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex = -1,
+            int hashCount = -1) :
             base(comparer)
         {
             Debug.Assert(source.Count != 0);
@@ -28,18 +32,17 @@ namespace System.Collections.Frozen
             var entries = new KeyValuePair<string, TValue>[source.Count];
             ((ICollection<KeyValuePair<string, TValue>>)source).CopyTo(entries, 0);
 
-            _keys = new string[entries.Length];
+            _keys = keys;
             _values = new TValue[entries.Length];
+            _minimumLength = minimumLength;
+            _maximumLengthDiff = maximumLengthDiff;
 
-            _partialComparer = ComparerPicker.Pick(
-                Array.ConvertAll(entries, pair => pair.Key),
-                ignoreCase: ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase),
-                out _minimumLength,
-                out _maximumLengthDiff);
+            HashIndex = hashIndex;
+            HashCount = hashCount;
 
             _hashTable = FrozenHashTable.Create(
                 entries,
-                pair => _partialComparer.GetHashCode(pair.Key),
+                pair => GetHashCode(pair.Key),
                 (index, pair) =>
                 {
                     _keys[index] = pair.Key;
@@ -47,33 +50,28 @@ namespace System.Collections.Frozen
                 });
         }
 
-        /// <inheritdoc />
+        private protected int HashIndex { get; }
+        private protected int HashCount { get; }
+        private protected abstract bool Equals(string? x, string? y);
+        private protected abstract int GetHashCode(string s);
         private protected override string[] KeysCore => _keys;
-
-        /// <inheritdoc />
         private protected override TValue[] ValuesCore => _values;
-
-        /// <inheritdoc />
         private protected override Enumerator GetEnumeratorCore() => new Enumerator(_keys, _values);
-
-        /// <inheritdoc />
         private protected override int CountCore => _hashTable.Count;
 
-        /// <inheritdoc />
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private protected override ref readonly TValue GetValueRefOrNullRefCore(string key)
         {
             if ((uint)(key.Length - _minimumLength) <= (uint)_maximumLengthDiff)
             {
-                StringComparerBase partialComparer = _partialComparer;
-
-                int hashCode = partialComparer.GetHashCode(key);
+                int hashCode = GetHashCode(key);
                 _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex);
 
                 while (index <= endIndex)
                 {
                     if (hashCode == _hashTable.HashCodes[index])
                     {
-                        if (partialComparer.Equals(key, _keys[index])) // partialComparer.Equals always compares the full input (EqualsPartial/GetHashCode don't)
+                        if (Equals(key, _keys[index]))
                         {
                             return ref _values[index];
                         }
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_Full.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_Full.cs
new file mode 100644 (file)
index 0000000..9ef93ca
--- /dev/null
@@ -0,0 +1,28 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenDictionary_Full<TValue> : OrdinalStringFrozenDictionary<TValue>
+    {
+        internal OrdinalStringFrozenDictionary_Full(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff)
+            : base(source, keys, comparer, minimumLength, maximumLengthDiff)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+        private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan());
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_FullCaseInsensitive.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_FullCaseInsensitive.cs
new file mode 100644 (file)
index 0000000..2a78207
--- /dev/null
@@ -0,0 +1,30 @@
+// 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.Runtime.CompilerServices;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenDictionary_FullCaseInsensitive<TValue> : OrdinalStringFrozenDictionary<TValue>
+    {
+        internal OrdinalStringFrozenDictionary_FullCaseInsensitive(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff)
+            : base(source, keys, comparer, minimumLength, maximumLengthDiff)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan());
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii.cs
new file mode 100644 (file)
index 0000000..c64c13c
--- /dev/null
@@ -0,0 +1,30 @@
+// 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.Runtime.CompilerServices;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii<TValue> : OrdinalStringFrozenDictionary<TValue>
+    {
+        internal OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff)
+            : base(source, keys, comparer, minimumLength, maximumLengthDiff)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan());
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring.cs
new file mode 100644 (file)
index 0000000..166ae6a
--- /dev/null
@@ -0,0 +1,30 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+    {
+        internal OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(HashIndex, HashCount));
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring.cs
new file mode 100644 (file)
index 0000000..dad71c4
--- /dev/null
@@ -0,0 +1,30 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+    {
+        internal OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(HashIndex, HashCount));
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedSingleChar.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedSingleChar.cs
new file mode 100644 (file)
index 0000000..ca03320
--- /dev/null
@@ -0,0 +1,29 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenDictionary_LeftJustifiedSingleChar<TValue> : OrdinalStringFrozenDictionary<TValue>
+    {
+        internal OrdinalStringFrozenDictionary_LeftJustifiedSingleChar(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex)
+            : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, 1)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+        private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+        private protected override int GetHashCode(string s) => s[HashIndex];
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedSubstring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedSubstring.cs
new file mode 100644 (file)
index 0000000..d48e473
--- /dev/null
@@ -0,0 +1,30 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenDictionary_LeftJustifiedSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+    {
+        internal OrdinalStringFrozenDictionary_LeftJustifiedSubstring(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+        private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(HashIndex, HashCount));
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring.cs
new file mode 100644 (file)
index 0000000..b26d2a1
--- /dev/null
@@ -0,0 +1,30 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+    {
+        internal OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(s.Length + HashIndex, HashCount));
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring.cs
new file mode 100644 (file)
index 0000000..fc348bc
--- /dev/null
@@ -0,0 +1,30 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+    {
+        internal OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(s.Length + HashIndex, HashCount));
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedSingleChar.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedSingleChar.cs
new file mode 100644 (file)
index 0000000..60c8ecc
--- /dev/null
@@ -0,0 +1,29 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenDictionary_RightJustifiedSingleChar<TValue> : OrdinalStringFrozenDictionary<TValue>
+    {
+        internal OrdinalStringFrozenDictionary_RightJustifiedSingleChar(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex)
+            : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, 1)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+        private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+        private protected override int GetHashCode(string s) => s[s.Length + HashIndex];
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedSubstring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedSubstring.cs
new file mode 100644 (file)
index 0000000..f4dd7ad
--- /dev/null
@@ -0,0 +1,30 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenDictionary_RightJustifiedSubstring<TValue> : OrdinalStringFrozenDictionary<TValue>
+    {
+        internal OrdinalStringFrozenDictionary_RightJustifiedSubstring(
+            Dictionary<string, TValue> source,
+            string[] keys,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(source, keys, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override ref readonly TValue GetValueRefOrNullRefCore(string key) => ref base.GetValueRefOrNullRefCore(key);
+
+        private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(s.Length + HashIndex, HashCount));
+    }
+}
index e7bd3e5..87e585c 100644 (file)
@@ -1,69 +1,66 @@
 // 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.Collections.Immutable;
-using System.Diagnostics;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
 
 namespace System.Collections.Frozen
 {
-    /// <summary>Provides a frozen set optimized for ordinal (case-sensitive or case-insensitive) lookup of strings.</summary>
-    internal sealed class OrdinalStringFrozenSet : FrozenSetInternalBase<string, OrdinalStringFrozenSet.GSW>
+    /// <summary>The base class for the specialized frozen string sets.</summary>
+    internal abstract class OrdinalStringFrozenSet : FrozenSetInternalBase<string, OrdinalStringFrozenSet.GSW>
     {
         private readonly FrozenHashTable _hashTable;
         private readonly string[] _items;
-        private readonly StringComparerBase _partialComparer;
         private readonly int _minimumLength;
         private readonly int _maximumLengthDiff;
 
-        internal OrdinalStringFrozenSet(HashSet<string> source, IEqualityComparer<string> comparer) :
-            base(comparer)
+        internal OrdinalStringFrozenSet(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex = -1,
+            int hashCount = -1)
+            : base(comparer)
         {
-            Debug.Assert(source.Count != 0);
-            Debug.Assert(comparer == EqualityComparer<string>.Default || comparer == StringComparer.Ordinal || comparer == StringComparer.OrdinalIgnoreCase);
-
-            string[] entries = new string[source.Count];
-            source.CopyTo(entries);
-
             _items = new string[entries.Length];
+            _minimumLength = minimumLength;
+            _maximumLengthDiff = maximumLengthDiff;
 
-            _partialComparer = ComparerPicker.Pick(
-                entries,
-                ignoreCase: ReferenceEquals(comparer, StringComparer.OrdinalIgnoreCase),
-                out _minimumLength,
-                out _maximumLengthDiff);
+            HashIndex = hashIndex;
+            HashCount = hashCount;
 
             _hashTable = FrozenHashTable.Create(
                 entries,
-                _partialComparer.GetHashCode,
+                GetHashCode,
                 (index, item) => _items[index] = item);
         }
 
-        /// <inheritdoc />
+        private protected int HashIndex { get; }
+        private protected int HashCount { get; }
+        private protected abstract bool Equals(string? x, string? y);
+        private protected abstract int GetHashCode(string s);
         private protected override string[] ItemsCore => _items;
-
-        /// <inheritdoc />
         private protected override Enumerator GetEnumeratorCore() => new Enumerator(_items);
-
-        /// <inheritdoc />
         private protected override int CountCore => _hashTable.Count;
 
-        /// <inheritdoc />
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         private protected override int FindItemIndex(string item)
         {
             if (item is not null && // this implementation won't be used for null values
                 (uint)(item.Length - _minimumLength) <= (uint)_maximumLengthDiff)
             {
-                StringComparerBase partialComparer = _partialComparer;
-
-                int hashCode = partialComparer.GetHashCode(item);
+                int hashCode = GetHashCode(item);
                 _hashTable.FindMatchingEntries(hashCode, out int index, out int endIndex);
 
                 while (index <= endIndex)
                 {
                     if (hashCode == _hashTable.HashCodes[index])
                     {
-                        if (partialComparer.Equals(item, _items[index])) // partialComparer.Equals always compares the full input (EqualsPartial/GetHashCode don't)
+                        if (Equals(item, _items[index]))
                         {
                             return index;
                         }
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_Full.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_Full.cs
new file mode 100644 (file)
index 0000000..9c10bb2
--- /dev/null
@@ -0,0 +1,27 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenSet_Full : OrdinalStringFrozenSet
+    {
+        internal OrdinalStringFrozenSet_Full(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff)
+            : base(entries, comparer, minimumLength, maximumLengthDiff)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+        private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan());
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_FullCaseInsensitive.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_FullCaseInsensitive.cs
new file mode 100644 (file)
index 0000000..345121f
--- /dev/null
@@ -0,0 +1,27 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenSet_FullCaseInsensitive : OrdinalStringFrozenSet
+    {
+        internal OrdinalStringFrozenSet_FullCaseInsensitive(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff)
+            : base(entries, comparer, minimumLength, maximumLengthDiff)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan());
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_FullCaseInsensitiveAscii.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_FullCaseInsensitiveAscii.cs
new file mode 100644 (file)
index 0000000..b032da0
--- /dev/null
@@ -0,0 +1,27 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenSet_FullCaseInsensitiveAscii : OrdinalStringFrozenSet
+    {
+        internal OrdinalStringFrozenSet_FullCaseInsensitiveAscii(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff)
+            : base(entries, comparer, minimumLength, maximumLengthDiff)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan());
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstrring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstrring.cs
new file mode 100644 (file)
index 0000000..b89d19a
--- /dev/null
@@ -0,0 +1,29 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstring : OrdinalStringFrozenSet
+    {
+        internal OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstring(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(HashIndex, HashCount));
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstrring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstrring.cs
new file mode 100644 (file)
index 0000000..548173e
--- /dev/null
@@ -0,0 +1,29 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstring : OrdinalStringFrozenSet
+    {
+        internal OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstring(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(HashIndex, HashCount));
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedSingleChar.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedSingleChar.cs
new file mode 100644 (file)
index 0000000..b47deea
--- /dev/null
@@ -0,0 +1,28 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenSet_LeftJustifiedSingleChar : OrdinalStringFrozenSet
+    {
+        internal OrdinalStringFrozenSet_LeftJustifiedSingleChar(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex)
+            : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, 1)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+        private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+        private protected override int GetHashCode(string s) => s[HashIndex];
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedSubstrring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedSubstrring.cs
new file mode 100644 (file)
index 0000000..bec754e
--- /dev/null
@@ -0,0 +1,29 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenSet_LeftJustifiedSubstring : OrdinalStringFrozenSet
+    {
+        internal OrdinalStringFrozenSet_LeftJustifiedSubstring(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+        private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(HashIndex, HashCount));
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring.cs
new file mode 100644 (file)
index 0000000..3020cfd
--- /dev/null
@@ -0,0 +1,29 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring : OrdinalStringFrozenSet
+    {
+        internal OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(s.Length + HashIndex, HashCount));
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring.cs
new file mode 100644 (file)
index 0000000..0cb70df
--- /dev/null
@@ -0,0 +1,29 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring : OrdinalStringFrozenSet
+    {
+        internal OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+        private protected override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
+        private protected  override int GetHashCode(string s) => Hashing.GetHashCodeOrdinalIgnoreCase(s.AsSpan(s.Length + HashIndex, HashCount));
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedSingleChar.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedSingleChar.cs
new file mode 100644 (file)
index 0000000..d9f510e
--- /dev/null
@@ -0,0 +1,28 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenSet_RightJustifiedSingleChar : OrdinalStringFrozenSet
+    {
+        internal OrdinalStringFrozenSet_RightJustifiedSingleChar(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex)
+            : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, 1)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+        private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+        private protected override int GetHashCode(string s) => s[s.Length + HashIndex];
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedSubstring.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedSubstring.cs
new file mode 100644 (file)
index 0000000..4cb73df
--- /dev/null
@@ -0,0 +1,29 @@
+// 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;
+
+namespace System.Collections.Frozen
+{
+    internal sealed class OrdinalStringFrozenSet_RightJustifiedSubstring : OrdinalStringFrozenSet
+    {
+        internal OrdinalStringFrozenSet_RightJustifiedSubstring(
+            string[] entries,
+            IEqualityComparer<string> comparer,
+            int minimumLength,
+            int maximumLengthDiff,
+            int hashIndex,
+            int hashCount)
+            : base(entries, comparer, minimumLength, maximumLengthDiff, hashIndex, hashCount)
+        {
+        }
+
+        // This override is necessary to force the jit to emit the code in such a way that it
+        // avoids virtual dispatch overhead when calling the Equals/GetHashCode methods. Don't
+        // remove this, or you'll tank performance.
+        private protected override int FindItemIndex(string item) => base.FindItemIndex(item);
+
+        private protected override bool Equals(string? x, string? y) => string.Equals(x, y);
+        private protected override int GetHashCode(string s) => Hashing.GetHashCodeOrdinal(s.AsSpan(s.Length + HashIndex, HashCount));
+    }
+}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/ComparerPicker.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/ComparerPicker.cs
deleted file mode 100644 (file)
index 2ea18d2..0000000
+++ /dev/null
@@ -1,242 +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.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
-
-namespace System.Collections.Frozen
-{
-    internal static class ComparerPicker
-    {
-        /// <summary>
-        /// Pick an optimal comparer for the set of strings and case-sensitivity mode.
-        /// </summary>
-        /// <remarks>
-        /// The idea here is to find the shortest substring slice across all the input strings which yields a set of
-        /// strings which are maximally unique. The optimal slice is then applied to incoming strings being hashed to
-        /// perform the dictionary lookup. Keeping the slices as small as possible minimizes the number of characters
-        /// involved in hashing, speeding up the whole process.
-        ///
-        /// What we do here is pretty simple. We loop over the input strings, looking for the shortest slice with a good
-        /// enough uniqueness factor. We look at all the strings both left-justified and right-justified as this maximizes
-        /// the opportunities to find unique slices, especially in the case of many strings with the same prefix or suffix.
-        ///
-        /// In whatever slice we end up with, if all the characters involved in the slice are ASCII and we're doing case-insensitive
-        /// operations, then we can select an ASCII-specific case-insensitive comparer which yields faster overall performance.
-        ///
-        /// Warning: This code may reorganize (e.g. sort) the entries in the input array. It will not delete or add anything though.
-        /// </remarks>
-        public static StringComparerBase Pick(ReadOnlySpan<string> uniqueStrings, bool ignoreCase, out int minimumLength, out int maximumLengthDiff)
-        {
-            Debug.Assert(uniqueStrings.Length != 0);
-
-            // First, try to pick a substring comparer.
-            // if we couldn't find a good substring comparer, fallback to a full string comparer.
-            StringComparerBase? c =
-                PickSubstringComparer(uniqueStrings, ignoreCase) ??
-                PickFullStringComparer(uniqueStrings, ignoreCase);
-
-            // Calculate the trivial rejection boundaries.
-            int min = int.MaxValue, max = 0;
-            foreach (string s in uniqueStrings)
-            {
-                if (s.Length < min)
-                {
-                    min = s.Length;
-                }
-
-                if (s.Length > max)
-                {
-                    max = s.Length;
-                }
-            }
-
-            minimumLength = min;
-            maximumLengthDiff = max - min;
-            return c;
-        }
-
-        private static StringComparerBase? PickSubstringComparer(ReadOnlySpan<string> uniqueStrings, bool ignoreCase)
-        {
-            const double SufficientUniquenessFactor = 0.95; // 95% is good enough
-
-            // What is the shortest string? This represent the maximum substring length we consider
-            int maxSubstringLength = int.MaxValue;
-            foreach (string s in uniqueStrings)
-            {
-                if (s.Length < maxSubstringLength)
-                {
-                    maxSubstringLength = s.Length;
-                }
-            }
-
-            SubstringComparerBase leftComparer = ignoreCase ? new LeftJustifiedCaseInsensitiveSubstringComparer() : new LeftJustifiedSubstringComparer();
-            SubstringComparerBase rightComparer = ignoreCase ? new RightJustifiedCaseInsensitiveSubstringComparer() : new RightJustifiedSubstringComparer();
-
-            // try to find the minimal unique substring to use for comparisons
-            var leftSet = new HashSet<string>(new ComparerWrapper(leftComparer));
-            var rightSet = new HashSet<string>(new ComparerWrapper(rightComparer));
-            for (int count = 1; count <= maxSubstringLength; count++)
-            {
-                for (int index = 0; index <= maxSubstringLength - count; index++)
-                {
-                    leftComparer.Index = index;
-                    leftComparer.Count = count;
-
-                    double factor = GetUniquenessFactor(leftSet, uniqueStrings);
-                    if (factor >= SufficientUniquenessFactor)
-                    {
-                        if (ignoreCase)
-                        {
-                            foreach (string ss in uniqueStrings)
-                            {
-                                if (!IsAllAscii(ss.AsSpan(leftComparer.Index, leftComparer.Count)))
-                                {
-                                    // keep the slower non-ascii comparer since we have some non-ascii text
-                                    return leftComparer;
-                                }
-                            }
-
-                            // optimize for all-ascii case
-                            return new LeftJustifiedCaseInsensitiveAsciiSubstringComparer
-                            {
-                                Index = leftComparer.Index,
-                                Count = leftComparer.Count,
-                            };
-                        }
-
-                        // Optimize the single char case
-                        if (leftComparer.Count == 1)
-                        {
-                            return new LeftJustifiedSingleCharComparer
-                            {
-                                Index = leftComparer.Index,
-                                Count = 1,
-                            };
-                        }
-
-                        return leftComparer;
-                    }
-
-                    rightComparer.Index = -index - count;
-                    rightComparer.Count = count;
-
-                    factor = GetUniquenessFactor(rightSet, uniqueStrings);
-                    if (factor >= SufficientUniquenessFactor)
-                    {
-                        if (ignoreCase)
-                        {
-                            foreach (string ss in uniqueStrings)
-                            {
-                                if (!IsAllAscii(ss.AsSpan(ss.Length + rightComparer.Index, rightComparer.Count)))
-                                {
-                                    // keep the slower non-ascii comparer since we have some non-ascii text
-                                    return rightComparer;
-                                }
-                            }
-
-                            // optimize for all-ascii case
-                            return new RightJustifiedCaseInsensitiveAsciiSubstringComparer
-                            {
-                                Index = rightComparer.Index,
-                                Count = rightComparer.Count,
-                            };
-                        }
-
-                        // Optimize the single char case
-                        if (rightComparer.Count == 1)
-                        {
-                            return new RightJustifiedSingleCharComparer
-                            {
-                                Index = rightComparer.Index,
-                                Count = 1,
-                            };
-                        }
-
-                        return rightComparer;
-                    }
-                }
-            }
-
-            return null;
-        }
-
-        private static StringComparerBase PickFullStringComparer(ReadOnlySpan<string> uniqueStrings, bool ignoreCase)
-        {
-            if (!ignoreCase)
-            {
-                return new FullStringComparer();
-            }
-
-            foreach (string s in uniqueStrings)
-            {
-                if (!IsAllAscii(s.AsSpan()))
-                {
-                    return new FullCaseInsensitiveStringComparer();
-                }
-            }
-
-            return new FullCaseInsensitiveAsciiStringComparer();
-        }
-
-        private sealed class ComparerWrapper : IEqualityComparer<string>
-        {
-            private readonly SubstringComparerBase _comp;
-
-            public ComparerWrapper(SubstringComparerBase comp) => _comp = comp;
-
-            public bool Equals(string? x, string? y) => _comp.EqualsPartial(x, y);
-            public int GetHashCode([DisallowNull] string obj) => _comp.GetHashCode(obj);
-        }
-
-        // TODO https://github.com/dotnet/runtime/issues/28230:
-        // Replace this once Ascii.IsValid exists.
-        internal static unsafe bool IsAllAscii(ReadOnlySpan<char> s)
-        {
-            fixed (char* src = s)
-            {
-                uint* ptrUInt32 = (uint*)src;
-                int length = s.Length;
-
-                while (length > 3)
-                {
-                    if (!AllCharsInUInt32AreAscii(ptrUInt32[0] | ptrUInt32[1]))
-                    {
-                        return false;
-                    }
-
-                    ptrUInt32 += 2;
-                    length -= 4;
-                }
-
-                char* ptrChar = (char*)ptrUInt32;
-                while (length-- > 0)
-                {
-                    char ch = *ptrChar++;
-                    if (ch >= 0x7f)
-                    {
-                        return false;
-                    }
-                }
-            }
-
-            return true;
-
-            [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            static bool AllCharsInUInt32AreAscii(uint value) => (value & ~0x007F_007Fu) == 0;
-        }
-
-        private static double GetUniquenessFactor(HashSet<string> set, ReadOnlySpan<string> uniqueStrings)
-        {
-            set.Clear();
-            foreach (string s in uniqueStrings)
-            {
-                set.Add(s);
-            }
-
-            return set.Count / (double)uniqueStrings.Length;
-        }
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveAsciiStringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveAsciiStringComparer.cs
deleted file mode 100644 (file)
index 5c3b60a..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    /// <summary>
-    /// A comparer for ordinal case-insensitive ascii-only string comparisons.
-    /// </summary>
-    /// <remarks>
-    /// This code doesn't perform any error checks on the input as it assumes
-    /// the data is always valid. This is ensured by precondition checks before
-    /// a key is used to perform a dictionary lookup.
-    /// </remarks>
-    internal sealed class FullCaseInsensitiveAsciiStringComparer : StringComparerBase
-    {
-        public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
-        public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan());
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveStringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullCaseInsensitiveStringComparer.cs
deleted file mode 100644 (file)
index 67add01..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    /// <summary>
-    /// A comparer for ordinal case-insensitive string comparisons.
-    /// </summary>
-    /// <remarks>
-    /// This code doesn't perform any error checks on the input as it assumes
-    /// the data is always valid. This is ensured by precondition checks before
-    /// a key is used to perform a dictionary lookup.
-    /// </remarks>
-    internal sealed class FullCaseInsensitiveStringComparer : StringComparerBase
-    {
-        public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
-        public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan());
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullStringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/FullStringComparer.cs
deleted file mode 100644 (file)
index 1c9ead8..0000000
+++ /dev/null
@@ -1,19 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    /// <summary>
-    /// A comparer for ordinal string comparisons.
-    /// </summary>
-    /// <remarks>
-    /// This code doesn't perform any error checks on the input as it assumes
-    /// the data is always valid. This is ensured by precondition checks before
-    /// a key is used to perform a dictionary lookup.
-    /// </remarks>
-    internal sealed class FullStringComparer : StringComparerBase
-    {
-        public override bool Equals(string? x, string? y) => string.Equals(x, y);
-        public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan());
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveAsciiSubstringComparer.cs
deleted file mode 100644 (file)
index 3fa6ba9..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    /// <summary>
-    /// A comparer that operates over a portion of the input strings.
-    /// </summary>
-    /// <remarks>
-    /// This comparer looks from the start of input strings.
-    ///
-    /// This code doesn't perform any error checks on the input as it assumes
-    /// the data is always valid. This is ensured by precondition checks before
-    /// a key is used to perform a dictionary lookup.
-    /// </remarks>
-    internal sealed class LeftJustifiedCaseInsensitiveAsciiSubstringComparer : SubstringComparerBase
-    {
-        public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
-        public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).Equals(y.AsSpan(Index, Count), StringComparison.OrdinalIgnoreCase);
-        public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(Index, Count));
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedCaseInsensitiveSubstringComparer.cs
deleted file mode 100644 (file)
index 6faa142..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    /// <summary>
-    /// A comparer that operates over a portion of the input strings.
-    /// </summary>
-    /// <remarks>
-    /// This comparer looks from the start of input strings.
-    ///
-    /// This code doesn't perform any error checks on the input as it assumes
-    /// the data is always valid. This is ensured by precondition checks before
-    /// a key is used to perform a dictionary lookup.
-    /// </remarks>
-    internal sealed class LeftJustifiedCaseInsensitiveSubstringComparer : SubstringComparerBase
-    {
-        public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
-        public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).Equals(y.AsSpan(Index, Count), StringComparison.OrdinalIgnoreCase);
-        public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan(Index, Count));
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSingleCharComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSingleCharComparer.cs
deleted file mode 100644 (file)
index 43aca98..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    /// <summary>
-    /// A comparer that operates over a single char of the input strings.
-    /// </summary>
-    /// <remarks>
-    /// This comparer looks from the start of input strings.
-    ///
-    /// This code doesn't perform any error checks on the input as it assumes
-    /// the data is always valid. This is ensured by precondition checks before
-    /// a key is used to perform a dictionary lookup.
-    /// </remarks>
-    internal sealed class LeftJustifiedSingleCharComparer : SubstringComparerBase
-    {
-        public override bool Equals(string? x, string? y) => string.Equals(x, y);
-        public override bool EqualsPartial(string? x, string? y) => x![Index] == y![Index];
-        public override int GetHashCode(string s) => s[Index];
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/LeftJustifiedSubstringComparer.cs
deleted file mode 100644 (file)
index 160c524..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    /// <summary>
-    /// A comparer that operates over a portion of the input strings.
-    /// </summary>
-    /// <remarks>
-    /// This comparer looks from the start of input strings.
-    ///
-    /// This code doesn't perform any error checks on the input as it assumes
-    /// the data is always valid. This is ensured by precondition checks before
-    /// a key is used to perform a dictionary lookup.
-    /// </remarks>
-    internal sealed class LeftJustifiedSubstringComparer : SubstringComparerBase
-    {
-        public override bool Equals(string? x, string? y) => string.Equals(x, y);
-        public override bool EqualsPartial(string? x, string? y) => x.AsSpan(Index, Count).SequenceEqual(y.AsSpan(Index, Count));
-        public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan(Index, Count));
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveAsciiSubstringComparer.cs
deleted file mode 100644 (file)
index cb6f72c..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    /// <summary>
-    /// A comparer that operates over a portion of the input strings.
-    /// </summary>
-    /// <remarks>
-    /// This comparer looks at the end of input strings.
-    ///
-    /// This code doesn't perform any error checks on the input as it assumes
-    /// the data is always valid. This is ensured by precondition checks before
-    /// a key is used to perform a dictionary lookup.
-    /// </remarks>
-    internal sealed class RightJustifiedCaseInsensitiveAsciiSubstringComparer : SubstringComparerBase
-    {
-        public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
-        public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).Equals(y.AsSpan(y!.Length + Index, Count), StringComparison.OrdinalIgnoreCase);
-        public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCaseAscii(s.AsSpan(s.Length + Index, Count));
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedCaseInsensitiveSubstringComparer.cs
deleted file mode 100644 (file)
index 37578f6..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    /// <summary>
-    /// A comparer that operates over a portion of the input strings.
-    /// </summary>
-    /// <remarks>
-    /// This comparer looks at the end of input strings.
-    ///
-    /// This code doesn't perform any error checks on the input as it assumes
-    /// the data is always valid. This is ensured by precondition checks before
-    /// a key is used to perform a dictionary lookup.
-    /// </remarks>
-    internal sealed class RightJustifiedCaseInsensitiveSubstringComparer : SubstringComparerBase
-    {
-        public override bool Equals(string? x, string? y) => StringComparer.OrdinalIgnoreCase.Equals(x, y);
-        public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).Equals(y.AsSpan(y!.Length + Index, Count), StringComparison.OrdinalIgnoreCase);
-        public override int GetHashCode(string s) => GetHashCodeOrdinalIgnoreCase(s.AsSpan(s.Length + Index, Count));
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSingleCharComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSingleCharComparer.cs
deleted file mode 100644 (file)
index 2508342..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    /// <summary>
-    /// A comparer that operates over a single character of the input strings.
-    /// </summary>
-    /// <remarks>
-    /// This comparer looks at the end of input strings.
-    ///
-    /// This code doesn't perform any error checks on the input as it assumes
-    /// the data is always valid. This is ensured by precondition checks before
-    /// a key is used to perform a dictionary lookup.
-    /// </remarks>
-    internal sealed class RightJustifiedSingleCharComparer : SubstringComparerBase
-    {
-        public override bool Equals(string? x, string? y) => string.Equals(x, y);
-        public override bool EqualsPartial(string? x, string? y) => x![x.Length + Index] == y![y.Length + Index];
-        public override int GetHashCode(string s) => s[s.Length + Index];
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSubstringComparer.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/RightJustifiedSubstringComparer.cs
deleted file mode 100644 (file)
index 2c1367a..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    /// <summary>
-    /// A comparer that operates over a portion of the input strings.
-    /// </summary>
-    /// <remarks>
-    /// This comparer looks at the end of input strings.
-    ///
-    /// This code doesn't perform any error checks on the input as it assumes
-    /// the data is always valid. This is ensured by precondition checks before
-    /// a key is used to perform a dictionary lookup.
-    /// </remarks>
-    internal sealed class RightJustifiedSubstringComparer : SubstringComparerBase
-    {
-        public override bool Equals(string? x, string? y) => string.Equals(x, y);
-        public override bool EqualsPartial(string? x, string? y) => x.AsSpan(x!.Length + Index, Count).SequenceEqual(y.AsSpan(y!.Length + Index, Count));
-        public override int GetHashCode(string s) => GetHashCodeOrdinal(s.AsSpan(s.Length + Index, Count));
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/SubstringComparerBase.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/SubstringComparerBase.cs
deleted file mode 100644 (file)
index 4f38237..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace System.Collections.Frozen
-{
-    internal abstract class SubstringComparerBase : StringComparerBase
-    {
-        public int Index;
-        public int Count;
-
-        public abstract bool EqualsPartial(string? x, string? y);
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/ComparerPickerTests.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/ComparerPickerTests.cs
deleted file mode 100644 (file)
index df318ca..0000000
+++ /dev/null
@@ -1,172 +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;
-using Xunit;
-
-namespace System.Collections.Frozen.Tests
-{
-    public static class ComparerPickerTests
-    {
-        private static StringComparerBase NewPicker(string[] values, bool ignoreCase)
-        {
-            StringComparerBase c = ComparerPicker.Pick(values, ignoreCase, out int minLen, out int maxLenDiff);
-
-            foreach (string s in values)
-            {
-                Assert.True(s.Length >= minLen);
-                Assert.True(s.Length <= minLen + maxLenDiff);
-            }
-
-            return c;
-        }
-
-        [Fact]
-        public static void LeftHand()
-        {
-            StringComparerBase c = NewPicker(new[] { "K0", "K20", "K300" }, false);
-            Assert.IsType<LeftJustifiedSingleCharComparer>(c);
-            Assert.Equal(1, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "S1" }, false);
-            Assert.IsType<LeftJustifiedSingleCharComparer>(c);
-            Assert.Equal(0, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "S1", "T1" }, false);
-            Assert.IsType<LeftJustifiedSingleCharComparer>(c);
-            Assert.Equal(0, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "SA1", "TA1", "SB1" }, false);
-            Assert.IsType<LeftJustifiedSubstringComparer>(c);
-            Assert.Equal(0, ((SubstringComparerBase)c).Index);
-            Assert.Equal(2, ((SubstringComparerBase)c).Count);
-        }
-
-        [Fact]
-        public static void LeftHandCaseInsensitive()
-        {
-            StringComparerBase c = NewPicker(new[] { "É1" }, true);
-            Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
-            Assert.Equal(0, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "É1", "T1" }, true);
-            Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
-            Assert.Equal(0, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "ÉA1", "TA1", "ÉB1" }, true);
-            Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
-            Assert.Equal(0, ((SubstringComparerBase)c).Index);
-            Assert.Equal(2, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "ABCDEÉ1ABCDEF", "ABCDETA1ABCDEF", "ABCDESB1ABCDEF" }, true);
-            Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
-            Assert.Equal(5, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "ABCDEFÉ1ABCDEF", "ABCDEFTA1ABCDEF", "ABCDEFSB1ABCDEF" }, true);
-            Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
-            Assert.Equal(6, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "ABCÉDEFÉ1ABCDEF", "ABCÉDEFTA1ABCDEF", "ABCÉDEFSB1ABCDEF" }, true);
-            Assert.IsType<LeftJustifiedCaseInsensitiveSubstringComparer>(c);
-            Assert.Equal(7, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-        }
-
-        [Fact]
-        public static void LeftHandCaseInsensitiveAscii()
-        {
-            StringComparerBase c = NewPicker(new[] { "S1" }, true);
-            Assert.IsType<LeftJustifiedCaseInsensitiveAsciiSubstringComparer>(c);
-            Assert.Equal(0, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "S1", "T1" }, true);
-            Assert.IsType<LeftJustifiedCaseInsensitiveAsciiSubstringComparer>(c);
-            Assert.Equal(0, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "SA1", "TA1", "SB1" }, true);
-            Assert.IsType<LeftJustifiedCaseInsensitiveAsciiSubstringComparer>(c);
-            Assert.Equal(0, ((SubstringComparerBase)c).Index);
-            Assert.Equal(2, ((SubstringComparerBase)c).Count);
-        }
-
-        [Fact]
-        public static void RightHand()
-        {
-            StringComparerBase c = NewPicker(new[] { "1S", "1T" }, false);
-            Assert.IsType<RightJustifiedSingleCharComparer>(c);
-            Assert.Equal(-1, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "1AS", "1AT", "1BS" }, false);
-            Assert.IsType<RightJustifiedSubstringComparer>(c);
-            Assert.Equal(-2, ((SubstringComparerBase)c).Index);
-            Assert.Equal(2, ((SubstringComparerBase)c).Count);
-        }
-
-        [Fact]
-        public static void RightHandCaseInsensitive()
-        {
-            StringComparerBase c = NewPicker(new[] { "1É", "1T" }, true);
-            Assert.IsType<RightJustifiedCaseInsensitiveSubstringComparer>(c);
-            Assert.Equal(-1, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "1AÉ", "1AT", "1BÉ" }, true);
-            Assert.IsType<RightJustifiedCaseInsensitiveSubstringComparer>(c);
-            Assert.Equal(-2, ((SubstringComparerBase)c).Index);
-            Assert.Equal(2, ((SubstringComparerBase)c).Count);
-        }
-
-        [Fact]
-        public static void RightHandCaseInsensitiveAscii()
-        {
-            StringComparerBase c = NewPicker(new[] { "1S", "1T" }, true);
-            Assert.IsType<RightJustifiedCaseInsensitiveAsciiSubstringComparer>(c);
-            Assert.Equal(-1, ((SubstringComparerBase)c).Index);
-            Assert.Equal(1, ((SubstringComparerBase)c).Count);
-
-            c = NewPicker(new[] { "1AS", "1AT", "1BS" }, true);
-            Assert.IsType<RightJustifiedCaseInsensitiveAsciiSubstringComparer>(c);
-            Assert.Equal(-2, ((SubstringComparerBase)c).Index);
-            Assert.Equal(2, ((SubstringComparerBase)c).Count);
-        }
-
-        [Fact]
-        public static void Full()
-        {
-            StringComparerBase c = NewPicker(new[] { "ABC", "DBC", "ADC", "ABD", "ABDABD" }, false);
-            Assert.IsType<FullStringComparer>(c);
-        }
-
-        [Fact]
-        public static void FullCaseInsensitive()
-        {
-            StringComparerBase c = NewPicker(new[] { "æbc", "DBC", "æDC", "æbd", "æbdæbd" }, true);
-            Assert.IsType<FullCaseInsensitiveStringComparer>(c);
-        }
-
-        [Fact]
-        public static void FullCaseInsensitiveAscii()
-        {
-            StringComparerBase c = NewPicker(new[] { "abc", "DBC", "aDC", "abd", "abdabd" }, true);
-            Assert.IsType<FullCaseInsensitiveAsciiStringComparer>(c);
-        }
-
-        [Fact]
-        public static void IsAllAscii()
-        {
-            Assert.True(ComparerPicker.IsAllAscii("abc".AsSpan()));
-            Assert.True(ComparerPicker.IsAllAscii("abcdefghij".AsSpan()));
-            Assert.False(ComparerPicker.IsAllAscii("abcdéfghij".AsSpan()));
-        }
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/ComparerTests.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/ComparerTests.cs
deleted file mode 100644 (file)
index be1074b..0000000
+++ /dev/null
@@ -1,340 +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;
-using Xunit;
-
-namespace System.Collections.Frozen.Tests
-{
-    public static class ComparerTests
-    {
-        private static void Equal(SubstringComparerBase c, string a, string b, bool fullEqual)
-        {
-            Assert.True(c.EqualsPartial(a, b));
-            Assert.Equal(c.GetHashCode(a), c.GetHashCode(b));
-            Assert.Equal(fullEqual, c.Equals(a, b));
-        }
-
-        private static void Equal(StringComparerBase c, string a, string b, bool fullEqual)
-        {
-            Assert.Equal(c.GetHashCode(a), c.GetHashCode(b));
-            Assert.Equal(fullEqual, c.Equals(a, b));
-        }
-
-        private static void NotEqual(SubstringComparerBase c, string a, string b)
-        {
-            Assert.False(c.EqualsPartial(a, b));
-            Assert.False(c.Equals(a, b));
-            Assert.NotEqual(c.GetHashCode(a), c.GetHashCode(b));
-        }
-
-        private static void NotEqual(StringComparerBase c, string a, string b)
-        {
-            Assert.False(c.Equals(a, b));
-            Assert.NotEqual(c.GetHashCode(a), c.GetHashCode(b));
-        }
-
-        [Fact]
-        public static void LeftHand()
-        {
-            var c = new LeftJustifiedSubstringComparer
-            {
-                Index = 0,
-                Count = 1
-            };
-
-            Equal(c, "a", "a", true);
-            Equal(c, "a", "aa", false);
-            Equal(c, "a", "ab", false);
-            NotEqual(c, "a", "A");
-            NotEqual(c, "a", "b");
-
-            c.Index = 1;
-            c.Count = 1;
-            Equal(c, "Aa", "Ba", false);
-            Equal(c, "Aa", "Baa", false);
-            Equal(c, "aa", "Bab", false);
-            Equal(c, "Aa", "Aa", true);
-            Equal(c, "Aab", "Aab", true);
-            NotEqual(c, "Aa", "BA");
-            NotEqual(c, "Aa", "Bb");
-
-            c.Index = 1;
-            c.Count = 2;
-            Equal(c, "Aaa", "Baa", false);
-            Equal(c, "Aaa", "Baaa", false);
-            Equal(c, "aaa", "Baab", false);
-            Equal(c, "Aaa", "Aaa", true);
-            Equal(c, "Aaab", "Aaab", true);
-            NotEqual(c, "Aaa", "BaA");
-            NotEqual(c, "Aaa", "Bab");
-        }
-
-        [Fact]
-        public static void LeftHandSingleChar()
-        {
-            var c = new LeftJustifiedSingleCharComparer
-            {
-                Index = 0,
-                Count = 1
-            };
-
-            Equal(c, "a", "a", true);
-            Equal(c, "a", "aa", false);
-            Equal(c, "a", "ab", false);
-            NotEqual(c, "a", "A");
-            NotEqual(c, "a", "b");
-
-            c.Index = 1;
-            c.Count = 1;
-            Equal(c, "Aa", "Ba", false);
-            Equal(c, "Aa", "Baa", false);
-            Equal(c, "aa", "Bab", false);
-            Equal(c, "Aa", "Aa", true);
-            Equal(c, "Aab", "Aab", true);
-            NotEqual(c, "Aa", "BA");
-            NotEqual(c, "Aa", "Bb");
-        }
-
-        [Fact]
-        public static void LeftHandCaseInsensitive()
-        {
-            var c = new LeftJustifiedCaseInsensitiveSubstringComparer
-            {
-                Index = 0,
-                Count = 1
-            };
-
-            Equal(c, "a", "a", true);
-            Equal(c, "a", "A", true);
-            Equal(c, "a", "aa", false);
-            Equal(c, "a", "AA", false);
-            Equal(c, "a", "ab", false);
-            Equal(c, "a", "AB", false);
-            NotEqual(c, "a", "b");
-
-            c.Index = 1;
-            c.Count = 1;
-            Equal(c, "Xa", "Ya", false);
-            Equal(c, "Xa", "YA", false);
-            Equal(c, "Xa", "Xa", true);
-            Equal(c, "Xa", "XA", true);
-            Equal(c, "Xa", "Yaa", false);
-            Equal(c, "Xa", "YAA", false);
-            Equal(c, "Xa", "Yab", false);
-            Equal(c, "Xa", "YAB", false);
-            NotEqual(c, "Xa", "Yb");
-        }
-
-        [Fact]
-        public static void LeftHandCaseInsensitiveAscii()
-        {
-            var c = new LeftJustifiedCaseInsensitiveAsciiSubstringComparer
-            {
-                Index = 0,
-                Count = 1
-            };
-
-            Equal(c, "a", "a", true);
-            Equal(c, "a", "A", true);
-            Equal(c, "a", "aa", false);
-            Equal(c, "a", "AA", false);
-            Equal(c, "a", "ab", false);
-            Equal(c, "a", "AB", false);
-            NotEqual(c, "a", "b");
-
-            c.Index = 1;
-            c.Count = 1;
-            Equal(c, "Xa", "Ya", false);
-            Equal(c, "Xa", "YA", false);
-            Equal(c, "Xa", "Xa", true);
-            Equal(c, "Xa", "XA", true);
-            Equal(c, "Xa", "Yaa", false);
-            Equal(c, "Xa", "YAA", false);
-            Equal(c, "Xa", "Yab", false);
-            Equal(c, "Xa", "YAB", false);
-            NotEqual(c, "Xa", "Yb");
-        }
-
-        [Fact]
-        public static void RightHand()
-        {
-            var c = new RightJustifiedSubstringComparer
-            {
-                Index = -1,
-                Count = 1
-            };
-
-            Equal(c, "a", "a", true);
-            Equal(c, "a", "aa", false);
-            Equal(c, "a", "ba", false);
-            NotEqual(c, "a", "A");
-            NotEqual(c, "a", "b");
-
-            c.Index = -2;
-            c.Count = 1;
-            Equal(c, "aX", "aY", false);
-            Equal(c, "XaX", "YaY", false);
-            Equal(c, "XaX", "YYaY", false);
-            Equal(c, "XXaX", "YaY", false);
-            NotEqual(c, "XXaX", "YYa");
-
-            c.Index = -2;
-            c.Count = 2;
-            Equal(c, "aa", "aa", true);
-            Equal(c, "aa", "aaa", false);
-            Equal(c, "aa", "baa", false);
-            NotEqual(c, "aa", "AA");
-            NotEqual(c, "aa", "bb");
-        }
-
-        [Fact]
-        public static void RightHandSingleChar()
-        {
-            var c = new RightJustifiedSingleCharComparer
-            {
-                Index = -1,
-                Count = 1
-            };
-
-            Equal(c, "a", "a", true);
-            Equal(c, "a", "aa", false);
-            Equal(c, "a", "ba", false);
-            NotEqual(c, "a", "A");
-            NotEqual(c, "a", "b");
-
-            c.Index = -2;
-            c.Count = 1;
-            Equal(c, "aX", "aY", false);
-            Equal(c, "XaX", "YaY", false);
-            Equal(c, "XaX", "YYaY", false);
-            Equal(c, "XXaX", "YaY", false);
-            NotEqual(c, "XXaX", "YYa");
-        }
-
-        [Fact]
-        public static void RightHandCaseInsensitive()
-        {
-            var c = new RightJustifiedCaseInsensitiveSubstringComparer
-            {
-                Index = -1,
-                Count = 1
-            };
-
-            Equal(c, "a", "a", true);
-            Equal(c, "a", "aa", false);
-            Equal(c, "a", "ba", false);
-            Equal(c, "a", "A", true);
-            Equal(c, "a", "AA", false);
-            Equal(c, "a", "BA", false);
-            NotEqual(c, "a", "b");
-
-            c.Index = -2;
-            c.Count = 1;
-            Equal(c, "aX", "aY", false);
-            Equal(c, "XaX", "YaY", false);
-            Equal(c, "XaX", "YYaY", false);
-            Equal(c, "XXaX", "YaY", false);
-            Equal(c, "aX", "AY", false);
-            Equal(c, "XaX", "YAY", false);
-            Equal(c, "XaX", "YYAY", false);
-            Equal(c, "XXaX", "YAY", false);
-            NotEqual(c, "XXaX", "YYa");
-
-            c.Index = -2;
-            c.Count = 2;
-            Equal(c, "aa", "aa", true);
-            Equal(c, "aa", "aaa", false);
-            Equal(c, "aa", "baa", false);
-            Equal(c, "aa", "AA", true);
-            Equal(c, "aa", "AAA", false);
-            Equal(c, "aa", "bAA", false);
-            NotEqual(c, "aa", "bb");
-        }
-
-        [Fact]
-        public static void RightHandCaseInsensitiveAscii()
-        {
-            var c = new RightJustifiedCaseInsensitiveAsciiSubstringComparer
-            {
-                Index = -1,
-                Count = 1
-            };
-
-            Equal(c, "a", "a", true);
-            Equal(c, "a", "aa", false);
-            Equal(c, "a", "ba", false);
-            Equal(c, "a", "A", true);
-            Equal(c, "a", "AA", false);
-            Equal(c, "a", "BA", false);
-            NotEqual(c, "a", "b");
-
-            c.Index = -2;
-            c.Count = 1;
-            Equal(c, "aX", "aY", false);
-            Equal(c, "XaX", "YaY", false);
-            Equal(c, "XaX", "YYaY", false);
-            Equal(c, "XXaX", "YaY", false);
-            Equal(c, "aX", "AY", false);
-            Equal(c, "XaX", "YAY", false);
-            Equal(c, "XaX", "YYAY", false);
-            Equal(c, "XXaX", "YAY", false);
-            NotEqual(c, "XXaX", "YYa");
-
-            c.Index = -2;
-            c.Count = 2;
-            Equal(c, "aa", "aa", true);
-            Equal(c, "aa", "aaa", false);
-            Equal(c, "aa", "baa", false);
-            Equal(c, "aa", "AA", true);
-            Equal(c, "aa", "AAA", false);
-            Equal(c, "aa", "bAA", false);
-            NotEqual(c, "aa", "bb");
-        }
-
-        [Fact]
-        public static void Full()
-        {
-            var c = new FullStringComparer();
-
-            Equal(c, "", "", true);
-            Equal(c, "A", "A", true);
-            Equal(c, "AA", "AA", true);
-
-            NotEqual(c, "A", "AA");
-            NotEqual(c, "AA", "A");
-        }
-
-        [Fact]
-        public static void FullCaseInsensitive()
-        {
-            var c = new FullCaseInsensitiveStringComparer();
-
-            Equal(c, "", "", true);
-            Equal(c, "A", "A", true);
-            Equal(c, "A", "a", true);
-            Equal(c, "a", "A", true);
-            Equal(c, "AA", "aa", true);
-            Equal(c, "aa", "AA", true);
-
-            NotEqual(c, "A", "AA");
-            NotEqual(c, "AA", "A");
-        }
-
-        [Fact]
-        public static void FullCaseInsensitiveAscii()
-        {
-            var c = new FullCaseInsensitiveAsciiStringComparer();
-
-            Equal(c, "", "", true);
-            Equal(c, "A", "A", true);
-            Equal(c, "A", "a", true);
-            Equal(c, "a", "A", true);
-            Equal(c, "AA", "aa", true);
-            Equal(c, "aa", "AA", true);
-
-            NotEqual(c, "A", "AA");
-            NotEqual(c, "AA", "A");
-        }
-    }
-}
diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/KeyAnalyzerTests.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/KeyAnalyzerTests.cs
new file mode 100644 (file)
index 0000000..c329ae4
--- /dev/null
@@ -0,0 +1,212 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Xunit;
+
+namespace System.Collections.Frozen.Tests
+{
+    public static class KeyAnalyzerTests
+    {
+        private static KeyAnalyzer.AnalysisResults RunAnalysis(string[] values, bool ignoreCase)
+        {
+            KeyAnalyzer.Analyze(values, ignoreCase, out KeyAnalyzer.AnalysisResults r);
+
+            foreach (string s in values)
+            {
+                Assert.True(s.Length >= r.MinimumLength);
+                Assert.True(s.Length <= r.MinimumLength + r.MaximumLengthDiff);
+            }
+
+            return r;
+        }
+
+        [Fact]
+        public static void LeftHand()
+        {
+            KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "K0", "K20", "K300" }, false);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.False(r.IgnoreCase);
+            Assert.Equal(1, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "S1" }, false);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.False(r.IgnoreCase);
+            Assert.Equal(0, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "S1", "T1" }, false);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.False(r.IgnoreCase);
+            Assert.Equal(0, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "SA1", "TA1", "SB1" }, false);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.False(r.IgnoreCase);
+            Assert.Equal(0, r.HashIndex);
+            Assert.Equal(2, r.HashCount);
+        }
+
+        [Fact]
+        public static void LeftHandCaseInsensitive()
+        {
+            KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "É1" }, true);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.False(r.AllAscii);
+            Assert.Equal(0, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "É1", "T1" }, true);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.False(r.AllAscii);
+            Assert.Equal(0, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "ÉA1", "TA1", "ÉB1" }, true);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.False(r.AllAscii);
+            Assert.Equal(0, r.HashIndex);
+            Assert.Equal(2, r.HashCount);
+
+            r = RunAnalysis(new[] { "ABCDEÉ1ABCDEF", "ABCDETA1ABCDEF", "ABCDESB1ABCDEF" }, true);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.False(r.AllAscii);
+            Assert.Equal(5, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "ABCDEFÉ1ABCDEF", "ABCDEFTA1ABCDEF", "ABCDEFSB1ABCDEF" }, true);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.False(r.AllAscii);
+            Assert.Equal(6, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "ABCÉDEFÉ1ABCDEF", "ABCÉDEFTA1ABCDEF", "ABCÉDEFSB1ABCDEF" }, true);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.False(r.AllAscii);
+            Assert.Equal(7, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+        }
+
+        [Fact]
+        public static void LeftHandCaseInsensitiveAscii()
+        {
+            KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "S1" }, true);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.True(r.AllAscii);
+            Assert.Equal(0, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "S1", "T1" }, true);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.True(r.AllAscii);
+            Assert.Equal(0, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "SA1", "TA1", "SB1" }, true);
+            Assert.False(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.True(r.AllAscii);
+            Assert.Equal(0, r.HashIndex);
+            Assert.Equal(2, r.HashCount);
+        }
+
+        [Fact]
+        public static void RightHand()
+        {
+            KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "1S", "1T" }, false);
+            Assert.True(r.RightJustifiedSubstring);
+            Assert.False(r.IgnoreCase);
+            Assert.True(r.AllAscii);
+            Assert.Equal(-1, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "1AS", "1AT", "1BS" }, false);
+            Assert.True(r.RightJustifiedSubstring);
+            Assert.False(r.IgnoreCase);
+            Assert.True(r.AllAscii);
+            Assert.Equal(-2, r.HashIndex);
+            Assert.Equal(2, r.HashCount);
+        }
+
+        [Fact]
+        public static void RightHandCaseInsensitive()
+        {
+            KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "1É", "1T" }, true);
+            Assert.True(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.False(r.AllAscii);
+            Assert.Equal(-1, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "1AÉ", "1AT", "1BÉ" }, true);
+            Assert.True(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.False(r.AllAscii);
+            Assert.Equal(-2, r.HashIndex);
+            Assert.Equal(2, r.HashCount);
+        }
+
+        [Fact]
+        public static void RightHandCaseInsensitiveAscii()
+        {
+            KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "1S", "1T" }, true);
+            Assert.True(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.True(r.AllAscii);
+            Assert.Equal(-1, r.HashIndex);
+            Assert.Equal(1, r.HashCount);
+
+            r = RunAnalysis(new[] { "1AS", "1AT", "1BS" }, true);
+            Assert.True(r.RightJustifiedSubstring);
+            Assert.True(r.IgnoreCase);
+            Assert.True(r.AllAscii);
+            Assert.Equal(-2, r.HashIndex);
+            Assert.Equal(2, r.HashCount);
+        }
+
+        [Fact]
+        public static void Full()
+        {
+            KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "ABC", "DBC", "ADC", "ABD", "ABDABD" }, false);
+            Assert.False(r.SubstringHashing);
+            Assert.False(r.IgnoreCase);
+            Assert.True(r.AllAscii);
+        }
+
+        [Fact]
+        public static void FullCaseInsensitive()
+        {
+            KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "æbc", "DBC", "æDC", "æbd", "æbdæbd" }, true);
+            Assert.False(r.SubstringHashing);
+            Assert.True(r.IgnoreCase);
+            Assert.False(r.AllAscii);
+        }
+
+        [Fact]
+        public static void FullCaseInsensitiveAscii()
+        {
+            KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "abc", "DBC", "aDC", "abd", "abdabd" }, true);
+            Assert.False(r.SubstringHashing);
+            Assert.True(r.IgnoreCase);
+            Assert.True(r.AllAscii);
+        }
+
+        [Fact]
+        public static void IsAllAscii()
+        {
+            Assert.True(KeyAnalyzer.IsAllAscii("abc".AsSpan()));
+            Assert.True(KeyAnalyzer.IsAllAscii("abcdefghij".AsSpan()));
+            Assert.False(KeyAnalyzer.IsAllAscii("abcdéfghij".AsSpan()));
+        }
+    }
+}
index 0a72d5d..dc5cd65 100644 (file)
@@ -11,6 +11,7 @@
     <Compile Include="$(CommonTestPath)System\Collections\DictionaryExtensions.cs" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" Link="Common\System\Collections\DictionaryExtensions.cs" />
     <Compile Include="BadHasher.cs" />
     <Compile Include="EverythingEqual.cs" />
+    <Compile Include="Frozen\KeyAnalyzerTests.cs" />
     <Compile Include="ImmutableArrayExtensionsTest.cs" />
     <Compile Include="ImmutableArrayTest.cs" />
     <Compile Include="ImmutableArrayTest.netcoreapp.cs" Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'" />
@@ -41,8 +42,6 @@
     <!-- Frozen Collections Tests -->
     <Compile Include="Frozen\FrozenSetTests.cs" />
     <Compile Include="Frozen\FrozenDictionaryTests.cs" />
-    <Compile Include="Frozen\ComparerPickerTests.cs" />
-    <Compile Include="Frozen\ComparerTests.cs" />
     <!-- Common Collections tests -->
     <Compile Include="$(CommonTestPath)System\Collections\CollectionAsserts.cs" Link="Common\System\Collections\CollectionAsserts.cs" />
     <Compile Include="$(CommonTestPath)System\Collections\DelegateEqualityComparer.cs" Link="Common\System\Collections\DelegateEqualityComparer.cs" />