From 57a76ccea798be4ad528a28d5c9d54ed0aa61742 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:15:37 +0000 Subject: [PATCH 1/3] Initial plan From e101740bdf69e4bd5acbbc0aef75c625c075e8a9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:07:35 +0000 Subject: [PATCH 2/3] Vectorize SearchValues.Create min/max and ASCII bitmap construction Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../SearchValuesBenchmark/Program.cs | 19 ++++++ .../SearchValuesBenchmark.csproj | 14 ++++ .../SearchValues/IndexOfAnyAsciiSearcher.cs | 56 ++++++++++++--- .../src/System/SearchValues/SearchValues.cs | 68 +++++++++++++++++-- 4 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/Program.cs create mode 100644 src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/SearchValuesBenchmark.csproj diff --git a/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/Program.cs b/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/Program.cs new file mode 100644 index 00000000000000..8969f2c9f01d8c --- /dev/null +++ b/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/Program.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; + +BenchmarkRunner.Run(); + +[MemoryDiagnoser] +public class SearchValuesBenchmark +{ + // The standard Base64 alphabet (64 characters). + private const string Base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + [Benchmark] + public SearchValues Create_Base64Alphabet() => + SearchValues.Create(Base64Chars); +} diff --git a/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/SearchValuesBenchmark.csproj b/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/SearchValuesBenchmark.csproj new file mode 100644 index 00000000000000..36f1e9d66e949e --- /dev/null +++ b/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/SearchValuesBenchmark.csproj @@ -0,0 +1,14 @@ + + + + $(NetCoreAppCurrent) + Exe + enable + true + + + + + + + diff --git a/src/libraries/System.Private.CoreLib/src/System/SearchValues/IndexOfAnyAsciiSearcher.cs b/src/libraries/System.Private.CoreLib/src/System/SearchValues/IndexOfAnyAsciiSearcher.cs index 3e716c7e22e6d2..4bef82d2eff0d4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SearchValues/IndexOfAnyAsciiSearcher.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SearchValues/IndexOfAnyAsciiSearcher.cs @@ -75,24 +75,62 @@ internal static unsafe void ComputeAsciiState(ReadOnlySpan values, out Asc { Debug.Assert(typeof(T) == typeof(byte) || typeof(T) == typeof(char)); - Vector128 bitmapSpace = default; - byte* bitmapLocal = (byte*)&bitmapSpace; BitVector256 lookupLocal = default; - foreach (T tValue in values) + if (Vector128.IsHardwareAccelerated) { - int value = int.CreateChecked(tValue); + // Build a boolean "seen" array first, then convert to the bitmap using SIMD. + // The bitmap encodes: bitmap[lowNibble] has bit highNibble set iff value (highNibble << 4 | lowNibble) is in the set. + // seen[r * 16 + c] == 1 iff value (r << 4 | c) is in the set (r = highNibble, c = lowNibble). + Span seen = stackalloc byte[128]; + seen.Clear(); - if (value > 127) + foreach (T tValue in values) { - continue; + int value = int.CreateChecked(tValue); + + if (value > 127) + { + continue; + } + + lookupLocal.Set(value); + seen[value] = 1; } - lookupLocal.Set(value); - SetBitmapBit(bitmapLocal, value); + // Convert seen[] to the bitmap vectorially. + // For each high-nibble row r (0-7), shift the 16 seen bytes left by r bits and OR into the bitmap. + // seen[] values are 0 or 1, so shifting by r (0-7) fits within each byte without cross-byte contamination. + ref byte seenRef = ref seen[0]; + Vector128 bitmap = Vector128.Zero; + for (int r = 0; r < 8; r++) + { + Vector128 row = Vector128.LoadUnsafe(ref Unsafe.Add(ref seenRef, r * 16)); + bitmap |= (row.AsUInt16() << r).AsByte(); + } + + state = new AsciiState(bitmap, lookupLocal); } + else + { + Vector128 bitmapSpace = default; + byte* bitmapLocal = (byte*)&bitmapSpace; + + foreach (T tValue in values) + { + int value = int.CreateChecked(tValue); - state = new AsciiState(bitmapSpace, lookupLocal); + if (value > 127) + { + continue; + } + + lookupLocal.Set(value); + SetBitmapBit(bitmapLocal, value); + } + + state = new AsciiState(bitmapSpace, lookupLocal); + } } public static bool CanUseUniqueLowNibbleSearch(ReadOnlySpan values, int maxInclusive) diff --git a/src/libraries/System.Private.CoreLib/src/System/SearchValues/SearchValues.cs b/src/libraries/System.Private.CoreLib/src/System/SearchValues/SearchValues.cs index 73de2d970c626d..e9e4f985d03065 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SearchValues/SearchValues.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SearchValues/SearchValues.cs @@ -244,13 +244,19 @@ public static SearchValues Create(ReadOnlySpan values, StringCom private static bool TryGetSingleRange(ReadOnlySpan values, out T minInclusive, out T maxInclusive) where T : struct, INumber, IMinMaxValue { - T min = T.MaxValue; - T max = T.MinValue; + T min; + T max; - foreach (T value in values) + if (typeof(T) == typeof(char)) + { + // char is not a valid Vector128 element type; treat as ushort instead. + GetMinMax(MemoryMarshal.Cast(values), out ushort minUshort, out ushort maxUshort); + min = Unsafe.BitCast(minUshort); + max = Unsafe.BitCast(maxUshort); + } + else { - min = T.Min(min, value); - max = T.Max(max, value); + GetMinMax(values, out min, out max); } minInclusive = min; @@ -280,6 +286,58 @@ private static bool TryGetSingleRange(ReadOnlySpan values, out T minInclus return true; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void GetMinMax(ReadOnlySpan values, out T min, out T max) + where T : struct, INumber, IMinMaxValue + { + Debug.Assert(values.Length >= 1); + + if (Vector128.IsHardwareAccelerated && Vector128.IsSupported && values.Length >= Vector128.Count) + { + ref T current = ref MemoryMarshal.GetReference(values); + ref T lastVectorStart = ref Unsafe.Add(ref current, values.Length - Vector128.Count); + + Vector128 vMin = Vector128.Create(T.MaxValue); + Vector128 vMax = Vector128.Create(T.MinValue); + + do + { + Vector128 v = Vector128.LoadUnsafe(ref current); + vMin = Vector128.Min(vMin, v); + vMax = Vector128.Max(vMax, v); + current = ref Unsafe.Add(ref current, Vector128.Count); + } + while (Unsafe.IsAddressLessThan(ref current, ref lastVectorStart)); + + // Process the last (possibly overlapping) vector. + Vector128 last = Vector128.LoadUnsafe(ref lastVectorStart); + vMin = Vector128.Min(vMin, last); + vMax = Vector128.Max(vMax, last); + + // Horizontal reduction. + min = vMin[0]; + max = vMax[0]; + for (int i = 1; i < Vector128.Count; i++) + { + min = T.Min(vMin[i], min); + max = T.Max(vMax[i], max); + } + return; + } + + // Scalar fallback. + T fallbackMin = T.MaxValue; + T fallbackMax = T.MinValue; + foreach (T value in values) + { + fallbackMin = T.Min(fallbackMin, value); + fallbackMax = T.Max(fallbackMax, value); + } + + min = fallbackMin; + max = fallbackMax; + } + internal interface IRuntimeConst { static abstract bool Value { get; } From 7933079b7e2fcedaed9c57303a50dfcbc1848f1a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 20:22:05 +0000 Subject: [PATCH 3/3] Revert ComputeAsciiState change and delete benchmark project Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../SearchValuesBenchmark/Program.cs | 19 ------- .../SearchValuesBenchmark.csproj | 14 ----- .../SearchValues/IndexOfAnyAsciiSearcher.cs | 56 +++---------------- 3 files changed, 9 insertions(+), 80 deletions(-) delete mode 100644 src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/Program.cs delete mode 100644 src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/SearchValuesBenchmark.csproj diff --git a/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/Program.cs b/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/Program.cs deleted file mode 100644 index 8969f2c9f01d8c..00000000000000 --- a/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/Program.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Buffers; -using BenchmarkDotNet.Attributes; -using BenchmarkDotNet.Running; - -BenchmarkRunner.Run(); - -[MemoryDiagnoser] -public class SearchValuesBenchmark -{ - // The standard Base64 alphabet (64 characters). - private const string Base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - - [Benchmark] - public SearchValues Create_Base64Alphabet() => - SearchValues.Create(Base64Chars); -} diff --git a/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/SearchValuesBenchmark.csproj b/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/SearchValuesBenchmark.csproj deleted file mode 100644 index 36f1e9d66e949e..00000000000000 --- a/src/libraries/System.Memory/tests/PerformanceTests/SearchValuesBenchmark/SearchValuesBenchmark.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - $(NetCoreAppCurrent) - Exe - enable - true - - - - - - - diff --git a/src/libraries/System.Private.CoreLib/src/System/SearchValues/IndexOfAnyAsciiSearcher.cs b/src/libraries/System.Private.CoreLib/src/System/SearchValues/IndexOfAnyAsciiSearcher.cs index 4bef82d2eff0d4..3e716c7e22e6d2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SearchValues/IndexOfAnyAsciiSearcher.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SearchValues/IndexOfAnyAsciiSearcher.cs @@ -75,62 +75,24 @@ internal static unsafe void ComputeAsciiState(ReadOnlySpan values, out Asc { Debug.Assert(typeof(T) == typeof(byte) || typeof(T) == typeof(char)); + Vector128 bitmapSpace = default; + byte* bitmapLocal = (byte*)&bitmapSpace; BitVector256 lookupLocal = default; - if (Vector128.IsHardwareAccelerated) + foreach (T tValue in values) { - // Build a boolean "seen" array first, then convert to the bitmap using SIMD. - // The bitmap encodes: bitmap[lowNibble] has bit highNibble set iff value (highNibble << 4 | lowNibble) is in the set. - // seen[r * 16 + c] == 1 iff value (r << 4 | c) is in the set (r = highNibble, c = lowNibble). - Span seen = stackalloc byte[128]; - seen.Clear(); + int value = int.CreateChecked(tValue); - foreach (T tValue in values) + if (value > 127) { - int value = int.CreateChecked(tValue); - - if (value > 127) - { - continue; - } - - lookupLocal.Set(value); - seen[value] = 1; + continue; } - // Convert seen[] to the bitmap vectorially. - // For each high-nibble row r (0-7), shift the 16 seen bytes left by r bits and OR into the bitmap. - // seen[] values are 0 or 1, so shifting by r (0-7) fits within each byte without cross-byte contamination. - ref byte seenRef = ref seen[0]; - Vector128 bitmap = Vector128.Zero; - for (int r = 0; r < 8; r++) - { - Vector128 row = Vector128.LoadUnsafe(ref Unsafe.Add(ref seenRef, r * 16)); - bitmap |= (row.AsUInt16() << r).AsByte(); - } - - state = new AsciiState(bitmap, lookupLocal); + lookupLocal.Set(value); + SetBitmapBit(bitmapLocal, value); } - else - { - Vector128 bitmapSpace = default; - byte* bitmapLocal = (byte*)&bitmapSpace; - - foreach (T tValue in values) - { - int value = int.CreateChecked(tValue); - if (value > 127) - { - continue; - } - - lookupLocal.Set(value); - SetBitmapBit(bitmapLocal, value); - } - - state = new AsciiState(bitmapSpace, lookupLocal); - } + state = new AsciiState(bitmapSpace, lookupLocal); } public static bool CanUseUniqueLowNibbleSearch(ReadOnlySpan values, int maxInclusive)