< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 57
Coverable lines: 57
Total lines: 182
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 26
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Cyclomatic complexity NPath complexity Sequence coverage
.ctor(...)0%12120%
IndexOfAny(...)0%220%
IndexOfAnyCore(...)0%10100%
IndexOfAnyCaseInsensitiveUnicode(...)0%220%

File(s)

https://raw.githubusercontent.com/dotnet/runtime/811a7eabb75c42db53440e8ba3f60c07511cfd1f/src/libraries/System.Private.CoreLib/src/System/SearchValues/Strings/Helpers/RabinKarp.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.Globalization;
 6using System.Runtime.CompilerServices;
 7using System.Runtime.InteropServices;
 8using static System.Buffers.StringSearchValuesHelper;
 9
 10namespace System.Buffers
 11{
 12    /// <summary>
 13    /// An implementation of the Rabin-Karp algorithm we use as a fallback for
 14    /// short inputs that we can't handle with Teddy.
 15    /// https://en.wikipedia.org/wiki/Rabin%E2%80%93Karp_algorithm
 16    /// Has an O(i * m) worst-case, but we will only use it for very short inputs.
 17    /// </summary>
 18    internal readonly struct RabinKarp
 19    {
 20        // The number of values we'll accept before falling back to Aho-Corasick.
 21        // This also affects when Teddy may be used.
 22        public const int MaxValues = 80;
 23
 24        // This is a tradeoff between memory consumption and the number of false positives
 25        // we have to rule out during the verification step.
 26        private const nuint BucketCount = 64;
 27
 28        // 18 = Vector128<byte>.Count + 2 (MatchStartOffset for N=3)
 29        // The logic in this class is not safe from overflows, but we avoid any issues by
 30        // only calling into it for inputs that are too short for Teddy to handle.
 31        private const int MaxInputLength = 18 - 1;
 32
 33        // We're using nuint as the rolling hash, so we can spread the hash over more bits on 64bit.
 34        private static int HashShiftPerElement => IntPtr.Size == 8 ? 2 : 1;
 35
 36        private readonly string[]?[] _buckets;
 37        private readonly int _hashLength;
 38        private readonly nuint _hashUpdateMultiplier;
 39
 40        public RabinKarp(ReadOnlySpan<string> values)
 41        {
 042            Debug.Assert(values.Length <= MaxValues);
 43
 044            int minimumLength = int.MaxValue;
 045            foreach (string value in values)
 46            {
 047                minimumLength = Math.Min(minimumLength, value.Length);
 48            }
 49
 050            Debug.Assert(minimumLength > 1);
 51
 052            _hashLength = minimumLength;
 053            _hashUpdateMultiplier = (nuint)1 << ((minimumLength - 1) * HashShiftPerElement);
 54
 055            if (minimumLength > MaxInputLength)
 56            {
 57                // All the values are long. They'll either be handled by Teddy or won't match at all.
 58                // There's no point in allocating the buckets as they will never be accessed.
 059                _buckets = null!;
 060                return;
 61            }
 62
 063            string[]?[] buckets = _buckets = new string[BucketCount][];
 64
 065            foreach (string value in values)
 66            {
 067                if (value.Length > MaxInputLength)
 68                {
 69                    // This value can never match. There's no point in including it in the buckets.
 70                    continue;
 71                }
 72
 073                nuint hash = 0;
 074                for (int i = 0; i < minimumLength; i++)
 75                {
 076                    hash = (hash << HashShiftPerElement) + value[i];
 77                }
 78
 079                nuint bucket = hash % BucketCount;
 80                string[] newBucket;
 81
 82                // Start with a bucket containing 1 element and reallocate larger ones if needed.
 83                // As MaxValues is similar to BucketCount, we will have 1 value per bucket on average.
 084                if (buckets[bucket] is string[] existingBucket)
 85                {
 086                    newBucket = new string[existingBucket.Length + 1];
 087                    existingBucket.AsSpan().CopyTo(newBucket);
 88                }
 89                else
 90                {
 091                    newBucket = new string[1];
 92                }
 93
 094                newBucket[^1] = value;
 095                buckets[bucket] = newBucket;
 96            }
 097        }
 98
 99        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 100        public readonly int IndexOfAny<TCaseSensitivity>(ReadOnlySpan<char> span)
 101            where TCaseSensitivity : struct, ICaseSensitivity
 102        {
 0103            return typeof(TCaseSensitivity) == typeof(CaseInsensitiveUnicode)
 0104                ? IndexOfAnyCaseInsensitiveUnicode(span)
 0105                : IndexOfAnyCore<TCaseSensitivity>(span);
 106        }
 107
 108        private readonly int IndexOfAnyCore<TCaseSensitivity>(ReadOnlySpan<char> span)
 109            where TCaseSensitivity : struct, ICaseSensitivity
 110        {
 0111            Debug.Assert(typeof(TCaseSensitivity) != typeof(CaseInsensitiveUnicode));
 0112            Debug.Assert(span.Length <= MaxInputLength, "Teddy should have handled short inputs.");
 113
 0114            ref char current = ref MemoryMarshal.GetReference(span);
 115
 0116            int hashLength = _hashLength;
 117
 0118            if (span.Length >= hashLength)
 119            {
 0120                ref char end = ref Unsafe.Add(ref MemoryMarshal.GetReference(span), (uint)(span.Length - hashLength));
 121
 0122                nuint hash = 0;
 0123                for (uint i = 0; i < hashLength; i++)
 124                {
 0125                    hash = (hash << HashShiftPerElement) + TCaseSensitivity.TransformInput(Unsafe.Add(ref current, i));
 126                }
 127
 0128                Debug.Assert(_buckets is not null);
 0129                ref string[]? bucketsRef = ref MemoryMarshal.GetArrayDataReference(_buckets);
 130
 0131                while (true)
 132                {
 0133                    ValidateReadPosition(span, ref current);
 134
 0135                    if (Unsafe.Add(ref bucketsRef, hash % BucketCount) is string[] bucket)
 136                    {
 0137                        int startOffset = (int)((nuint)Unsafe.ByteOffset(ref MemoryMarshal.GetReference(span), ref curre
 138
 0139                        if (StartsWith<TCaseSensitivity>(ref current, span.Length - startOffset, bucket))
 140                        {
 0141                            return startOffset;
 142                        }
 143                    }
 144
 0145                    if (Unsafe.IsAddressGreaterThanOrEqualTo(ref current, ref end))
 146                    {
 147                        break;
 148                    }
 149
 0150                    char previous = TCaseSensitivity.TransformInput(current);
 0151                    char next = TCaseSensitivity.TransformInput(Unsafe.Add(ref current, (uint)hashLength));
 152
 153                    // Update the hash by removing the previous character and adding the next one.
 0154                    hash = ((hash - (previous * _hashUpdateMultiplier)) << HashShiftPerElement) + next;
 0155                    current = ref Unsafe.Add(ref current, 1);
 156                }
 157            }
 158
 0159            return -1;
 160        }
 161
 162        private readonly unsafe int IndexOfAnyCaseInsensitiveUnicode(ReadOnlySpan<char> span)
 163        {
 0164            Debug.Assert(span.Length <= MaxInputLength, "Teddy should have handled long inputs.");
 165
 0166            if (_hashLength > span.Length)
 167            {
 168                // Can't possibly match, all the values are longer than our input span.
 0169                return -1;
 170            }
 171
 0172            Span<char> upperCase = stackalloc char[MaxInputLength].Slice(0, span.Length);
 173
 0174            int charsWritten = Ordinal.ToUpperOrdinal(span, upperCase);
 0175            Debug.Assert(charsWritten == upperCase.Length);
 176
 177            // CaseSensitive instead of CaseInsensitiveUnicode as we've already done the case conversion.
 0178            return IndexOfAnyCore<CaseSensitive>(upperCase);
 179        }
 180    }
 181}
 182