< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 179
Coverable lines: 179
Total lines: 508
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 168
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/StringSearchValues.cs

#LineLine coverage
 1// Licensed to the .NET Foundation under one or more agreements.
 2// The .NET Foundation licenses this file to you under the MIT license.
 3
 4using System.Collections.Generic;
 5using System.Diagnostics;
 6using System.Diagnostics.CodeAnalysis;
 7using System.Runtime.Intrinsics;
 8using System.Runtime.Intrinsics.Arm;
 9using System.Runtime.Intrinsics.Wasm;
 10using System.Runtime.Intrinsics.X86;
 11using System.Text;
 12using System.Text.Unicode;
 13using static System.Buffers.StringSearchValuesHelper;
 14
 15namespace System.Buffers
 16{
 17    internal static class StringSearchValues
 18    {
 19        private const int TeddyBucketCount = 8;
 20
 021        private static readonly SearchValues<char> s_asciiLetters =
 022            SearchValues.Create("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
 23
 24        public static SearchValues<string> Create(ReadOnlySpan<string> values, bool ignoreCase)
 25        {
 026            if (values.Length == 0)
 27            {
 028                return new EmptySearchValues<string>();
 29            }
 30
 031            if (values.Length == 1)
 32            {
 33                // Avoid additional overheads for single-value inputs.
 034                string value = values[0];
 035                ArgumentNullException.ThrowIfNull(value, nameof(values));
 036                string normalizedValue = NormalizeIfNeeded(value, ignoreCase);
 37
 038                AnalyzeValues(new ReadOnlySpan<string>(ref normalizedValue), ref ignoreCase, out bool ascii, out bool as
 039                return CreateForSingleValue(normalizedValue, uniqueValues: null, ignoreCase, ascii, asciiLettersOnly);
 40            }
 41
 042            var uniqueValues = new HashSet<string>(values.Length, ignoreCase ? StringComparer.OrdinalIgnoreCase : String
 43
 044            foreach (string value in values)
 45            {
 046                ArgumentNullException.ThrowIfNull(value, nameof(values));
 47
 048                uniqueValues.Add(value);
 49            }
 50
 051            if (uniqueValues.Contains(string.Empty))
 52            {
 053                return new SingleStringSearchValuesFallback<SearchValues.FalseConst>(string.Empty, uniqueValues);
 54            }
 55
 056            Span<string> normalizedValues = new string[uniqueValues.Count];
 057            int i = 0;
 058            foreach (string value in uniqueValues)
 59            {
 060                normalizedValues[i++] = NormalizeIfNeeded(value, ignoreCase);
 61            }
 062            Debug.Assert(i == normalizedValues.Length);
 63
 64            // Aho-Corasick's ctor expects values to be sorted by length.
 065            normalizedValues.Sort(static (a, b) => a.Length.CompareTo(b.Length));
 66
 67            // We may not end up choosing Aho-Corasick as the implementation, but it has a nice property of
 68            // finding all the unreachable values during the construction stage, so we build the trie early.
 069            HashSet<string>? unreachableValues = null;
 070            var ahoCorasickBuilder = new AhoCorasickBuilder(normalizedValues, ignoreCase, ref unreachableValues);
 71
 072            if (unreachableValues is not null)
 73            {
 74                // Some values are exact prefixes of other values.
 75                // Exclude those values now to reduce the number of buckets and make verification steps cheaper during s
 076                normalizedValues = RemoveUnreachableValues(normalizedValues, unreachableValues);
 77            }
 78
 079            SearchValues<string> searchValues = CreateFromNormalizedValues(normalizedValues, uniqueValues, ignoreCase, r
 080            ahoCorasickBuilder.Dispose();
 081            return searchValues;
 82
 83            static string NormalizeIfNeeded(string value, bool ignoreCase) =>
 084                ignoreCase ? value.ToUpperOrdinal() : value;
 85
 86            static Span<string> RemoveUnreachableValues(Span<string> values, HashSet<string> unreachableValues)
 87            {
 088                int newCount = 0;
 089                foreach (string value in values)
 90                {
 091                    if (!unreachableValues.Contains(value))
 92                    {
 093                        values[newCount++] = value;
 94                    }
 95                }
 96
 097                Debug.Assert(newCount <= values.Length - unreachableValues.Count);
 098                Debug.Assert(newCount > 0);
 99
 0100                return values.Slice(0, newCount);
 101            }
 102        }
 103
 104        private static SearchValues<string> CreateFromNormalizedValues(
 105            ReadOnlySpan<string> values,
 106            HashSet<string> uniqueValues,
 107            bool ignoreCase,
 108            ref AhoCorasickBuilder ahoCorasickBuilder)
 109        {
 0110            AnalyzeValues(values, ref ignoreCase, out bool allAscii, out bool asciiLettersOnly, out bool nonAsciiAffecte
 111
 0112            if (values.Length == 1)
 113            {
 114                // We may reach this if we've removed unreachable values and ended up with only 1 remaining.
 0115                return CreateForSingleValue(values[0], uniqueValues, ignoreCase, allAscii, asciiLettersOnly);
 116            }
 117
 0118            if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported || PackedSimd.IsSupported) &&
 0119                TryGetTeddyAcceleratedValues(values, uniqueValues, ignoreCase, allAscii, asciiLettersOnly, nonAsciiAffec
 120            {
 0121                return searchValues;
 122            }
 123
 124            // Fall back to Aho-Corasick for all other multi-value sets.
 0125            AhoCorasick ahoCorasick = ahoCorasickBuilder.Build();
 126
 0127            if (!ignoreCase)
 128            {
 0129                return PickAhoCorasickImplementation<CaseSensitive>(ahoCorasick, uniqueValues);
 130            }
 131
 0132            if (nonAsciiAffectedByCaseConversion)
 133            {
 0134                if (ContainsInvalidValues(values))
 135                {
 136                    // Aho-Corasick can't deal with the matching semantics of invalid values.
 137                    // We will use a slow but correct O(n * m) fallback implementation.
 0138                    return new MultiStringIgnoreCaseSearchValuesFallback(uniqueValues);
 139                }
 140
 0141                return PickAhoCorasickImplementation<CaseInsensitiveUnicode>(ahoCorasick, uniqueValues);
 142            }
 143
 0144            if (asciiLettersOnly)
 145            {
 0146                return PickAhoCorasickImplementation<CaseInsensitiveAsciiLetters>(ahoCorasick, uniqueValues);
 147            }
 148
 0149            return PickAhoCorasickImplementation<CaseInsensitiveAscii>(ahoCorasick, uniqueValues);
 150
 151            static SearchValues<string> PickAhoCorasickImplementation<TCaseSensitivity>(AhoCorasick ahoCorasick, HashSet
 152                where TCaseSensitivity : struct, ICaseSensitivity
 153            {
 0154                return ahoCorasick.ShouldUseAsciiFastScan
 0155                    ? new StringSearchValuesAhoCorasick<TCaseSensitivity, AhoCorasick.IndexOfAnyAsciiFastScan>(ahoCorasi
 0156                    : new StringSearchValuesAhoCorasick<TCaseSensitivity, AhoCorasick.NoFastScan>(ahoCorasick, uniqueVal
 157            }
 158        }
 159
 160        private static SearchValues<string>? TryGetTeddyAcceleratedValues(
 161            ReadOnlySpan<string> values,
 162            HashSet<string> uniqueValues,
 163            bool ignoreCase,
 164            bool allAscii,
 165            bool asciiLettersOnly,
 166            bool nonAsciiAffectedByCaseConversion,
 167            int minLength)
 168        {
 0169            if (minLength == 1)
 170            {
 171                // An 'N=1' implementation is possible, but callers should
 172                // consider using SearchValues<char> instead in such cases.
 173                // It can be added if Regex ends up running into this case.
 0174                return null;
 175            }
 176
 0177            if (values.Length > RabinKarp.MaxValues)
 178            {
 179                // The more values we have, the higher the chance of hash/fingerprint collisions.
 180                // To avoid spending too much time in verification steps, fallback to Aho-Corasick which guarantees O(n)
 181                // If it turns out that this limit is commonly exceeded, we can tweak the number of buckets
 182                // in the implementation, or use different variants depending on input.
 0183                return null;
 184            }
 185
 0186            int n = minLength == 2 ? 2 : 3;
 187
 0188            if (Ssse3.IsSupported || PackedSimd.IsSupported)
 189            {
 0190                foreach (string value in values)
 191                {
 0192                    if (value.AsSpan(0, n).Contains('\0'))
 193                    {
 194                        // If we let null chars through here, Teddy would still work correctly, but it
 195                        // would hit more false positives that the verification step would have to rule out.
 196                        // Ssse3.PackUnsignedSaturate and PackedSimd.ConvertNarrowingSaturateUnsigned both
 197                        // treat negative signed-16 values as 0, so we filter out null-containing needles
 198                        // for both to avoid that source of false positives.
 0199                        return null;
 200                    }
 201                }
 202            }
 203
 204            // Even if the values contain non-ASCII chars, we may be able to use Teddy as long as the
 205            // first N characters are ASCII.
 0206            if (!allAscii)
 207            {
 0208                foreach (string value in values)
 209                {
 0210                    if (!Ascii.IsValid(value.AsSpan(0, n)))
 211                    {
 212                        // A vectorized implementation for non-ASCII values is possible.
 213                        // It can be added if it turns out to be a common enough scenario.
 0214                        return null;
 215                    }
 216                }
 217            }
 218
 0219            if (!ignoreCase)
 220            {
 0221                return PickTeddyImplementation<CaseSensitive, CaseSensitive>(values, uniqueValues, n);
 222            }
 223
 0224            if (asciiLettersOnly)
 225            {
 0226                return PickTeddyImplementation<CaseInsensitiveAsciiLetters, CaseInsensitiveAsciiLetters>(values, uniqueV
 227            }
 228
 229            // Even if the whole value isn't ASCII letters only, we can still use a faster approach
 230            // for the vectorized part as long as the first N characters are.
 0231            bool asciiStartLettersOnly = true;
 0232            bool asciiStartUnaffectedByCaseConversion = true;
 233
 0234            foreach (string value in values)
 235            {
 0236                ReadOnlySpan<char> slice = value.AsSpan(0, n);
 0237                asciiStartLettersOnly = asciiStartLettersOnly && !slice.ContainsAnyExcept(s_asciiLetters);
 0238                asciiStartUnaffectedByCaseConversion = asciiStartUnaffectedByCaseConversion && !slice.ContainsAny(s_asci
 239            }
 240
 0241            Debug.Assert(!(asciiStartLettersOnly && asciiStartUnaffectedByCaseConversion));
 242
 243            // If we still have empty buckets we could use and we're ignoring case, we may be able to
 244            // generate all possible permutations of the first N characters and switch to case-sensitive searching.
 245            // E.g. ["ab", "c!"] => ["ab", "Ab" "aB", "AB", "c!", "C!"].
 246            // This won't apply to inputs with many letters (e.g. "abc" => 8 permutations on its own).
 0247            if (!asciiStartUnaffectedByCaseConversion &&
 0248                values.Length < TeddyBucketCount &&
 0249                TryGenerateAllCasePermutationsForPrefixes(values, n, TeddyBucketCount, out string[]? newValues))
 250            {
 0251                asciiStartUnaffectedByCaseConversion = true;
 0252                values = newValues;
 253            }
 254
 0255            if (asciiStartUnaffectedByCaseConversion)
 256            {
 0257                return nonAsciiAffectedByCaseConversion
 0258                    ? PickTeddyImplementation<CaseSensitive, CaseInsensitiveUnicode>(values, uniqueValues, n)
 0259                    : PickTeddyImplementation<CaseSensitive, CaseInsensitiveAscii>(values, uniqueValues, n);
 260            }
 261
 0262            if (nonAsciiAffectedByCaseConversion)
 263            {
 0264                return asciiStartLettersOnly
 0265                    ? PickTeddyImplementation<CaseInsensitiveAsciiLetters, CaseInsensitiveUnicode>(values, uniqueValues,
 0266                    : PickTeddyImplementation<CaseInsensitiveAscii, CaseInsensitiveUnicode>(values, uniqueValues, n);
 267            }
 268
 0269            return asciiStartLettersOnly
 0270                ? PickTeddyImplementation<CaseInsensitiveAsciiLetters, CaseInsensitiveAscii>(values, uniqueValues, n)
 0271                : PickTeddyImplementation<CaseInsensitiveAscii, CaseInsensitiveAscii>(values, uniqueValues, n);
 272        }
 273
 274        private static SearchValues<string> PickTeddyImplementation<TStartCaseSensitivity, TCaseSensitivity>(
 275            ReadOnlySpan<string> values,
 276            HashSet<string> uniqueValues,
 277            int n)
 278            where TStartCaseSensitivity : struct, ICaseSensitivity
 279            where TCaseSensitivity : struct, ICaseSensitivity
 280        {
 0281            Debug.Assert(typeof(TStartCaseSensitivity) != typeof(CaseInsensitiveUnicode));
 0282            Debug.Assert(values.Length > 1);
 0283            Debug.Assert(n is 2 or 3);
 284
 0285            if (values.Length > TeddyBucketCount)
 286            {
 0287                string[][] buckets = TeddyBucketizer.Bucketize(values, TeddyBucketCount, n);
 288
 289                // Potential optimization: We don't have to pick the first N characters for the fingerprint.
 290                // Different offset selection can noticeably improve throughput (e.g. 2x).
 291
 0292                return n == 2
 0293                    ? new AsciiStringSearchValuesTeddyBucketizedN2<TStartCaseSensitivity, TCaseSensitivity>(buckets, val
 0294                    : new AsciiStringSearchValuesTeddyBucketizedN3<TStartCaseSensitivity, TCaseSensitivity>(buckets, val
 295            }
 296            else
 297            {
 0298                return n == 2
 0299                    ? new AsciiStringSearchValuesTeddyNonBucketizedN2<TStartCaseSensitivity, TCaseSensitivity>(values, u
 0300                    : new AsciiStringSearchValuesTeddyNonBucketizedN3<TStartCaseSensitivity, TCaseSensitivity>(values, u
 301            }
 302        }
 303
 304        private static bool TryGenerateAllCasePermutationsForPrefixes(ReadOnlySpan<string> values, int n, int maxValues,
 305        {
 0306            Debug.Assert(n is 2 or 3);
 0307            Debug.Assert(values.Length < maxValues);
 308
 309            // Count how many possible permutations there are.
 0310            int newValuesCount = 0;
 311
 0312            foreach (string value in values)
 313            {
 0314                int permutations = 1;
 315
 0316                foreach (char c in value.AsSpan(0, n))
 317                {
 0318                    Debug.Assert(char.IsAscii(c));
 319
 0320                    if (char.IsAsciiLetter(c))
 321                    {
 0322                        permutations *= 2;
 323                    }
 324                }
 325
 0326                newValuesCount += permutations;
 327            }
 328
 0329            Debug.Assert(newValuesCount > values.Length, "Shouldn't have been called if there were no letters present");
 330
 0331            if (newValuesCount > maxValues)
 332            {
 0333                newValues = null;
 0334                return false;
 335            }
 336
 337            // Generate the permutations.
 0338            newValues = new string[newValuesCount];
 0339            newValuesCount = 0;
 340
 0341            foreach (string value in values)
 342            {
 0343                int start = newValuesCount;
 344
 0345                newValues[newValuesCount++] = value;
 346
 0347                for (int i = 0; i < n; i++)
 348                {
 0349                    char c = value[i];
 350
 0351                    if (char.IsAsciiLetter(c))
 352                    {
 353                        // Copy all the previous permutations of this value but change the casing of the i-th character.
 0354                        foreach (string previous in newValues.AsSpan(start, newValuesCount - start))
 355                        {
 0356                            newValues[newValuesCount++] = $"{previous.AsSpan(0, i)}{(char)(c ^ 0x20)}{previous.AsSpan(i 
 357                        }
 358                    }
 359                }
 360            }
 361
 0362            Debug.Assert(newValuesCount == newValues.Length);
 0363            return true;
 364        }
 365
 366        private static SearchValues<string> CreateForSingleValue(
 367            string value,
 368            HashSet<string>? uniqueValues,
 369            bool ignoreCase,
 370            bool allAscii,
 371            bool asciiLettersOnly)
 372        {
 373            // We make use of optimizations that may overflow on 32bit systems for long values.
 374            int maxLength = IntPtr.Size == 4 ? 1_000_000_000 : int.MaxValue;
 375
 0376            if (Vector128.IsHardwareAccelerated && value.Length > 1 && value.Length <= maxLength)
 377            {
 0378                SearchValues<string>? searchValues = value.Length switch
 0379                {
 0380                    < 4 => TryCreateSingleValuesThreeChars<ValueLengthLessThan4>(value, uniqueValues, ignoreCase, allAsc
 0381                    <= 8 => TryCreateSingleValuesThreeChars<ValueLength4To8>(value, uniqueValues, ignoreCase, allAscii, 
 0382                    <= 16 => TryCreateSingleValuesThreeChars<ValueLength9To16>(value, uniqueValues, ignoreCase, allAscii
 0383                    _ => TryCreateSingleValuesThreeChars<ValueLengthLongOrUnknown>(value, uniqueValues, ignoreCase, allA
 0384                };
 385
 0386                if (searchValues is not null)
 387                {
 0388                    return searchValues;
 389                }
 390            }
 391
 0392            uniqueValues ??= new HashSet<string>(1, ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordin
 393
 0394            return ignoreCase
 0395                ? new SingleStringSearchValuesFallback<SearchValues.TrueConst>(value, uniqueValues)
 0396                : new SingleStringSearchValuesFallback<SearchValues.FalseConst>(value, uniqueValues);
 397        }
 398
 399        private static SearchValues<string>? TryCreateSingleValuesThreeChars<TValueLength>(
 400            string value,
 401            HashSet<string>? uniqueValues,
 402            bool ignoreCase,
 403            bool allAscii,
 404            bool asciiLettersOnly)
 405            where TValueLength : struct, IValueLength
 406        {
 0407            if (!ignoreCase)
 408            {
 0409                return CreateSingleValuesThreeChars<TValueLength, CaseSensitive>(value, uniqueValues);
 410            }
 411
 0412            if (asciiLettersOnly)
 413            {
 0414                return CreateSingleValuesThreeChars<TValueLength, CaseInsensitiveAsciiLetters>(value, uniqueValues);
 415            }
 416
 0417            if (allAscii)
 418            {
 0419                return CreateSingleValuesThreeChars<TValueLength, CaseInsensitiveAscii>(value, uniqueValues);
 420            }
 421
 422            // SingleStringSearchValuesThreeChars doesn't have logic to handle non-ASCII case conversion, so we require 
 423            // Right now we're always selecting the first character as one of the anchors, and we need at least two.
 0424            if (char.IsAscii(value[0]) && value.AsSpan(1).ContainsAnyInRange((char)0, (char)127))
 425            {
 0426                return CreateSingleValuesThreeChars<TValueLength, CaseInsensitiveUnicode>(value, uniqueValues);
 427            }
 428
 0429            return null;
 430        }
 431
 432        private static SearchValues<string> CreateSingleValuesThreeChars<TValueLength, TCaseSensitivity>(
 433            string value,
 434            HashSet<string>? uniqueValues)
 435            where TValueLength : struct, IValueLength
 436            where TCaseSensitivity : struct, ICaseSensitivity
 437        {
 0438            CharacterFrequencyHelper.GetSingleStringMultiCharacterOffsets(value, ignoreCase: typeof(TCaseSensitivity) !=
 439
 0440            if (CanUsePackedImpl(value[0]) && CanUsePackedImpl(value[ch2Offset]) && CanUsePackedImpl(value[ch3Offset]))
 441            {
 0442                return new SingleStringSearchValuesPackedThreeChars<TValueLength, TCaseSensitivity>(uniqueValues, value,
 443            }
 444
 0445            return new SingleStringSearchValuesThreeChars<TValueLength, TCaseSensitivity>(uniqueValues, value, ch2Offset
 446
 447            // Unlike with PackedSpanHelpers (Sse2 only), we are also using this approach on ARM64.
 448            // We use PackUnsignedSaturate on X86 and UnzipEven on ARM, so the set of allowed characters differs slightl
 449            static bool CanUsePackedImpl(char c) =>
 450                PackedSpanHelpers.PackedIndexOfIsSupported ? PackedSpanHelpers.CanUsePackedIndexOf(c) :
 451                (AdvSimd.Arm64.IsSupported && c <= byte.MaxValue);
 452        }
 453
 454        private static void AnalyzeValues(
 455            ReadOnlySpan<string> values,
 456            ref bool ignoreCase,
 457            out bool allAscii,
 458            out bool asciiLettersOnly,
 459            out bool nonAsciiAffectedByCaseConversion,
 460            out int minLength)
 461        {
 0462            allAscii = true;
 0463            asciiLettersOnly = true;
 0464            minLength = int.MaxValue;
 465
 0466            foreach (string value in values)
 467            {
 0468                allAscii = allAscii && Ascii.IsValid(value);
 0469                asciiLettersOnly = asciiLettersOnly && !value.AsSpan().ContainsAnyExcept(s_asciiLetters);
 0470                minLength = Math.Min(minLength, value.Length);
 471            }
 472
 473            // Potential optimization: Not all characters participate in Unicode case conversion.
 474            // If we can determine that none of the non-ASCII characters do, we can make searching faster
 475            // by using the same paths as we do for ASCII-only values.
 0476            nonAsciiAffectedByCaseConversion = ignoreCase && !allAscii;
 477
 478            // If all the characters in values are unaffected by casing, we can avoid the ignoreCase overhead.
 0479            if (ignoreCase && !nonAsciiAffectedByCaseConversion && !asciiLettersOnly)
 480            {
 0481                ignoreCase = false;
 482
 0483                foreach (string value in values)
 484                {
 0485                    if (value.AsSpan().ContainsAny(s_asciiLetters))
 486                    {
 0487                        ignoreCase = true;
 0488                        break;
 489                    }
 490                }
 491            }
 0492        }
 493
 494        private static bool ContainsInvalidValues(ReadOnlySpan<string> values)
 495        {
 0496            foreach (string value in values)
 497            {
 0498                if (!Utf16.IsValid(value))
 499                {
 0500                    return true;
 501                }
 502            }
 503
 0504            return false;
 505        }
 506    }
 507}
 508

Methods/Properties

.cctor()
Create(System.ReadOnlySpan`1<System.String>,System.Boolean)
NormalizeIfNeeded(System.String,System.Boolean)
RemoveUnreachableValues(System.Span`1<System.String>,System.Collections.Generic.HashSet`1<System.String>)
CreateFromNormalizedValues(System.ReadOnlySpan`1<System.String>,System.Collections.Generic.HashSet`1<System.String>,System.Boolean,System.Buffers.AhoCorasickBuilder&)
PickAhoCorasickImplementation(System.Buffers.AhoCorasick,System.Collections.Generic.HashSet`1<System.String>)
TryGetTeddyAcceleratedValues(System.ReadOnlySpan`1<System.String>,System.Collections.Generic.HashSet`1<System.String>,System.Boolean,System.Boolean,System.Boolean,System.Boolean,System.Int32)
PickTeddyImplementation(System.ReadOnlySpan`1<System.String>,System.Collections.Generic.HashSet`1<System.String>,System.Int32)
TryGenerateAllCasePermutationsForPrefixes(System.ReadOnlySpan`1<System.String>,System.Int32,System.Int32,System.String[]&)
CreateForSingleValue(System.String,System.Collections.Generic.HashSet`1<System.String>,System.Boolean,System.Boolean,System.Boolean)
TryCreateSingleValuesThreeChars(System.String,System.Collections.Generic.HashSet`1<System.String>,System.Boolean,System.Boolean,System.Boolean)
CreateSingleValuesThreeChars(System.String,System.Collections.Generic.HashSet`1<System.String>)
AnalyzeValues(System.ReadOnlySpan`1<System.String>,System.Boolean&,System.Boolean&,System.Boolean&,System.Boolean&,System.Int32&)
ContainsInvalidValues(System.ReadOnlySpan`1<System.String>)