| | | 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 | | |
| | | 4 | | using System.Collections; |
| | | 5 | | using System.Collections.Generic; |
| | | 6 | | using System.Diagnostics; |
| | | 7 | | using System.Diagnostics.CodeAnalysis; |
| | | 8 | | using System.Runtime.CompilerServices; |
| | | 9 | | using System.Runtime.InteropServices; |
| | | 10 | | |
| | | 11 | | namespace System.Buffers |
| | | 12 | | { |
| | | 13 | | /// <summary> |
| | | 14 | | /// Stores the state necessary to call vectorized members on <see cref="ProbabilisticMap"/>, |
| | | 15 | | /// as well as (optionally) a precomputed perfect hash table for faster single-character lookups/match confirmations |
| | | 16 | | /// When the hash table isn't available, the structure stores a pointer to the span of values in the set instead. |
| | | 17 | | /// </summary> |
| | | 18 | | internal unsafe struct ProbabilisticMapState |
| | | 19 | | { |
| | | 20 | | private const int MaxModulus = char.MaxValue + 1; |
| | | 21 | | |
| | | 22 | | public ProbabilisticMap Map; |
| | | 23 | | |
| | | 24 | | // Hash entries store each value from the set at the index determined by the remainder modulo the table size. |
| | | 25 | | // As every value has a unique remainder, we can check if a value is contained in the set by checking |
| | | 26 | | // _hashEntries[value % _hashEntries.Length] == value (see FastContains below). |
| | | 27 | | // The multiplier is used for faster modulo operations when determining the hash table index. |
| | | 28 | | // Exactly one of _hashEntries and _slowContainsValuesPtr may be initialized at the same time. |
| | | 29 | | private readonly uint _multiplier; |
| | | 30 | | private readonly char[]? _hashEntries; |
| | | 31 | | private readonly ReadOnlySpan<char>* _slowContainsValuesPtr; |
| | | 32 | | |
| | | 33 | | public ProbabilisticMapState(ReadOnlySpan<char> values, int maxInclusive) |
| | | 34 | | { |
| | 1831 | 35 | | Debug.Assert(!values.IsEmpty); |
| | | 36 | | |
| | 1831 | 37 | | Map = new ProbabilisticMap(values); |
| | | 38 | | |
| | 1831 | 39 | | uint modulus = FindModulus(values, maxInclusive); |
| | 1831 | 40 | | _multiplier = GetFastModMultiplier(modulus); |
| | 1831 | 41 | | _hashEntries = new char[modulus]; |
| | | 42 | | |
| | | 43 | | // Some hash entries will remain unused. |
| | | 44 | | // We can't leave them uninitialized as we would otherwise erroneously match (char)0. |
| | | 45 | | // The exact value doesn't matter, as long as it's in the set of our values. |
| | 1831 | 46 | | _hashEntries.AsSpan().Fill(values[0]); |
| | | 47 | | |
| | 187432 | 48 | | foreach (char c in values) |
| | | 49 | | { |
| | 91885 | 50 | | _hashEntries[FastMod(c, modulus, _multiplier)] = c; |
| | | 51 | | } |
| | 1831 | 52 | | } |
| | | 53 | | |
| | | 54 | | // valuesPtr must remain valid for as long as this ProbabilisticMapState is used. |
| | | 55 | | public ProbabilisticMapState(ReadOnlySpan<char>* valuesPtr) |
| | | 56 | | { |
| | 0 | 57 | | Debug.Assert((IntPtr)valuesPtr != IntPtr.Zero); |
| | | 58 | | |
| | 0 | 59 | | Map = new ProbabilisticMap(*valuesPtr); |
| | 0 | 60 | | _slowContainsValuesPtr = valuesPtr; |
| | 0 | 61 | | } |
| | | 62 | | |
| | | 63 | | public char[] GetValues() |
| | | 64 | | { |
| | 0 | 65 | | Debug.Assert(_hashEntries is not null); |
| | | 66 | | |
| | 0 | 67 | | var unique = new HashSet<char>(_hashEntries); |
| | 0 | 68 | | char[] values = new char[unique.Count]; |
| | 0 | 69 | | unique.CopyTo(values); |
| | 0 | 70 | | return values; |
| | | 71 | | } |
| | | 72 | | |
| | | 73 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 74 | | public bool FastContains(char value) |
| | | 75 | | { |
| | 379260 | 76 | | Debug.Assert(_hashEntries is not null); |
| | 379260 | 77 | | Debug.Assert((IntPtr)_slowContainsValuesPtr == IntPtr.Zero); |
| | | 78 | | |
| | 379260 | 79 | | return FastContains(_hashEntries, _multiplier, value); |
| | | 80 | | } |
| | | 81 | | |
| | | 82 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 83 | | private static bool FastContains(char[] hashEntries, uint multiplier, char value) |
| | | 84 | | { |
| | 514424 | 85 | | ulong offset = FastMod(value, (uint)hashEntries.Length, multiplier); |
| | 514424 | 86 | | Debug.Assert(offset < (ulong)hashEntries.Length); |
| | | 87 | | |
| | 514424 | 88 | | return Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(hashEntries), (nuint)offset) == value; |
| | | 89 | | } |
| | | 90 | | |
| | | 91 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 92 | | private bool SlowProbabilisticContains(char value) |
| | | 93 | | { |
| | 0 | 94 | | Debug.Assert(_hashEntries is null); |
| | 0 | 95 | | Debug.Assert((IntPtr)_slowContainsValuesPtr != IntPtr.Zero); |
| | | 96 | | |
| | 0 | 97 | | return ProbabilisticMap.Contains( |
| | 0 | 98 | | ref Unsafe.As<ProbabilisticMap, uint>(ref Map), |
| | 0 | 99 | | *_slowContainsValuesPtr, |
| | 0 | 100 | | value); |
| | | 101 | | } |
| | | 102 | | |
| | | 103 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 104 | | private bool SlowContains(char value) |
| | | 105 | | { |
| | 0 | 106 | | Debug.Assert(_hashEntries is null); |
| | 0 | 107 | | Debug.Assert((IntPtr)_slowContainsValuesPtr != IntPtr.Zero); |
| | | 108 | | |
| | 0 | 109 | | return ProbabilisticMap.Contains(*_slowContainsValuesPtr, value); |
| | | 110 | | } |
| | | 111 | | |
| | | 112 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 113 | | public bool ConfirmProbabilisticMatch<TUseFastContains>(char value) |
| | | 114 | | where TUseFastContains : struct, SearchValues.IRuntimeConst |
| | | 115 | | { |
| | 104550 | 116 | | if (TUseFastContains.Value) |
| | | 117 | | { |
| | 104550 | 118 | | return FastContains(value); |
| | | 119 | | } |
| | | 120 | | else |
| | | 121 | | { |
| | | 122 | | // We use SlowContains instead of SlowProbabilisticContains here as we've already checked |
| | | 123 | | // the value against the probabilistic filter and are now confirming the potential match. |
| | 0 | 124 | | return SlowContains(value); |
| | | 125 | | } |
| | | 126 | | } |
| | | 127 | | |
| | | 128 | | /// <summary>Finds a modulus where remainders for all values in the set are unique.</summary> |
| | | 129 | | private static uint FindModulus(ReadOnlySpan<char> values, int maxInclusive) |
| | | 130 | | { |
| | 1831 | 131 | | Debug.Assert(maxInclusive <= char.MaxValue); |
| | | 132 | | |
| | 1831 | 133 | | int modulus = HashHelpers.GetPrime(values.Length); |
| | 1831 | 134 | | bool removedDuplicates = false; |
| | | 135 | | |
| | 1831 | 136 | | if (modulus >= maxInclusive) |
| | | 137 | | { |
| | 38 | 138 | | return (uint)(maxInclusive + 1); |
| | | 139 | | } |
| | | 140 | | |
| | 64983 | 141 | | while (true) |
| | | 142 | | { |
| | 68355 | 143 | | if (modulus >= maxInclusive) |
| | | 144 | | { |
| | | 145 | | // Try to remove duplicates and try again. |
| | 1579 | 146 | | if (!removedDuplicates && TryRemoveDuplicates(values, out char[]? deduplicated)) |
| | | 147 | | { |
| | 1579 | 148 | | removedDuplicates = true; |
| | 1579 | 149 | | values = deduplicated; |
| | 1579 | 150 | | modulus = HashHelpers.GetPrime(values.Length); |
| | 1579 | 151 | | continue; |
| | | 152 | | } |
| | | 153 | | |
| | 0 | 154 | | return (uint)(maxInclusive + 1); |
| | | 155 | | } |
| | | 156 | | |
| | 66776 | 157 | | if (TestModulus(values, modulus)) |
| | | 158 | | { |
| | 1793 | 159 | | return (uint)modulus; |
| | | 160 | | } |
| | | 161 | | |
| | 64983 | 162 | | modulus = HashHelpers.GetPrime(modulus + 1); |
| | | 163 | | } |
| | | 164 | | |
| | | 165 | | static bool TestModulus(ReadOnlySpan<char> values, int modulus) |
| | | 166 | | { |
| | 66776 | 167 | | Debug.Assert(modulus < MaxModulus); |
| | | 168 | | |
| | 66776 | 169 | | bool[] seen = ArrayPool<bool>.Shared.Rent(modulus); |
| | 66776 | 170 | | seen.AsSpan(0, modulus).Clear(); |
| | | 171 | | |
| | 66776 | 172 | | uint multiplier = GetFastModMultiplier((uint)modulus); |
| | | 173 | | |
| | 737215 | 174 | | foreach (char c in values) |
| | | 175 | | { |
| | 334323 | 176 | | ulong index = FastMod(c, (uint)modulus, multiplier); |
| | | 177 | | |
| | 334323 | 178 | | if (seen[index]) |
| | | 179 | | { |
| | 64983 | 180 | | ArrayPool<bool>.Shared.Return(seen); |
| | 64983 | 181 | | return false; |
| | | 182 | | } |
| | | 183 | | |
| | 269340 | 184 | | seen[index] = true; |
| | | 185 | | } |
| | | 186 | | |
| | | 187 | | // Saw no duplicates. |
| | 1793 | 188 | | ArrayPool<bool>.Shared.Return(seen); |
| | 1793 | 189 | | return true; |
| | | 190 | | } |
| | | 191 | | |
| | | 192 | | static bool TryRemoveDuplicates(ReadOnlySpan<char> values, [NotNullWhen(true)] out char[]? deduplicated) |
| | | 193 | | { |
| | 1579 | 194 | | HashSet<char> unique = [.. values]; |
| | | 195 | | |
| | 1579 | 196 | | if (unique.Count == values.Length) |
| | | 197 | | { |
| | 0 | 198 | | deduplicated = null; |
| | 0 | 199 | | return false; |
| | | 200 | | } |
| | | 201 | | |
| | 1579 | 202 | | deduplicated = new char[unique.Count]; |
| | 1579 | 203 | | unique.CopyTo(deduplicated); |
| | 1579 | 204 | | return true; |
| | | 205 | | } |
| | | 206 | | } |
| | | 207 | | |
| | | 208 | | // This is a variant of HashHelpers.GetFastModMultiplier, specialized for smaller divisors (<= 65536). |
| | | 209 | | private static uint GetFastModMultiplier(uint divisor) |
| | | 210 | | { |
| | 1009239 | 211 | | Debug.Assert(divisor > 0); |
| | 1009239 | 212 | | Debug.Assert(divisor <= MaxModulus); |
| | | 213 | | |
| | 1009239 | 214 | | return uint.MaxValue / divisor + 1; |
| | | 215 | | } |
| | | 216 | | |
| | | 217 | | // This is a faster variant of HashHelpers.FastMod, specialized for smaller divisors (<= 65536). |
| | | 218 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 219 | | private static ulong FastMod(char value, uint divisor, uint multiplier) |
| | | 220 | | { |
| | 940632 | 221 | | Debug.Assert(multiplier == GetFastModMultiplier(divisor)); |
| | | 222 | | |
| | 940632 | 223 | | ulong result = ((ulong)(multiplier * value) * divisor) >> 32; |
| | | 224 | | |
| | 940632 | 225 | | Debug.Assert(result == (value % divisor)); |
| | 940632 | 226 | | return result; |
| | | 227 | | } |
| | | 228 | | |
| | | 229 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 230 | | public static int IndexOfAnySimpleLoop<TUseFastContains, TNegator>(ref char searchSpace, int searchSpaceLength, |
| | | 231 | | where TUseFastContains : struct, SearchValues.IRuntimeConst |
| | | 232 | | where TNegator : struct, IndexOfAnyAsciiSearcher.INegator |
| | | 233 | | { |
| | 10595 | 234 | | ref char searchSpaceEnd = ref Unsafe.Add(ref searchSpace, searchSpaceLength); |
| | 10595 | 235 | | ref char cur = ref searchSpace; |
| | | 236 | | |
| | 10595 | 237 | | if (TUseFastContains.Value) |
| | | 238 | | { |
| | 10595 | 239 | | Debug.Assert(state._hashEntries is not null); |
| | | 240 | | |
| | 10595 | 241 | | char[] hashEntries = state._hashEntries; |
| | 10595 | 242 | | uint multiplier = state._multiplier; |
| | | 243 | | |
| | 124476 | 244 | | while (!Unsafe.AreSame(ref cur, ref searchSpaceEnd)) |
| | | 245 | | { |
| | 118610 | 246 | | char c = cur; |
| | 118610 | 247 | | if (TNegator.NegateIfNeeded(FastContains(hashEntries, multiplier, c))) |
| | | 248 | | { |
| | 4729 | 249 | | return (int)((nuint)Unsafe.ByteOffset(ref searchSpace, ref cur) / sizeof(char)); |
| | | 250 | | } |
| | | 251 | | |
| | 113881 | 252 | | cur = ref Unsafe.Add(ref cur, 1); |
| | | 253 | | } |
| | | 254 | | } |
| | | 255 | | else |
| | | 256 | | { |
| | 0 | 257 | | while (!Unsafe.AreSame(ref cur, ref searchSpaceEnd)) |
| | | 258 | | { |
| | 0 | 259 | | char c = cur; |
| | 0 | 260 | | if (TNegator.NegateIfNeeded(state.SlowProbabilisticContains(c))) |
| | | 261 | | { |
| | 0 | 262 | | return (int)((nuint)Unsafe.ByteOffset(ref searchSpace, ref cur) / sizeof(char)); |
| | | 263 | | } |
| | | 264 | | |
| | 0 | 265 | | cur = ref Unsafe.Add(ref cur, 1); |
| | | 266 | | } |
| | | 267 | | } |
| | | 268 | | |
| | 5866 | 269 | | return -1; |
| | | 270 | | } |
| | | 271 | | |
| | | 272 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 273 | | public static int LastIndexOfAnySimpleLoop<TUseFastContains, TNegator>(ref char searchSpace, int searchSpaceLeng |
| | | 274 | | where TUseFastContains : struct, SearchValues.IRuntimeConst |
| | | 275 | | where TNegator : struct, IndexOfAnyAsciiSearcher.INegator |
| | | 276 | | { |
| | 5216 | 277 | | if (TUseFastContains.Value) |
| | | 278 | | { |
| | 5216 | 279 | | Debug.Assert(state._hashEntries is not null); |
| | | 280 | | |
| | 5216 | 281 | | char[] hashEntries = state._hashEntries; |
| | 5216 | 282 | | uint multiplier = state._multiplier; |
| | | 283 | | |
| | 19252 | 284 | | while (--searchSpaceLength >= 0) |
| | | 285 | | { |
| | 16554 | 286 | | char c = Unsafe.Add(ref searchSpace, searchSpaceLength); |
| | 16554 | 287 | | if (TNegator.NegateIfNeeded(FastContains(hashEntries, multiplier, c))) |
| | | 288 | | { |
| | | 289 | | break; |
| | | 290 | | } |
| | | 291 | | } |
| | | 292 | | } |
| | | 293 | | else |
| | | 294 | | { |
| | 0 | 295 | | while (--searchSpaceLength >= 0) |
| | | 296 | | { |
| | 0 | 297 | | char c = Unsafe.Add(ref searchSpace, searchSpaceLength); |
| | 0 | 298 | | if (TNegator.NegateIfNeeded(state.SlowProbabilisticContains(c))) |
| | | 299 | | { |
| | | 300 | | break; |
| | | 301 | | } |
| | | 302 | | } |
| | | 303 | | } |
| | | 304 | | |
| | 5216 | 305 | | return searchSpaceLength; |
| | | 306 | | } |
| | | 307 | | } |
| | | 308 | | } |
| | | 309 | | |