diff --git a/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj b/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj index 991f67ba550d23..32f97b137e678a 100644 --- a/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj +++ b/src/libraries/System.Collections.Immutable/src/System.Collections.Immutable.csproj @@ -12,7 +12,7 @@ The System.Collections.Immutable library is built-in as part of the shared frame - + @@ -67,6 +67,14 @@ The System.Collections.Immutable library is built-in as part of the shared frame + + + + + + + + diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs index e4fbdcef00b3c6..cfde792bfc71f2 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenDictionary.cs @@ -181,45 +181,67 @@ private static FrozenDictionary CreateFromDictionary { if (analysis.RightJustifiedSubstring) { - if (analysis.IgnoreCase) + if (analysis.IgnoreCaseForHash) { - frozenDictionary = analysis.AllAsciiIfIgnoreCase + Debug.Assert(analysis.IgnoreCase); + frozenDictionary = analysis.AllAsciiIfIgnoreCaseForHash ? new OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveAsciiSubstring(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount) : new OrdinalStringFrozenDictionary_RightJustifiedCaseInsensitiveSubstring(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); } else { - frozenDictionary = analysis.HashCount == 1 - ? new OrdinalStringFrozenDictionary_RightJustifiedSingleChar(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex) - : new OrdinalStringFrozenDictionary_RightJustifiedSubstring(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); + if (analysis.HashCount == 1) + { + frozenDictionary = analysis.IgnoreCase + ? new OrdinalStringFrozenDictionary_RightJustifiedSingleCharCaseInsensitive(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex) + : new OrdinalStringFrozenDictionary_RightJustifiedSingleChar(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex); + } + else + { + frozenDictionary = analysis.IgnoreCase + ? new OrdinalStringFrozenDictionary_RightJustifiedSubstringCaseInsensitive(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount) + : new OrdinalStringFrozenDictionary_RightJustifiedSubstring(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); + } } } else { - if (analysis.IgnoreCase) + if (analysis.IgnoreCaseForHash) { - frozenDictionary = analysis.AllAsciiIfIgnoreCase + Debug.Assert(analysis.IgnoreCase); + frozenDictionary = analysis.AllAsciiIfIgnoreCaseForHash ? new OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveAsciiSubstring(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount) : new OrdinalStringFrozenDictionary_LeftJustifiedCaseInsensitiveSubstring(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); } else { - frozenDictionary = analysis.HashCount == 1 - ? new OrdinalStringFrozenDictionary_LeftJustifiedSingleChar(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex) - : new OrdinalStringFrozenDictionary_LeftJustifiedSubstring(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); + if (analysis.HashCount == 1) + { + frozenDictionary = analysis.IgnoreCase + ? new OrdinalStringFrozenDictionary_LeftJustifiedSingleCharCaseInsensitive(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex) + : new OrdinalStringFrozenDictionary_LeftJustifiedSingleChar(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex); + } + else + { + frozenDictionary = analysis.IgnoreCase + ? new OrdinalStringFrozenDictionary_LeftJustifiedSubstringCaseInsensitive(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount) + : new OrdinalStringFrozenDictionary_LeftJustifiedSubstring(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); + } } } } else { - if (analysis.IgnoreCase) + if (analysis.IgnoreCaseForHash) { - frozenDictionary = analysis.AllAsciiIfIgnoreCase + Debug.Assert(analysis.IgnoreCase); + frozenDictionary = analysis.AllAsciiIfIgnoreCaseForHash ? new OrdinalStringFrozenDictionary_FullCaseInsensitiveAscii(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff) : new OrdinalStringFrozenDictionary_FullCaseInsensitive(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff); } else { + // if (IgnoreCase) => Can only be true if there are no letters, thus case sensitive comparison still works here. frozenDictionary = new OrdinalStringFrozenDictionary_Full(keys, values, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff); } } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs index 8c315f214fe03c..4bafeccd3882a2 100644 --- a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/FrozenSet.cs @@ -129,45 +129,67 @@ private static FrozenSet CreateFromSet(HashSet source) { if (analysis.RightJustifiedSubstring) { - if (analysis.IgnoreCase) + if (analysis.IgnoreCaseForHash) { - frozenSet = analysis.AllAsciiIfIgnoreCase + Debug.Assert(analysis.IgnoreCase); + frozenSet = analysis.AllAsciiIfIgnoreCaseForHash ? new OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveAsciiSubstring(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount) : new OrdinalStringFrozenSet_RightJustifiedCaseInsensitiveSubstring(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); } else { - frozenSet = analysis.HashCount == 1 - ? new OrdinalStringFrozenSet_RightJustifiedSingleChar(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex) - : new OrdinalStringFrozenSet_RightJustifiedSubstring(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); + if (analysis.HashCount == 1) + { + frozenSet = analysis.IgnoreCase + ? new OrdinalStringFrozenSet_RightJustifiedSingleCharCaseInsensitive(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex) + : new OrdinalStringFrozenSet_RightJustifiedSingleChar(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex); ; + } + else + { + frozenSet = analysis.IgnoreCase + ? new OrdinalStringFrozenSet_RightJustifiedSubstringCaseInsensitive(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount) + : new OrdinalStringFrozenSet_RightJustifiedSubstring(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); + } } } else { - if (analysis.IgnoreCase) + if (analysis.IgnoreCaseForHash) { - frozenSet = analysis.AllAsciiIfIgnoreCase + Debug.Assert(analysis.IgnoreCase); + frozenSet = analysis.AllAsciiIfIgnoreCaseForHash ? new OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveAsciiSubstring(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount) : new OrdinalStringFrozenSet_LeftJustifiedCaseInsensitiveSubstring(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); } else { - frozenSet = analysis.HashCount == 1 - ? new OrdinalStringFrozenSet_LeftJustifiedSingleChar(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex) - : new OrdinalStringFrozenSet_LeftJustifiedSubstring(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); + if (analysis.HashCount == 1) + { + frozenSet = analysis.IgnoreCase + ? new OrdinalStringFrozenSet_LeftJustifiedSingleCharCaseInsensitive(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex) + : new OrdinalStringFrozenSet_LeftJustifiedSingleChar(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex); + } + else + { + frozenSet = analysis.IgnoreCase + ? new OrdinalStringFrozenSet_LeftJustifiedSubstringCaseInsensitive(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount) + : new OrdinalStringFrozenSet_LeftJustifiedSubstring(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff, analysis.HashIndex, analysis.HashCount); + } } } } else { - if (analysis.IgnoreCase) + if (analysis.IgnoreCaseForHash) { - frozenSet = analysis.AllAsciiIfIgnoreCase + Debug.Assert(analysis.IgnoreCase); + frozenSet = analysis.AllAsciiIfIgnoreCaseForHash ? new OrdinalStringFrozenSet_FullCaseInsensitiveAscii(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff) : new OrdinalStringFrozenSet_FullCaseInsensitive(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff); } else { + // if (IgnoreCase) => Can only be true if there are no letters, thus case sensitive comparison still works here. frozenSet = new OrdinalStringFrozenSet_Full(entries, stringComparer, analysis.MinimumLength, analysis.MaximumLengthDiff); } } 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 index f6907367d8b9a2..41931cbc55981b 100644 --- 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 @@ -118,18 +118,20 @@ private static AnalysisResults CreateAnalysisResults( ReadOnlySpan uniqueStrings, bool ignoreCase, int minLength, int maxLength, int index, int count, GetSpan getSubstringSpan) { // Start off by assuming all strings are ASCII - bool allAsciiIfIgnoreCase = true; + bool allAsciiIfIgnoreCaseForHash = true; + + bool ignoreCaseForHash = ignoreCase; // If we're case-sensitive, it doesn't matter if the strings are ASCII or not. // But if we're case-insensitive, we can switch to a faster comparer if all the // substrings are ASCII, so we check each. - if (ignoreCase) + if (ignoreCaseForHash) { // Further, if the ASCII substrings don't contain any letters, then we can // actually perform the comparison as case-sensitive even if case-insensitive // was requested, as there's nothing that would compare equally to the substring // other than the substring itself. - bool canSwitchIgnoreCaseToCaseSensitive = true; + bool canSwitchIgnoreCaseHashToCaseSensitive = true; foreach (string s in uniqueStrings) { @@ -139,28 +141,28 @@ private static AnalysisResults CreateAnalysisResults( // If the substring isn't ASCII, bail out to return the results. if (!IsAllAscii(substring)) { - allAsciiIfIgnoreCase = false; - canSwitchIgnoreCaseToCaseSensitive = false; + allAsciiIfIgnoreCaseForHash = false; + canSwitchIgnoreCaseHashToCaseSensitive = false; break; } // All substrings so far are still ASCII only. If this one contains any ASCII // letters, mark that we can't switch to case-sensitive. - if (canSwitchIgnoreCaseToCaseSensitive && ContainsAnyLetters(substring)) + if (canSwitchIgnoreCaseHashToCaseSensitive && ContainsAnyLetters(substring)) { - canSwitchIgnoreCaseToCaseSensitive = false; + canSwitchIgnoreCaseHashToCaseSensitive = false; } } // If we can switch to case-sensitive, do so. - if (canSwitchIgnoreCaseToCaseSensitive) + if (canSwitchIgnoreCaseHashToCaseSensitive) { - ignoreCase = false; + ignoreCaseForHash = false; } } // Return the analysis results. - return new AnalysisResults(ignoreCase, allAsciiIfIgnoreCase, index, count, minLength, maxLength); + return new AnalysisResults(ignoreCase, ignoreCaseForHash, allAsciiIfIgnoreCaseForHash, index, count, minLength, maxLength); } private delegate ReadOnlySpan GetSpan(string s, int index, int count); @@ -243,10 +245,11 @@ internal static bool HasSufficientUniquenessFactor(HashSet set, ReadOnly internal readonly struct AnalysisResults { - public AnalysisResults(bool ignoreCase, bool allAsciiIfIgnoreCase, int hashIndex, int hashCount, int minLength, int maxLength) + public AnalysisResults(bool ignoreCase, bool ignoreCaseForHash, bool allAsciiIfIgnoreCaseForHash, int hashIndex, int hashCount, int minLength, int maxLength) { IgnoreCase = ignoreCase; - AllAsciiIfIgnoreCase = allAsciiIfIgnoreCase; + IgnoreCaseForHash = ignoreCaseForHash; + AllAsciiIfIgnoreCaseForHash = allAsciiIfIgnoreCaseForHash; HashIndex = hashIndex; HashCount = hashCount; MinimumLength = minLength; @@ -254,7 +257,8 @@ public AnalysisResults(bool ignoreCase, bool allAsciiIfIgnoreCase, int hashIndex } public bool IgnoreCase { get; } - public bool AllAsciiIfIgnoreCase { get; } + public bool IgnoreCaseForHash { get; } + public bool AllAsciiIfIgnoreCaseForHash { get; } public int HashIndex { get; } public int HashCount { get; } public int MinimumLength { get; } diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedSingleCharCaseInsensitive.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedSingleCharCaseInsensitive.cs new file mode 100644 index 00000000000000..2bebf7e04c18f0 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedSingleCharCaseInsensitive.cs @@ -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_LeftJustifiedSingleCharCaseInsensitive : OrdinalStringFrozenDictionary + { + internal OrdinalStringFrozenDictionary_LeftJustifiedSingleCharCaseInsensitive( + string[] keys, + TValue[] values, + IEqualityComparer comparer, + int minimumLength, + int maximumLengthDiff, + int hashIndex) + : base(keys, values, 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) => StringComparer.OrdinalIgnoreCase.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_LeftJustifiedSubstringCaseInsensitive.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedSubstringCaseInsensitive.cs new file mode 100644 index 00000000000000..0aec2fbe80473c --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_LeftJustifiedSubstringCaseInsensitive.cs @@ -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_LeftJustifiedSubstringCaseInsensitive : OrdinalStringFrozenDictionary + { + internal OrdinalStringFrozenDictionary_LeftJustifiedSubstringCaseInsensitive( + string[] keys, + TValue[] values, + IEqualityComparer comparer, + int minimumLength, + int maximumLengthDiff, + int hashIndex, + int hashCount) + : base(keys, values, 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.GetHashCodeOrdinal(s.AsSpan(HashIndex, HashCount)); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedSingleCharCaseInsensitive.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedSingleCharCaseInsensitive.cs new file mode 100644 index 00000000000000..b60fb38ae32405 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedSingleCharCaseInsensitive.cs @@ -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_RightJustifiedSingleCharCaseInsensitive : OrdinalStringFrozenDictionary + { + internal OrdinalStringFrozenDictionary_RightJustifiedSingleCharCaseInsensitive( + string[] keys, + TValue[] values, + IEqualityComparer comparer, + int minimumLength, + int maximumLengthDiff, + int hashIndex) + : base(keys, values, 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) => StringComparer.OrdinalIgnoreCase.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_RightJustifiedSubstringCaseInsensitive.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedSubstringCaseInsensitive.cs new file mode 100644 index 00000000000000..117d2f329ccf4b --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenDictionary_RightJustifiedSubstringCaseInsensitive.cs @@ -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_RightJustifiedSubstringCaseInsensitive : OrdinalStringFrozenDictionary + { + internal OrdinalStringFrozenDictionary_RightJustifiedSubstringCaseInsensitive( + string[] keys, + TValue[] values, + IEqualityComparer comparer, + int minimumLength, + int maximumLengthDiff, + int hashIndex, + int hashCount) + : base(keys, values, 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.GetHashCodeOrdinal(s.AsSpan(s.Length + HashIndex, HashCount)); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedSingleCharCaseInsensitive.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedSingleCharCaseInsensitive.cs new file mode 100644 index 00000000000000..09691e80e75ceb --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedSingleCharCaseInsensitive.cs @@ -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_LeftJustifiedSingleCharCaseInsensitive : OrdinalStringFrozenSet + { + internal OrdinalStringFrozenSet_LeftJustifiedSingleCharCaseInsensitive( + string[] entries, + IEqualityComparer 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) => StringComparer.OrdinalIgnoreCase.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_LeftJustifiedSubstringCaseInsensitive.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedSubstringCaseInsensitive.cs new file mode 100644 index 00000000000000..ebcf89c332b9ab --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_LeftJustifiedSubstringCaseInsensitive.cs @@ -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_LeftJustifiedSubstringCaseInsensitive : OrdinalStringFrozenSet + { + internal OrdinalStringFrozenSet_LeftJustifiedSubstringCaseInsensitive( + string[] entries, + IEqualityComparer 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.GetHashCodeOrdinal(s.AsSpan(HashIndex, HashCount)); + } +} diff --git a/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedSingleCharCaseInsensitive.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedSingleCharCaseInsensitive.cs new file mode 100644 index 00000000000000..e6843b135f2401 --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedSingleCharCaseInsensitive.cs @@ -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_RightJustifiedSingleCharCaseInsensitive : OrdinalStringFrozenSet + { + internal OrdinalStringFrozenSet_RightJustifiedSingleCharCaseInsensitive( + string[] entries, + IEqualityComparer 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) => StringComparer.OrdinalIgnoreCase.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_RightJustifiedSubstringCaseInsensitive.cs b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedSubstringCaseInsensitive.cs new file mode 100644 index 00000000000000..0921da6db088ca --- /dev/null +++ b/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/String/OrdinalStringFrozenSet_RightJustifiedSubstringCaseInsensitive.cs @@ -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_RightJustifiedSubstringCaseInsensitive : OrdinalStringFrozenSet + { + internal OrdinalStringFrozenSet_RightJustifiedSubstringCaseInsensitive( + string[] entries, + IEqualityComparer 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.GetHashCodeOrdinal(s.AsSpan(s.Length + HashIndex, HashCount)); + } +} diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenFromKnownValuesTests.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenFromKnownValuesTests.cs index e11de4412941e5..1b68dfbdfb4c7e 100644 --- a/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenFromKnownValuesTests.cs +++ b/src/libraries/System.Collections.Immutable/tests/Frozen/FrozenFromKnownValuesTests.cs @@ -27,6 +27,7 @@ public static IEnumerable StringStringData() => from comparer in new[] { StringComparer.Ordinal, StringComparer.OrdinalIgnoreCase } from keys in new string[][] { + // from https://github.com/dotnet/runtime/blob/a30de6d40f69ef612b514344a5ec83fffd10b957/src/libraries/Common/src/Interop/Unix/System.Native/Interop.MountPoints.FormatInfo.cs#L84-L327 new[] { @@ -133,14 +134,14 @@ public static IEnumerable StringStringData() => }, // exercise left/right justified ordinal comparers - Enumerable.Range(0, 10).Select(i => $"{i}ABCDEFGH").ToArray(), // left justified single char ascii - Enumerable.Range(0, 10).Select(i => $"ABCDEFGH{i}").ToArray(), // right justified single char ascii - Enumerable.Range(0, 100).Select(i => $"{i:D2}ABCDEFGH").ToArray(), // left justified substring ascii - Enumerable.Range(0, 100).Select(i => $"ABCDEFGH{i:D2}").ToArray(), // right justified substring ascii - Enumerable.Range(0, 10).Select(i => $"{i}ABCDEFGH\U0001F600").ToArray(), // left justified single char non-ascii - Enumerable.Range(0, 10).Select(i => $"ABCDEFGH\U0001F600{i}").ToArray(), // right justified single char non-ascii - Enumerable.Range(0, 100).Select(i => $"{i:D2}ABCDEFGH\U0001F600").ToArray(), // left justified substring non-ascii - Enumerable.Range(0, 100).Select(i => $"ABCDEFGH\U0001F600{i:D2}").ToArray(), // right justified substring non-ascii + Enumerable.Range(0, 10).Select(i => $"{i}ABCDefgh").ToArray(), // left justified single char ascii + Enumerable.Range(0, 10).Select(i => $"ABCDefgh{i}").ToArray(), // right justified single char ascii + Enumerable.Range(0, 100).Select(i => $"{i:D2}ABCDefgh").ToArray(), // left justified substring ascii + Enumerable.Range(0, 100).Select(i => $"ABCDefgh{i:D2}").ToArray(), // right justified substring ascii + Enumerable.Range(0, 10).Select(i => $"{i}ABCDefgh\U0001F600").ToArray(), // left justified single char non-ascii + Enumerable.Range(0, 10).Select(i => $"ABCDefgh\U0001F600{i}").ToArray(), // right justified single char non-ascii + Enumerable.Range(0, 100).Select(i => $"{i:D2}ABCDefgh\U0001F600").ToArray(), // left justified substring non-ascii + Enumerable.Range(0, 100).Select(i => $"ABCDefgh\U0001F600{i:D2}").ToArray(), // right justified substring non-ascii Enumerable.Range(0, 20).Select(i => i.ToString("D2")).Select(s => (char)(s[0] + 128) + "" + (char)(s[1] + 128)).ToArray(), // left-justified non-ascii } select new object[] { keys.ToDictionary(i => i, i => i, comparer) }; @@ -191,6 +192,23 @@ private static void FrozenDictionaryWorker(Dictionary pair in source) + { + TKey keyUpper = (TKey)(object)((string)(object)pair.Key).ToUpper(); + bool isValidTest = frozen.Comparer.Equals(pair.Key, keyUpper); + if (isValidTest) + { + Assert.Equal(pair.Value, frozen.GetValueRefOrNullRef(keyUpper)); + Assert.Equal(pair.Value, frozen[keyUpper]); + Assert.True(frozen.TryGetValue(keyUpper, out TValue value)); + Assert.Equal(pair.Value, value); + } + } + } + foreach (KeyValuePair pair in frozen) { Assert.True(source.TryGetValue(pair.Key, out TValue value)); @@ -201,6 +219,7 @@ private static void FrozenDictionaryWorker(Dictionary(Dictionary source) Assert.True(frozen.TryGetValue(pair.Key, out TKey actualKey)); Assert.Equal(pair.Key, actualKey); } + + if (typeof(TKey) == typeof(string) && ReferenceEquals(frozen.Comparer, StringComparer.OrdinalIgnoreCase)) + { + foreach (KeyValuePair pair in source) + { + TKey keyUpper = (TKey)(object)((string)(object)pair.Key).ToUpper(); + bool isValidTest = frozen.Comparer.Equals(pair.Key, keyUpper); + if (isValidTest) + { + Assert.True(frozen.Contains(keyUpper)); + Assert.True(frozen.TryGetValue(keyUpper, out TKey actualKey)); + Assert.Equal(pair.Key, actualKey); + } + } + } + foreach (TKey key in frozen) { Assert.True(source.TryGetValue(key, out _)); @@ -278,6 +313,7 @@ private void FrozenSetWorker(Dictionary source) { Assert.True(frozen.Contains(key)); } + foreach (TKey item in frozen.Items) { Assert.True(source.ContainsKey(item)); diff --git a/src/libraries/System.Collections.Immutable/tests/Frozen/KeyAnalyzerTests.cs b/src/libraries/System.Collections.Immutable/tests/Frozen/KeyAnalyzerTests.cs index da729d7abe8ba1..141e07162d572f 100644 --- a/src/libraries/System.Collections.Immutable/tests/Frozen/KeyAnalyzerTests.cs +++ b/src/libraries/System.Collections.Immutable/tests/Frozen/KeyAnalyzerTests.cs @@ -32,24 +32,28 @@ public static void LeftHand() { KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "K0", "K20", "K300" }, false); Assert.False(r.RightJustifiedSubstring); + Assert.False(r.IgnoreCaseForHash); 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.IgnoreCaseForHash); 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.IgnoreCaseForHash); 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.IgnoreCaseForHash); Assert.False(r.IgnoreCase); Assert.Equal(0, r.HashIndex); Assert.Equal(2, r.HashCount); @@ -60,45 +64,60 @@ public static void LeftHandCaseInsensitive() { KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "É1" }, true); Assert.False(r.RightJustifiedSubstring); + Assert.True(r.IgnoreCaseForHash); Assert.True(r.IgnoreCase); - Assert.False(r.AllAsciiIfIgnoreCase); + Assert.False(r.AllAsciiIfIgnoreCaseForHash); Assert.Equal(0, r.HashIndex); Assert.Equal(1, r.HashCount); r = RunAnalysis(new[] { "É1", "T1" }, true); Assert.False(r.RightJustifiedSubstring); + Assert.True(r.IgnoreCaseForHash); Assert.True(r.IgnoreCase); - Assert.False(r.AllAsciiIfIgnoreCase); + Assert.False(r.AllAsciiIfIgnoreCaseForHash); Assert.Equal(0, r.HashIndex); Assert.Equal(1, r.HashCount); r = RunAnalysis(new[] { "ÉA1", "TA1", "ÉB1" }, true); Assert.False(r.RightJustifiedSubstring); + Assert.True(r.IgnoreCaseForHash); Assert.True(r.IgnoreCase); - Assert.False(r.AllAsciiIfIgnoreCase); + Assert.False(r.AllAsciiIfIgnoreCaseForHash); 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.IgnoreCaseForHash); Assert.True(r.IgnoreCase); - Assert.False(r.AllAsciiIfIgnoreCase); + Assert.False(r.AllAsciiIfIgnoreCaseForHash); 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.IgnoreCaseForHash); Assert.True(r.IgnoreCase); - Assert.False(r.AllAsciiIfIgnoreCase); + Assert.False(r.AllAsciiIfIgnoreCaseForHash); 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.IgnoreCaseForHash); Assert.True(r.IgnoreCase); - Assert.False(r.AllAsciiIfIgnoreCase); + Assert.False(r.AllAsciiIfIgnoreCaseForHash); Assert.Equal(7, r.HashIndex); Assert.Equal(1, r.HashCount); + + r = RunAnalysis(new[] { "1abc", "2abc", "3abc", "4abc", "5abc", "6abc" }, true); + Assert.False(r.RightJustifiedSubstring); + Assert.False(r.IgnoreCaseForHash); + Assert.True(r.IgnoreCase); + Assert.True(r.AllAsciiIfIgnoreCaseForHash); + Assert.Equal(0, r.HashIndex); + Assert.Equal(1, r.HashCount); + } [Fact] @@ -107,21 +126,24 @@ public static void LeftHandCaseInsensitiveAscii() KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "S1" }, true); Assert.False(r.RightJustifiedSubstring); Assert.True(r.IgnoreCase); - Assert.True(r.AllAsciiIfIgnoreCase); + Assert.True(r.IgnoreCaseForHash); + Assert.True(r.AllAsciiIfIgnoreCaseForHash); 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.AllAsciiIfIgnoreCase); + Assert.True(r.IgnoreCaseForHash); + Assert.True(r.AllAsciiIfIgnoreCaseForHash); 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.AllAsciiIfIgnoreCase); + Assert.True(r.IgnoreCaseForHash); + Assert.True(r.AllAsciiIfIgnoreCaseForHash); Assert.Equal(0, r.HashIndex); Assert.Equal(2, r.HashCount); } @@ -132,14 +154,16 @@ public static void RightHand() KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "1T1", "1T" }, false); Assert.True(r.RightJustifiedSubstring); Assert.False(r.IgnoreCase); - Assert.True(r.AllAsciiIfIgnoreCase); + Assert.False(r.IgnoreCaseForHash); + Assert.True(r.AllAsciiIfIgnoreCaseForHash); Assert.Equal(-1, r.HashIndex); Assert.Equal(1, r.HashCount); r = RunAnalysis(new[] { "1ATA", "1ATB", "1BS" }, false); Assert.True(r.RightJustifiedSubstring); Assert.False(r.IgnoreCase); - Assert.True(r.AllAsciiIfIgnoreCase); + Assert.False(r.IgnoreCaseForHash); + Assert.True(r.AllAsciiIfIgnoreCaseForHash); Assert.Equal(-1, r.HashIndex); Assert.Equal(1, r.HashCount); } @@ -150,14 +174,16 @@ public static void RightHandCaseInsensitive() KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "1ÉÉ", "1É" }, true); Assert.True(r.RightJustifiedSubstring); Assert.True(r.IgnoreCase); - Assert.False(r.AllAsciiIfIgnoreCase); + Assert.True(r.IgnoreCaseForHash); + Assert.False(r.AllAsciiIfIgnoreCaseForHash); Assert.Equal(-2, r.HashIndex); Assert.Equal(1, r.HashCount); r = RunAnalysis(new[] { "ÉA", "1AT", "1AÉT" }, true); Assert.True(r.RightJustifiedSubstring); Assert.True(r.IgnoreCase); - Assert.False(r.AllAsciiIfIgnoreCase); + Assert.True(r.IgnoreCaseForHash); + Assert.False(r.AllAsciiIfIgnoreCaseForHash); Assert.Equal(-2, r.HashIndex); Assert.Equal(2, r.HashCount); } @@ -168,14 +194,16 @@ public static void RightHandCaseInsensitiveAscii() KeyAnalyzer.AnalysisResults r = RunAnalysis(new[] { "a1", "A1T" }, true); Assert.True(r.RightJustifiedSubstring); Assert.True(r.IgnoreCase); - Assert.True(r.AllAsciiIfIgnoreCase); + Assert.True(r.IgnoreCaseForHash); + Assert.True(r.AllAsciiIfIgnoreCaseForHash); Assert.Equal(-1, r.HashIndex); Assert.Equal(1, r.HashCount); r = RunAnalysis(new[] { "bÉÉ", "caT", "cAÉT" }, true); Assert.True(r.RightJustifiedSubstring); Assert.True(r.IgnoreCase); - Assert.True(r.AllAsciiIfIgnoreCase); + Assert.True(r.IgnoreCaseForHash); + Assert.True(r.AllAsciiIfIgnoreCaseForHash); Assert.Equal(-3, r.HashIndex); Assert.Equal(1, r.HashCount); } @@ -186,7 +214,8 @@ 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.AllAsciiIfIgnoreCase); + Assert.False(r.IgnoreCaseForHash); + Assert.True(r.AllAsciiIfIgnoreCaseForHash); } [Fact] @@ -195,7 +224,8 @@ 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.AllAsciiIfIgnoreCase); + Assert.True(r.IgnoreCaseForHash); + Assert.False(r.AllAsciiIfIgnoreCaseForHash); } [Fact] @@ -204,7 +234,8 @@ 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.AllAsciiIfIgnoreCase); + Assert.True(r.IgnoreCaseForHash); + Assert.True(r.AllAsciiIfIgnoreCaseForHash); } [Fact]