< 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        {
 836430            if (values.IsEmpty)
 31            {
 7332                return new EmptySearchValues<byte>();
 33            }
 34
 829135            if (values.Length == 1)
 36            {
 26537                return new Any1SearchValues<byte, byte>(values);
 38            }
 39
 40            // RangeByteSearchValues is slower than SingleByteSearchValues, but faster than Any2ByteSearchValues
 802641            if (TryGetSingleRange(values, out byte minInclusive, out byte maxInclusive))
 42            {
 69343                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).
 733348            if (values.Length >= 4 && IndexOfAnyAsciiSearcher.CanUseUniqueLowNibbleSearch(values, maxInclusive))
 49            {
 31150                return new AsciiByteSearchValues<TrueConst>(values);
 51            }
 52
 702253            if (values.Length <= 5)
 54            {
 241755                Debug.Assert(values.Length is 2 or 3 or 4 or 5);
 241756                return values.Length switch
 241757                {
 36858                    2 => new Any2SearchValues<byte, byte>(values),
 57459                    3 => new Any3SearchValues<byte, byte>(values),
 82160                    4 => new Any4SearchValues<byte, byte>(values),
 65461                    _ => new Any5SearchValues<byte, byte>(values),
 241762                };
 63            }
 64
 460565            if (IndexOfAnyAsciiSearcher.IsVectorizationSupported && maxInclusive < 128)
 66            {
 183267                return new AsciiByteSearchValues<FalseConst>(values);
 68            }
 69
 277370            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        {
 836580            if (values.IsEmpty)
 81            {
 33882                return new EmptySearchValues<char>();
 83            }
 84
 85            // Vector128<char> isn't valid. Treat the values as shorts instead.
 802786            ReadOnlySpan<short> shortValues = MemoryMarshal.Cast<char, short>(values);
 87
 802788            if (values.Length == 1)
 89            {
 108190                char value = values[0];
 91
 108192                return PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(value)
 108193                    ? new Any1CharPackedSearchValues(value)
 108194                    : new Any1SearchValues<char, short>(shortValues);
 95            }
 96
 97            // RangeCharSearchValues is slower than SingleCharSearchValues, but faster than Any2CharSearchValues
 694698            if (TryGetSingleRange(values, out char minInclusive, out char maxInclusive))
 99            {
 619100                return PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(minInclusive)
 619101                    ? new RangeCharSearchValues<TrueConst>(minInclusive, maxInclusive)
 619102                    : new RangeCharSearchValues<FalseConst>(minInclusive, maxInclusive);
 103            }
 104
 6327105            if (values.Length == 2)
 106            {
 1702107                char value0 = values[0];
 1702108                char value1 = values[1];
 109
 1702110                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 
 424115                    return (value0 ^ value1) == 0x20
 424116                        ? new Any1CharPackedIgnoreCaseSearchValues((char)Math.Max(value0, value1))
 424117                        : new Any2CharPackedSearchValues(value0, value1);
 118                }
 119
 1278120                return new Any2SearchValues<char, short>(shortValues);
 121            }
 122
 4625123            if (values.Length == 3)
 124            {
 692125                char value0 = values[0];
 692126                char value1 = values[1];
 692127                char value2 = values[2];
 128
 692129                return PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(value0) && Pa
 692130                    ? new Any3CharPackedSearchValues(value0, value1, value2)
 692131                    : 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.
 3933137            if (IndexOfAnyAsciiSearcher.IsVectorizationSupported && PackedSpanHelpers.PackedIndexOfIsSupported &&
 3933138                maxInclusive < 128 && values.Length == 4 && minInclusive > 0)
 139            {
 502140                Span<char> copy = ['\0', '\0', '\0', '\0'];
 502141                values.CopyTo(copy);
 502142                copy.Sort();
 143
 502144                if ((copy[0] ^ copy[2]) == 0x20 &&
 502145                    (copy[1] ^ copy[3]) == 0x20)
 146                {
 147                    // We pick the higher two values (with the 0x20 bit set). "AaBb" => 'a', 'b'
 333148                    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).
 3600154            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
 3594162            if (IndexOfAnyAsciiSearcher.IsVectorizationSupported && maxInclusive < 128)
 163            {
 739164                return (Ssse3.IsSupported || PackedSimd.IsSupported) && minInclusive == 0
 739165                    ? new AsciiCharSearchValues<IndexOfAnyAsciiSearcher.Ssse3AndWasmHandleZeroInNeedle, FalseConst>(valu
 739166                    : new AsciiCharSearchValues<IndexOfAnyAsciiSearcher.Default, FalseConst>(values);
 167            }
 168
 2855169            if (values.Length == 4)
 170            {
 128171                return new Any4SearchValues<char, short>(shortValues);
 172            }
 173
 2727174            if (values.Length == 5)
 175            {
 65176                return new Any5SearchValues<char, short>(shortValues);
 177            }
 178
 2662179            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
 1621184                return (Ssse3.IsSupported || PackedSimd.IsSupported) && minInclusive == 0
 1621185                    ? new ProbabilisticWithAsciiCharSearchValues<IndexOfAnyAsciiSearcher.Ssse3AndWasmHandleZeroInNeedle>
 1621186                    : new ProbabilisticWithAsciiCharSearchValues<IndexOfAnyAsciiSearcher.Default>(values, maxInclusive);
 187            }
 188
 1041189            if (ShouldUseProbabilisticMap(values.Length, maxInclusive))
 190            {
 289191                return new ProbabilisticCharSearchValues(values, maxInclusive);
 192            }
 193
 194            // This will also match ASCII values when IndexOfAnyAsciiSearcher is not supported.
 752195            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
 1041203                if (valuesLength > MaxValuesForProbabilisticMap)
 204                {
 205                    // If the number of values is too high, we won't see any benefits from the 'probabilistic' part.
 752206                    return false;
 207                }
 208
 289209                if (Sse41.IsSupported || AdvSimd.Arm64.IsSupported)
 210                {
 211                    // If the probabilistic map is vectorized, we prefer it.
 289212                    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        {
 14972247            minInclusive = values.Min();
 14972248            maxInclusive = values.Max();
 249
 14972250            uint range = uint.CreateChecked(maxInclusive - minInclusive) + 1;
 14972251            if (range > values.Length)
 252            {
 11940253                return false;
 254            }
 255
 3032256            Span<bool> seenValues = range <= 256 ? stackalloc bool[256] : new bool[range];
 3032257            seenValues = seenValues.Slice(0, (int)range);
 3032258            seenValues.Clear();
 259
 1348634260            foreach (T value in values)
 261            {
 671285262                int offset = int.CreateChecked(value - minInclusive);
 671285263                seenValues[offset] = true;
 264            }
 265
 3032266            if (seenValues.Contains(false))
 267            {
 1720268                return false;
 269            }
 270
 1312271            return true;
 272        }
 273
 274        internal interface IRuntimeConst
 275        {
 276            static abstract bool Value { get; }
 277        }
 278
 279        internal readonly struct TrueConst : IRuntimeConst
 280        {
 297663281            public static bool Value => true;
 282        }
 283
 284        internal readonly struct FalseConst : IRuntimeConst
 285        {
 62560286            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        {
 14934298            if (Ssse3.IsSupported)
 299            {
 14934300                return Ssse3.Shuffle(vector, indices);
 301            }
 302
 0303            return Vector128.Shuffle(vector, indices);
 304        }
 305    }
 306}