< Summary

Information
Class: System.Buffers.SearchValues
Assembly: System.Private.CoreLib
File(s): D:\runner\runtime\src\libraries\System.Private.CoreLib\src\System\SearchValues\SearchValues.cs
Line coverage
93%
Covered lines: 96
Uncovered lines: 7
Coverable lines: 103
Total lines: 306
Line coverage: 93.2%
Branch coverage
83%
Covered branches: 105
Total branches: 126
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
Create(...)95.45%2222100%
Create(...)84.52%8484100%
ShouldUseProbabilisticMap(System.Int32,System.Int32)66.66%6657.14%
Create(...)0%440%
TryGetSingleRange(...)100%88100%
ShuffleNativeModified(...)50%2266.66%

File(s)

D:\runner\runtime\src\libraries\System.Private.CoreLib\src\System\SearchValues\SearchValues.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.Diagnostics;
 5using System.Numerics;
 6using System.Runtime.CompilerServices;
 7using System.Runtime.InteropServices;
 8using System.Runtime.Intrinsics;
 9using System.Runtime.Intrinsics.Arm;
 10using System.Runtime.Intrinsics.Wasm;
 11using System.Runtime.Intrinsics.X86;
 12
 13namespace System.Buffers
 14{
 15    /// <summary>
 16    /// Provides a set of initialization methods for instances of the <see cref="SearchValues{T}"/> class.
 17    /// </summary>
 18    /// <remarks>
 19    /// SearchValues are optimized for situations where the same set of values is frequently used for searching at runti
 20    /// </remarks>
 21    public static class SearchValues
 22    {
 23        /// <summary>
 24        /// Creates an optimized representation of <paramref name="values"/> used for efficient searching.
 25        /// </summary>
 26        /// <param name="values">The set of values.</param>
 27        /// <returns>The optimized representation of <paramref name="values"/> used for efficient searching.</returns>
 28        public static SearchValues<byte> Create(params ReadOnlySpan<byte> values)
 29        {
 847930            if (values.IsEmpty)
 31            {
 7732                return new EmptySearchValues<byte>();
 33            }
 34
 840235            if (values.Length == 1)
 36            {
 26837                return new Any1SearchValues<byte, byte>(values);
 38            }
 39
 40            // RangeByteSearchValues is slower than SingleByteSearchValues, but faster than Any2ByteSearchValues
 813441            if (TryGetSingleRange(values, out byte minInclusive, out byte maxInclusive))
 42            {
 70143                return new RangeByteSearchValues(minInclusive, maxInclusive);
 44            }
 45
 46            // Depending on the hardware, UniqueLowNibble can be faster than even range or 2 values.
 47            // It's currently consistently faster than 4/5 values on all tested platforms (Arm, Avx2, Avx512).
 743348            if (values.Length >= 4 && IndexOfAnyAsciiSearcher.CanUseUniqueLowNibbleSearch(values, maxInclusive))
 49            {
 31350                return new AsciiByteSearchValues<TrueConst>(values);
 51            }
 52
 712053            if (values.Length <= 5)
 54            {
 242955                Debug.Assert(values.Length is 2 or 3 or 4 or 5);
 242956                return values.Length switch
 242957                {
 37058                    2 => new Any2SearchValues<byte, byte>(values),
 58159                    3 => new Any3SearchValues<byte, byte>(values),
 82360                    4 => new Any4SearchValues<byte, byte>(values),
 65561                    _ => new Any5SearchValues<byte, byte>(values),
 242962                };
 63            }
 64
 469165            if (IndexOfAnyAsciiSearcher.IsVectorizationSupported && maxInclusive < 128)
 66            {
 186267                return new AsciiByteSearchValues<FalseConst>(values);
 68            }
 69
 282970            return new AnyByteSearchValues(values);
 71        }
 72
 73        /// <summary>
 74        /// Creates an optimized representation of <paramref name="values"/> used for efficient searching.
 75        /// </summary>
 76        /// <param name="values">The set of values.</param>
 77        /// <returns>The optimized representation of <paramref name="values"/> used for efficient searching.</returns>
 78        public static unsafe SearchValues<char> Create(params ReadOnlySpan<char> values)
 79        {
 848080            if (values.IsEmpty)
 81            {
 34582                return new EmptySearchValues<char>();
 83            }
 84
 85            // Vector128<char> isn't valid. Treat the values as shorts instead.
 813586            ReadOnlySpan<short> shortValues = MemoryMarshal.Cast<char, short>(values);
 87
 813588            if (values.Length == 1)
 89            {
 109090                char value = values[0];
 91
 109092                return PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(value)
 109093                    ? new Any1CharPackedSearchValues(value)
 109094                    : new Any1SearchValues<char, short>(shortValues);
 95            }
 96
 97            // RangeCharSearchValues is slower than SingleCharSearchValues, but faster than Any2CharSearchValues
 704598            if (TryGetSingleRange(values, out char minInclusive, out char maxInclusive))
 99            {
 626100                return PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(minInclusive)
 626101                    ? new RangeCharSearchValues<TrueConst>(minInclusive, maxInclusive)
 626102                    : new RangeCharSearchValues<FalseConst>(minInclusive, maxInclusive);
 103            }
 104
 6419105            if (values.Length == 2)
 106            {
 1707107                char value0 = values[0];
 1707108                char value1 = values[1];
 109
 1707110                if (PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(value0) && Packe
 111                {
 112                    // If the two values are the same ASCII letter with both cases, we can use an approach that
 113                    // reduces the number of comparisons by masking off the bit that differs between lower and upper cas
 114                    // While this most commonly applies to ASCII letters, it also works for other values that differ by 
 425115                    return (value0 ^ value1) == 0x20
 425116                        ? new Any1CharPackedIgnoreCaseSearchValues((char)Math.Max(value0, value1))
 425117                        : new Any2CharPackedSearchValues(value0, value1);
 118                }
 119
 1282120                return new Any2SearchValues<char, short>(shortValues);
 121            }
 122
 4712123            if (values.Length == 3)
 124            {
 698125                char value0 = values[0];
 698126                char value1 = values[1];
 698127                char value2 = values[2];
 128
 698129                return PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(value0) && Pa
 698130                    ? new Any3CharPackedSearchValues(value0, value1, value2)
 698131                    : new Any3SearchValues<char, short>(shortValues);
 132            }
 133
 134            // If the values are sets of 2 ASCII letters with both cases, we can use an approach that
 135            // reduces the number of comparisons by masking off the bit that differs between lower and upper case (0x20)
 136            // While this most commonly applies to ASCII letters, it also works for other values that differ by 0x20 (e.
 4014137            if (IndexOfAnyAsciiSearcher.IsVectorizationSupported && PackedSpanHelpers.PackedIndexOfIsSupported &&
 4014138                maxInclusive < 128 && values.Length == 4 && minInclusive > 0)
 139            {
 523140                Span<char> copy = ['\0', '\0', '\0', '\0'];
 523141                values.CopyTo(copy);
 523142                copy.Sort();
 143
 523144                if ((copy[0] ^ copy[2]) == 0x20 &&
 523145                    (copy[1] ^ copy[3]) == 0x20)
 146                {
 147                    // We pick the higher two values (with the 0x20 bit set). "AaBb" => 'a', 'b'
 341148                    return new Any2CharPackedIgnoreCaseSearchValues(copy[2], copy[3]);
 149                }
 150            }
 151
 152            // Depending on the hardware, UniqueLowNibble can be faster than most implementations we currently prefer ab
 153            // It's currently consistently faster than 4/5 values or Ascii on all tested platforms (Arm, Avx2, Avx512).
 3673154            if (IndexOfAnyAsciiSearcher.CanUseUniqueLowNibbleSearch(values, maxInclusive))
 155            {
 6156                return (Ssse3.IsSupported || PackedSimd.IsSupported) && minInclusive == 0
 6157                    ? new AsciiCharSearchValues<IndexOfAnyAsciiSearcher.Ssse3AndWasmHandleZeroInNeedle, TrueConst>(value
 6158                    : new AsciiCharSearchValues<IndexOfAnyAsciiSearcher.Default, TrueConst>(values);
 159            }
 160
 161            // IndexOfAnyAsciiSearcher for chars is slower than Any3CharSearchValues, but faster than Any4SearchValues
 3667162            if (IndexOfAnyAsciiSearcher.IsVectorizationSupported && maxInclusive < 128)
 163            {
 755164                return (Ssse3.IsSupported || PackedSimd.IsSupported) && minInclusive == 0
 755165                    ? new AsciiCharSearchValues<IndexOfAnyAsciiSearcher.Ssse3AndWasmHandleZeroInNeedle, FalseConst>(valu
 755166                    : new AsciiCharSearchValues<IndexOfAnyAsciiSearcher.Default, FalseConst>(values);
 167            }
 168
 2912169            if (values.Length == 4)
 170            {
 130171                return new Any4SearchValues<char, short>(shortValues);
 172            }
 173
 2782174            if (values.Length == 5)
 175            {
 72176                return new Any5SearchValues<char, short>(shortValues);
 177            }
 178
 2710179            if (IndexOfAnyAsciiSearcher.IsVectorizationSupported && minInclusive < 128)
 180            {
 181                // If we have both ASCII and non-ASCII characters, use an implementation that
 182                // does an optimistic ASCII fast-path and then falls back to the ProbabilisticMap.
 183
 1655184                return (Ssse3.IsSupported || PackedSimd.IsSupported) && minInclusive == 0
 1655185                    ? new ProbabilisticWithAsciiCharSearchValues<IndexOfAnyAsciiSearcher.Ssse3AndWasmHandleZeroInNeedle>
 1655186                    : new ProbabilisticWithAsciiCharSearchValues<IndexOfAnyAsciiSearcher.Default>(values, maxInclusive);
 187            }
 188
 1055189            if (ShouldUseProbabilisticMap(values.Length, maxInclusive))
 190            {
 296191                return new ProbabilisticCharSearchValues(values, maxInclusive);
 192            }
 193
 194            // This will also match ASCII values when IndexOfAnyAsciiSearcher is not supported.
 759195            return new BitmapCharSearchValues(values, maxInclusive);
 196
 197            static bool ShouldUseProbabilisticMap(int valuesLength, int maxInclusive)
 198            {
 199                // *Rough estimates*. The current implementation uses 256 bits for the bloom filter.
 200                // If the implementation is vectorized we can get away with a decent false positive rate.
 201                const int MaxValuesForProbabilisticMap = 256;
 202
 1055203                if (valuesLength > MaxValuesForProbabilisticMap)
 204                {
 205                    // If the number of values is too high, we won't see any benefits from the 'probabilistic' part.
 759206                    return false;
 207                }
 208
 296209                if (Sse41.IsSupported || AdvSimd.Arm64.IsSupported)
 210                {
 211                    // If the probabilistic map is vectorized, we prefer it.
 296212                    return true;
 213                }
 214
 215                // The probabilistic map is more memory efficient for spare sets, while the bitmap is more efficient for
 0216                int bitmapFootprintBytesEstimate = 64 + (maxInclusive / 8);
 0217                int probabilisticFootprintBytesEstimate = 128 + (valuesLength * 4);
 218
 219                // The bitmap is a bit faster than the perfect hash checks employed by the probabilistic map.
 220                // Sacrifice some memory usage for faster lookups.
 221                const int AcceptableSizeMultiplier = 2;
 222
 0223                return AcceptableSizeMultiplier * probabilisticFootprintBytesEstimate < bitmapFootprintBytesEstimate;
 224            }
 225        }
 226
 227        /// <summary>
 228        /// Creates an optimized representation of <paramref name="values"/> used for efficient searching.
 229        /// </summary>
 230        /// <param name="values">The set of values.</param>
 231        /// <param name="comparisonType">Specifies whether to use <see cref="StringComparison.Ordinal"/> or <see cref="S
 232        /// <returns>The optimized representation of <paramref name="values"/> used for efficient searching.</returns>
 233        /// <remarks>Only <see cref="StringComparison.Ordinal"/> or <see cref="StringComparison.OrdinalIgnoreCase"/> may
 234        public static SearchValues<string> Create(ReadOnlySpan<string> values, StringComparison comparisonType)
 235        {
 0236            if (comparisonType is not (StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase))
 237            {
 0238                throw new ArgumentException(SR.Argument_SearchValues_UnsupportedStringComparison, nameof(comparisonType)
 239            }
 240
 0241            return StringSearchValues.Create(values, ignoreCase: comparisonType == StringComparison.OrdinalIgnoreCase);
 242        }
 243
 244        private static bool TryGetSingleRange<T>(ReadOnlySpan<T> values, out T minInclusive, out T maxInclusive)
 245            where T : struct, INumber<T>
 246        {
 15179247            minInclusive = values.Min();
 15179248            maxInclusive = values.Max();
 249
 15179250            uint range = uint.CreateChecked(maxInclusive - minInclusive) + 1;
 15179251            if (range > values.Length)
 252            {
 12111253                return false;
 254            }
 255
 3068256            Span<bool> seenValues = range <= 256 ? stackalloc bool[256] : new bool[range];
 3068257            seenValues = seenValues.Slice(0, (int)range);
 3068258            seenValues.Clear();
 259
 1368422260            foreach (T value in values)
 261            {
 681143262                int offset = int.CreateChecked(value - minInclusive);
 681143263                seenValues[offset] = true;
 264            }
 265
 3068266            if (seenValues.Contains(false))
 267            {
 1741268                return false;
 269            }
 270
 1327271            return true;
 272        }
 273
 274        internal interface IRuntimeConst
 275        {
 276            static abstract bool Value { get; }
 277        }
 278
 279        internal readonly struct TrueConst : IRuntimeConst
 280        {
 309480281            public static bool Value => true;
 282        }
 283
 284        internal readonly struct FalseConst : IRuntimeConst
 285        {
 63654286            public static bool Value => false;
 287        }
 288
 289        /// <summary>
 290        /// Same as <see cref="Vector128.ShuffleNative(Vector128{byte}, Vector128{byte})"/>, except that we guarantee th
 291        /// Some logic in <see cref="SearchValues"/> relies on this exact behavior (implicit AND 0xF, and zeroing when t
 292        /// </summary>
 293        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 294        [CompExactlyDependsOn(typeof(Ssse3))]
 295        [CompHasFallback]
 296        internal static Vector128<byte> ShuffleNativeModified(Vector128<byte> vector, Vector128<byte> indices)
 297        {
 15178298            if (Ssse3.IsSupported)
 299            {
 15178300                return Ssse3.Shuffle(vector, indices);
 301            }
 302
 0303            return Vector128.Shuffle(vector, indices);
 304        }
 305    }
 306}