< Summary

Line coverage
93%
Covered lines: 96
Uncovered lines: 7
Coverable lines: 103
Total lines: 307
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)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/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        {
 892430            if (values.IsEmpty)
 31            {
 7232                return new EmptySearchValues<byte>();
 33            }
 34
 885235            if (values.Length == 1)
 36            {
 29137                return new Any1SearchValues<byte, byte>(values);
 38            }
 39
 40            // RangeByteSearchValues is slower than SingleByteSearchValues, but faster than Any2ByteSearchValues
 856141            if (TryGetSingleRange(values, out byte minInclusive, out byte maxInclusive))
 42            {
 99943                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).
 756248            if (values.Length >= 4 && IndexOfAnyAsciiSearcher.CanUseUniqueLowNibbleSearch(values, maxInclusive))
 49            {
 28950                return new AsciiByteSearchValues<TrueConst>(values);
 51            }
 52
 727353            if (values.Length <= 5)
 54            {
 260055                Debug.Assert(values.Length is 2 or 3 or 4 or 5);
 260056                return values.Length switch
 260057                {
 34858                    2 => new Any2SearchValues<byte, byte>(values),
 51359                    3 => new Any3SearchValues<byte, byte>(values),
 68360                    4 => new Any4SearchValues<byte, byte>(values),
 105661                    _ => new Any5SearchValues<byte, byte>(values),
 260062                };
 63            }
 64
 467365            if (IndexOfAnyAsciiSearcher.IsVectorizationSupported && maxInclusive < 128)
 66            {
 184967                return new AsciiByteSearchValues<FalseConst>(values);
 68            }
 69
 282470            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        {
 892580            if (values.IsEmpty)
 81            {
 36382                return new EmptySearchValues<char>();
 83            }
 84
 85            // Vector128<char> isn't valid. Treat the values as shorts instead.
 856286            ReadOnlySpan<short> shortValues = MemoryMarshal.Cast<char, short>(values);
 87
 856288            if (values.Length == 1)
 89            {
 115390                char value = values[0];
 91
 115392                return PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(value)
 115393                    ? new Any1CharPackedSearchValues(value)
 115394                    : new Any1SearchValues<char, short>(shortValues);
 95            }
 96
 97            // RangeCharSearchValues is slower than SingleCharSearchValues, but faster than Any2CharSearchValues
 740998            if (TryGetSingleRange(values, out char minInclusive, out char maxInclusive))
 99            {
 1033100                return PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(minInclusive)
 1033101                    ? new RangeCharSearchValues<TrueConst>(minInclusive, maxInclusive)
 1033102                    : new RangeCharSearchValues<FalseConst>(minInclusive, maxInclusive);
 103            }
 104
 6376105            if (values.Length == 2)
 106            {
 1906107                char value0 = values[0];
 1906108                char value1 = values[1];
 109
 1906110                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 
 487115                    return (value0 ^ value1) == 0x20
 487116                        ? new Any1CharPackedIgnoreCaseSearchValues((char)Math.Max(value0, value1))
 487117                        : new Any2CharPackedSearchValues(value0, value1);
 118                }
 119
 1419120                return new Any2SearchValues<char, short>(shortValues);
 121            }
 122
 4470123            if (values.Length == 3)
 124            {
 820125                char value0 = values[0];
 820126                char value1 = values[1];
 820127                char value2 = values[2];
 128
 820129                return PackedSpanHelpers.PackedIndexOfIsSupported && PackedSpanHelpers.CanUsePackedIndexOf(value0) && Pa
 820130                    ? new Any3CharPackedSearchValues(value0, value1, value2)
 820131                    : 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.
 3650137            if (IndexOfAnyAsciiSearcher.IsVectorizationSupported && PackedSpanHelpers.PackedIndexOfIsSupported &&
 3650138                maxInclusive < 128 && values.Length == 4 && minInclusive > 0)
 139            {
 435140                Span<char> copy = ['\0', '\0', '\0', '\0'];
 435141                values.CopyTo(copy);
 435142                copy.Sort();
 143
 435144                if ((copy[0] ^ copy[2]) == 0x20 &&
 435145                    (copy[1] ^ copy[3]) == 0x20)
 146                {
 147                    // We pick the higher two values (with the 0x20 bit set). "AaBb" => 'a', 'b'
 360148                    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).
 3290154            if (IndexOfAnyAsciiSearcher.CanUseUniqueLowNibbleSearch(values, maxInclusive))
 155            {
 43156                return (Ssse3.IsSupported || PackedSimd.IsSupported) && minInclusive == 0
 43157                    ? new AsciiCharSearchValues<IndexOfAnyAsciiSearcher.Ssse3AndWasmHandleZeroInNeedle, TrueConst>(value
 43158                    : new AsciiCharSearchValues<IndexOfAnyAsciiSearcher.Default, TrueConst>(values);
 159            }
 160
 161            // IndexOfAnyAsciiSearcher for chars is slower than Any3CharSearchValues, but faster than Any4SearchValues
 3247162            if (IndexOfAnyAsciiSearcher.IsVectorizationSupported && maxInclusive < 128)
 163            {
 662164                return (Ssse3.IsSupported || PackedSimd.IsSupported) && minInclusive == 0
 662165                    ? new AsciiCharSearchValues<IndexOfAnyAsciiSearcher.Ssse3AndWasmHandleZeroInNeedle, FalseConst>(valu
 662166                    : new AsciiCharSearchValues<IndexOfAnyAsciiSearcher.Default, FalseConst>(values);
 167            }
 168
 2585169            if (values.Length == 4)
 170            {
 153171                return new Any4SearchValues<char, short>(shortValues);
 172            }
 173
 2432174            if (values.Length == 5)
 175            {
 87176                return new Any5SearchValues<char, short>(shortValues);
 177            }
 178
 2345179            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
 1395184                return (Ssse3.IsSupported || PackedSimd.IsSupported) && minInclusive == 0
 1395185                    ? new ProbabilisticWithAsciiCharSearchValues<IndexOfAnyAsciiSearcher.Ssse3AndWasmHandleZeroInNeedle>
 1395186                    : new ProbabilisticWithAsciiCharSearchValues<IndexOfAnyAsciiSearcher.Default>(values, maxInclusive);
 187            }
 188
 950189            if (ShouldUseProbabilisticMap(values.Length, maxInclusive))
 190            {
 436191                return new ProbabilisticCharSearchValues(values, maxInclusive);
 192            }
 193
 194            // This will also match ASCII values when IndexOfAnyAsciiSearcher is not supported.
 514195            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
 950203                if (valuesLength > MaxValuesForProbabilisticMap)
 204                {
 205                    // If the number of values is too high, we won't see any benefits from the 'probabilistic' part.
 514206                    return false;
 207                }
 208
 436209                if (Sse41.IsSupported || AdvSimd.Arm64.IsSupported)
 210                {
 211                    // If the probabilistic map is vectorized, we prefer it.
 436212                    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        {
 15970247            minInclusive = values.Min();
 15970248            maxInclusive = values.Max();
 249
 15970250            uint range = uint.CreateChecked(maxInclusive - minInclusive) + 1;
 15970251            if (range > values.Length)
 252            {
 12587253                return false;
 254            }
 255
 3383256            Span<bool> seenValues = range <= 256 ? stackalloc bool[256] : new bool[range];
 3383257            seenValues = seenValues.Slice(0, (int)range);
 3383258            seenValues.Clear();
 259
 1082374260            foreach (T value in values)
 261            {
 537804262                int offset = int.CreateChecked(value - minInclusive);
 537804263                seenValues[offset] = true;
 264            }
 265
 3383266            if (seenValues.Contains(false))
 267            {
 1351268                return false;
 269            }
 270
 2032271            return true;
 272        }
 273
 274        internal interface IRuntimeConst
 275        {
 276            static abstract bool Value { get; }
 277        }
 278
 279        internal readonly struct TrueConst : IRuntimeConst
 280        {
 141934281            public static bool Value => true;
 282        }
 283
 284        internal readonly struct FalseConst : IRuntimeConst
 285        {
 81187286            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        {
 15764298            if (Ssse3.IsSupported)
 299            {
 15764300                return Ssse3.Shuffle(vector, indices);
 301            }
 302
 0303            return Vector128.Shuffle(vector, indices);
 304        }
 305    }
 306}
 307