File: src\runtime\src\libraries\System.Private.CoreLib\src\System\SearchValues\Strings\Helpers\RabinKarp.cs
Web Access
Project: System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using static System.Buffers.StringSearchValuesHelper;

namespace System.Buffers
{
    /// <summary>
    /// An implementation of the Rabin-Karp algorithm we use as a fallback for
    /// short inputs that we can't handle with Teddy.
    /// https://en.wikipedia.org/wiki/Rabin%E2%80%93Karp_algorithm
    /// Has an O(i * m) worst-case, but we will only use it for very short inputs.
    /// </summary>
    internal readonly struct RabinKarp
    {
        // The number of values we'll accept before falling back to Aho-Corasick.
        // This also affects when Teddy may be used.
        public const int MaxValues = 80;

        // This is a tradeoff between memory consumption and the number of false positives
        // we have to rule out during the verification step.
        private const nuint BucketCount = 64;

        // 18 = Vector128<byte>.Count + 2 (MatchStartOffset for N=3)
        // The logic in this class is not safe from overflows, but we avoid any issues by
        // only calling into it for inputs that are too short for Teddy to handle.
        private const int MaxInputLength = 18 - 1;

        // We're using nuint as the rolling hash, so we can spread the hash over more bits on 64bit.
        private static int HashShiftPerElement => IntPtr.Size == 8 ? 2 : 1;

        private readonly string[]?[] _buckets;
        private readonly int _hashLength;
        private readonly nuint _hashUpdateMultiplier;

        public RabinKarp(ReadOnlySpan<string> values)
        {
            Debug.Assert(values.Length <= MaxValues);

            int minimumLength = int.MaxValue;
            foreach (string value in values)
            {
                minimumLength = Math.Min(minimumLength, value.Length);
            }

            Debug.Assert(minimumLength > 1);

            _hashLength = minimumLength;
            _hashUpdateMultiplier = (nuint)1 << ((minimumLength - 1) * HashShiftPerElement);

            if (minimumLength > MaxInputLength)
            {
                // All the values are long. They'll either be handled by Teddy or won't match at all.
                // There's no point in allocating the buckets as they will never be accessed.
                _buckets = null!;
                return;
            }

            string[]?[] buckets = _buckets = new string[BucketCount][];

            foreach (string value in values)
            {
                if (value.Length > MaxInputLength)
                {
                    // This value can never match. There's no point in including it in the buckets.
                    continue;
                }

                nuint hash = 0;
                for (int i = 0; i < minimumLength; i++)
                {
                    hash = (hash << HashShiftPerElement) + value[i];
                }

                nuint bucket = hash % BucketCount;
                string[] newBucket;

                // Start with a bucket containing 1 element and reallocate larger ones if needed.
                // As MaxValues is similar to BucketCount, we will have 1 value per bucket on average.
                if (buckets[bucket] is string[] existingBucket)
                {
                    newBucket = new string[existingBucket.Length + 1];
                    existingBucket.AsSpan().CopyTo(newBucket);
                }
                else
                {
                    newBucket = new string[1];
                }

                newBucket[^1] = value;
                buckets[bucket] = newBucket;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public readonly int IndexOfAny<TCaseSensitivity>(ReadOnlySpan<char> span)
            where TCaseSensitivity : struct, ICaseSensitivity
        {
            return typeof(TCaseSensitivity) == typeof(CaseInsensitiveUnicode)
                ? IndexOfAnyCaseInsensitiveUnicode(span)
                : IndexOfAnyCore<TCaseSensitivity>(span);
        }

        private readonly int IndexOfAnyCore<TCaseSensitivity>(ReadOnlySpan<char> span)
            where TCaseSensitivity : struct, ICaseSensitivity
        {
            Debug.Assert(typeof(TCaseSensitivity) != typeof(CaseInsensitiveUnicode));
            Debug.Assert(span.Length <= MaxInputLength, "Teddy should have handled short inputs.");

            ref char current = ref MemoryMarshal.GetReference(span);

            int hashLength = _hashLength;

            if (span.Length >= hashLength)
            {
                ref char end = ref Unsafe.Add(ref MemoryMarshal.GetReference(span), (uint)(span.Length - hashLength));

                nuint hash = 0;
                for (uint i = 0; i < hashLength; i++)
                {
                    hash = (hash << HashShiftPerElement) + TCaseSensitivity.TransformInput(Unsafe.Add(ref current, i));
                }

                Debug.Assert(_buckets is not null);
                ref string[]? bucketsRef = ref MemoryMarshal.GetArrayDataReference(_buckets);

                while (true)
                {
                    ValidateReadPosition(span, ref current);

                    if (Unsafe.Add(ref bucketsRef, hash % BucketCount) is string[] bucket)
                    {
                        int startOffset = (int)((nuint)Unsafe.ByteOffset(ref MemoryMarshal.GetReference(span), ref current) / sizeof(char));

                        if (StartsWith<TCaseSensitivity>(ref current, span.Length - startOffset, bucket))
                        {
                            return startOffset;
                        }
                    }

                    if (Unsafe.IsAddressGreaterThanOrEqualTo(ref current, ref end))
                    {
                        break;
                    }

                    char previous = TCaseSensitivity.TransformInput(current);
                    char next = TCaseSensitivity.TransformInput(Unsafe.Add(ref current, (uint)hashLength));

                    // Update the hash by removing the previous character and adding the next one.
                    hash = ((hash - (previous * _hashUpdateMultiplier)) << HashShiftPerElement) + next;
                    current = ref Unsafe.Add(ref current, 1);
                }
            }

            return -1;
        }

        private readonly unsafe int IndexOfAnyCaseInsensitiveUnicode(ReadOnlySpan<char> span)
        {
            Debug.Assert(span.Length <= MaxInputLength, "Teddy should have handled long inputs.");

            if (_hashLength > span.Length)
            {
                // Can't possibly match, all the values are longer than our input span.
                return -1;
            }

            Span<char> upperCase = stackalloc char[MaxInputLength].Slice(0, span.Length);

            int charsWritten = Ordinal.ToUpperOrdinal(span, upperCase);
            Debug.Assert(charsWritten == upperCase.Length);

            // CaseSensitive instead of CaseInsensitiveUnicode as we've already done the case conversion.
            return IndexOfAnyCore<CaseSensitive>(upperCase);
        }
    }
}