From 3e2e8d3a9b628965d4391a4505dc2766796bd99b Mon Sep 17 00:00:00 2001 From: EgorBo Date: Thu, 9 Apr 2026 10:59:17 +0200 Subject: [PATCH 1/2] optimize for arm64 --- .../Text/Json/Reader/JsonReaderHelper.net8.cs | 62 ++++++++++++++++--- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.net8.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.net8.cs index 08f72de280193c..3157de2ce5d2f0 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.net8.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.net8.cs @@ -2,7 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; namespace System.Text.Json { @@ -12,14 +16,58 @@ internal static partial class JsonReaderHelper /// https://tools.ietf.org/html/rfc8259 private static readonly SearchValues s_controlQuoteBackslash = SearchValues.Create( // Any Control, < 32 (' ') - "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F"u8 + - // Quote - "\""u8 + - // Backslash - "\\"u8); + "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009"u8 + + "\u000A\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013"u8 + + "\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D"u8 + + "\u001E\u001F\"\\"u8); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan span) => - span.IndexOfAny(s_controlQuoteBackslash); + public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan span) + { + // Per requirements: focus on first 32 bytes + if (!AdvSimd.IsSupported || span.Length < 32) + { + return span.IndexOfAny(s_controlQuoteBackslash); + } + + Vector128 ControlBound = Vector128.Create((byte)32); + Vector128 VecQuote = Vector128.Create((byte)'"'); + Vector128 VecBackslash = Vector128.Create((byte)'\\'); + + ref byte ptr = ref MemoryMarshal.GetReference(span); + + // --- BLOCK 1 (Likely Hit) --- + Vector128 v1 = Vector128.LoadUnsafe(ref ptr); + + // Combine all checks into one mask + Vector128 m1 = Vector128.LessThan(v1, ControlBound) | + Vector128.Equals(v1, VecQuote) | + Vector128.Equals(v1, VecBackslash); + + // Narrow to 64-bit scalar: Each 4 bits in 'mask1' represents 1 byte in 'v1' + ulong mask1 = AdvSimd.ShiftRightLogicalNarrowingLower(m1.AsUInt16(), 4).AsUInt64().ToScalar(); + + // Single scalar branch (CBNZ on ARM64) + if (mask1 != 0) + { + // TrailingZeroCount / 4 gives the byte index (0-15) + return BitOperations.TrailingZeroCount(mask1) >> 2; + } + + // --- BLOCK 2 --- + Vector128 v2 = Vector128.LoadUnsafe(ref ptr, 16); + Vector128 m2 = Vector128.LessThan(v2, ControlBound) | + Vector128.Equals(v2, VecQuote) | + Vector128.Equals(v2, VecBackslash); + + ulong mask2 = AdvSimd.ShiftRightLogicalNarrowingLower(m2.AsUInt16(), 4).AsUInt64().ToScalar(); + + if (mask2 != 0) + { + return 16 + (BitOperations.TrailingZeroCount(mask2) >> 2); + } + + return span.Slice(32).IndexOfAny(s_controlQuoteBackslash); + } } } From d148284cc18376e64213ad6c3f5eef39659d9f4e Mon Sep 17 00:00:00 2001 From: EgorBo Date: Thu, 9 Apr 2026 11:29:02 +0200 Subject: [PATCH 2/2] simpler impl --- .../Text/Json/Reader/JsonReaderHelper.net8.cs | 64 +++++++------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.net8.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.net8.cs index 3157de2ce5d2f0..8bfa2da0c0986e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.net8.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.net8.cs @@ -24,50 +24,30 @@ internal static partial class JsonReaderHelper [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan span) { - // Per requirements: focus on first 32 bytes - if (!AdvSimd.IsSupported || span.Length < 32) + // Fast path for " in the first 16 bytes + if (Vector128.IsHardwareAccelerated && span.Length >= 16) { - return span.IndexOfAny(s_controlQuoteBackslash); + ref byte ptr = ref MemoryMarshal.GetReference(span); + Vector128 matches = Vector128.Equals(Vector128.LoadUnsafe(ref ptr), Vector128.Create((byte)'"')); + if (AdvSimd.IsSupported) + { + // TODO: use Vector128.IndexOf for both AdvSimd and Sse2 once + ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(matches.AsUInt16(), 4).AsUInt64().ToScalar(); + if (mask != 0) + { + return BitOperations.TrailingZeroCount(mask) >> 2; + } + } + else + { + uint mask = matches.ExtractMostSignificantBits(); + if (mask != 0) + { + return BitOperations.TrailingZeroCount(mask); + } + } } - - Vector128 ControlBound = Vector128.Create((byte)32); - Vector128 VecQuote = Vector128.Create((byte)'"'); - Vector128 VecBackslash = Vector128.Create((byte)'\\'); - - ref byte ptr = ref MemoryMarshal.GetReference(span); - - // --- BLOCK 1 (Likely Hit) --- - Vector128 v1 = Vector128.LoadUnsafe(ref ptr); - - // Combine all checks into one mask - Vector128 m1 = Vector128.LessThan(v1, ControlBound) | - Vector128.Equals(v1, VecQuote) | - Vector128.Equals(v1, VecBackslash); - - // Narrow to 64-bit scalar: Each 4 bits in 'mask1' represents 1 byte in 'v1' - ulong mask1 = AdvSimd.ShiftRightLogicalNarrowingLower(m1.AsUInt16(), 4).AsUInt64().ToScalar(); - - // Single scalar branch (CBNZ on ARM64) - if (mask1 != 0) - { - // TrailingZeroCount / 4 gives the byte index (0-15) - return BitOperations.TrailingZeroCount(mask1) >> 2; - } - - // --- BLOCK 2 --- - Vector128 v2 = Vector128.LoadUnsafe(ref ptr, 16); - Vector128 m2 = Vector128.LessThan(v2, ControlBound) | - Vector128.Equals(v2, VecQuote) | - Vector128.Equals(v2, VecBackslash); - - ulong mask2 = AdvSimd.ShiftRightLogicalNarrowingLower(m2.AsUInt16(), 4).AsUInt64().ToScalar(); - - if (mask2 != 0) - { - return 16 + (BitOperations.TrailingZeroCount(mask2) >> 2); - } - - return span.Slice(32).IndexOfAny(s_controlQuoteBackslash); + return span.IndexOfAny(s_controlQuoteBackslash); } } }