From de98b8989a406aaf8794e98b1b5ea7002680cf37 Mon Sep 17 00:00:00 2001 From: EgorBo Date: Thu, 9 Apr 2026 13:40:16 +0200 Subject: [PATCH 01/11] Add a fast path to IndexOfQuoteOrAnyControlOrBackSlash --- .../Text/Json/Reader/JsonReaderHelper.net8.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) 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..c78d23b97e311d 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,10 @@ // 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.Intrinsics; +using System.Runtime.Intrinsics.Arm; namespace System.Text.Json { @@ -19,7 +22,40 @@ internal static partial class JsonReaderHelper "\\"u8); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan span) => + public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan span) + { + // For most inputs, we have a large span and we typically find a match within the first 16 bytes + // Usually, it's a quote in a property name. + if (!Vector128.IsHardwareAccelerated || span.Length < 16) + return IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span); + + Vector128 vec = Vector128.Create(span); + + // Any control character (i.e. 0 to 31) or '"' or '\' + Vector128 cmp = Vector128.LessThan(vec, Vector128.Create((byte)32)) | + Vector128.Equals(vec, Vector128.Create((byte)'"')) | + Vector128.Equals(vec, Vector128.Create((byte)'\\')); + + // TODO: this really should be just Vector128.IndexOfWhereAllBitsSet + // but that is not currently optimized in JIT for ARM64, so we do it manually here. + if (AdvSimd.IsSupported) + { + ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar(); + if (mask != 0) + return BitOperations.TrailingZeroCount(mask) >> 2; + } + else + { + uint mask = cmp.ExtractMostSignificantBits(); + if (mask != 0) + return BitOperations.TrailingZeroCount(mask); + } + + return IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span.Slice(16)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static int IndexOfQuoteOrAnyControlOrBackSlash_Fallback(ReadOnlySpan span) => span.IndexOfAny(s_controlQuoteBackslash); } } From 1796e5e46e24ef53685caa0e2a16c589e8deaea1 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 9 Apr 2026 13:59:54 +0200 Subject: [PATCH 02/11] Update src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.net8.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/System/Text/Json/Reader/JsonReaderHelper.net8.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 c78d23b97e311d..eee98ef390fdc8 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 @@ -51,7 +51,8 @@ public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan sp return BitOperations.TrailingZeroCount(mask); } - return IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span.Slice(16)); + int fallbackIndex = IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span.Slice(16)); + return fallbackIndex >= 0 ? fallbackIndex + 16 : fallbackIndex; } [MethodImpl(MethodImplOptions.NoInlining)] From ccee1e8a02177cbc2de58e1e0a3c42297e9a7f27 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 9 Apr 2026 16:22:32 +0200 Subject: [PATCH 03/11] Optimize bitmask check for ARM64 in JsonReaderHelper --- .../src/System/Text/Json/Reader/JsonReaderHelper.net8.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 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 eee98ef390fdc8..6a96e32c061b1d 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 @@ -40,9 +40,11 @@ public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan sp // but that is not currently optimized in JIT for ARM64, so we do it manually here. if (AdvSimd.IsSupported) { - ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar(); - if (mask != 0) + if (cmp != Vector128.Zero) + { + ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar(); return BitOperations.TrailingZeroCount(mask) >> 2; + } } else { From 024cd8fb7f98cb1ab939b673ff462f31e10bdd05 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 9 Apr 2026 19:11:59 +0200 Subject: [PATCH 04/11] Refactor mask calculation for ARM64 optimization --- .../src/System/Text/Json/Reader/JsonReaderHelper.net8.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 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 6a96e32c061b1d..eee98ef390fdc8 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 @@ -40,11 +40,9 @@ public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan sp // but that is not currently optimized in JIT for ARM64, so we do it manually here. if (AdvSimd.IsSupported) { - if (cmp != Vector128.Zero) - { - ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar(); + ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar(); + if (mask != 0) return BitOperations.TrailingZeroCount(mask) >> 2; - } } else { From 495360f9692135d34e1a43ba095bc5aaca41105d Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 9 Apr 2026 19:46:20 +0200 Subject: [PATCH 05/11] Update src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.net8.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/System/Text/Json/Reader/JsonReaderHelper.net8.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 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 eee98ef390fdc8..79348ffc0e541b 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 @@ -32,9 +32,9 @@ public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan sp Vector128 vec = Vector128.Create(span); // Any control character (i.e. 0 to 31) or '"' or '\' - Vector128 cmp = Vector128.LessThan(vec, Vector128.Create((byte)32)) | - Vector128.Equals(vec, Vector128.Create((byte)'"')) | - Vector128.Equals(vec, Vector128.Create((byte)'\\')); + Vector128 cmp = Vector128.LessThan(vec, Vector128.Create(JsonConstants.Space)) | + Vector128.Equals(vec, Vector128.Create(JsonConstants.Quote)) | + Vector128.Equals(vec, Vector128.Create(JsonConstants.BackSlash)); // TODO: this really should be just Vector128.IndexOfWhereAllBitsSet // but that is not currently optimized in JIT for ARM64, so we do it manually here. From f8637e403f58d1e5140d89b9d1f2ef4b6cb3f35b Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 9 Apr 2026 19:47:31 +0200 Subject: [PATCH 06/11] Update JsonReaderHelper.net8.cs --- .../src/System/Text/Json/Reader/JsonReaderHelper.net8.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 79348ffc0e541b..d984ce1f6d5d99 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 @@ -26,7 +26,7 @@ public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan sp { // For most inputs, we have a large span and we typically find a match within the first 16 bytes // Usually, it's a quote in a property name. - if (!Vector128.IsHardwareAccelerated || span.Length < 16) + if (!Vector128.IsHardwareAccelerated || !BitConverter.IsLittleEndian || span.Length < Vector128.Count) return IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span); Vector128 vec = Vector128.Create(span); From ac7115c8c3193063c2154e32bb31aeb48065155c Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 9 Apr 2026 19:50:43 +0200 Subject: [PATCH 07/11] Update JsonReaderHelper.net8.cs --- .../src/System/Text/Json/Reader/JsonReaderHelper.net8.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 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 d984ce1f6d5d99..5ceb9f84a5c162 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 @@ -51,8 +51,8 @@ public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan sp return BitOperations.TrailingZeroCount(mask); } - int fallbackIndex = IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span.Slice(16)); - return fallbackIndex >= 0 ? fallbackIndex + 16 : fallbackIndex; + int fallbackIndex = IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span.Slice(Vector128.Count)); + return fallbackIndex >= 0 ? fallbackIndex + Vector128.Count : fallbackIndex; } [MethodImpl(MethodImplOptions.NoInlining)] From 39d053775a94b325191319d1ac9a6cf1a4a25f70 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 9 Apr 2026 21:48:21 +0200 Subject: [PATCH 08/11] Update JsonReaderHelper.net8.cs --- .../Text/Json/Reader/JsonReaderHelper.net8.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) 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 5ceb9f84a5c162..1f0dfd68150ef9 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 @@ -29,6 +29,37 @@ public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan sp if (!Vector128.IsHardwareAccelerated || !BitConverter.IsLittleEndian || span.Length < Vector128.Count) return IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span); +#pragma warning disable SYSLIB5003 // SVE is experimental + if (Sve.IsSupported) + { + // SVE: Use predicated comparisons + brkb + cntp to find the index. + // The result flows through predicate registers directly to a scalar cntp — + // no SIMD-to-GP transfer (UMOV) is needed at all. + // On V2: cmpeq(4cy) + cmplo(4cy) + orr(1cy) + brkb(2cy) + cntp(2cy) + ref byte searchSpace = ref MemoryMarshal.GetReference(span); + unsafe + { + fixed (byte* ptr = &searchSpace) + { + Vector mask16 = Sve.CreateTrueMaskByte(SveMaskPattern.VectorCount16); + Vector data = Sve.LoadVector(mask16, ptr); + + Vector combined = Sve.CompareEqual(data, new Vector((byte)'"')) + | Sve.CompareEqual(data, new Vector((byte)'\\')) + | Sve.CompareLessThan(data, new Vector((byte)0x20)); + + if (Sve.TestAnyTrue(mask16, combined)) + { + // brkb: sets predicate bits BEFORE the first match + // cntp: counts those bits = index of first match + Vector beforeFirst = Sve.CreateBreakBeforeMask(mask16, combined); + return (int)Sve.GetActiveElementCount(mask16, beforeFirst); + } + } + } + } +#pragma warning restore SYSLIB5003 // SVE is experimental + Vector128 vec = Vector128.Create(span); // Any control character (i.e. 0 to 31) or '"' or '\' From c6fd2808c001cfd24258d639e98527ee5f8bd56e Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Thu, 9 Apr 2026 22:11:35 +0200 Subject: [PATCH 09/11] Remove experimental SVE implementation from JsonReaderHelper Removed experimental SVE code for finding index of quote or control characters. --- .../Text/Json/Reader/JsonReaderHelper.net8.cs | 31 ------------------- 1 file changed, 31 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 1f0dfd68150ef9..5ceb9f84a5c162 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 @@ -29,37 +29,6 @@ public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan sp if (!Vector128.IsHardwareAccelerated || !BitConverter.IsLittleEndian || span.Length < Vector128.Count) return IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span); -#pragma warning disable SYSLIB5003 // SVE is experimental - if (Sve.IsSupported) - { - // SVE: Use predicated comparisons + brkb + cntp to find the index. - // The result flows through predicate registers directly to a scalar cntp — - // no SIMD-to-GP transfer (UMOV) is needed at all. - // On V2: cmpeq(4cy) + cmplo(4cy) + orr(1cy) + brkb(2cy) + cntp(2cy) - ref byte searchSpace = ref MemoryMarshal.GetReference(span); - unsafe - { - fixed (byte* ptr = &searchSpace) - { - Vector mask16 = Sve.CreateTrueMaskByte(SveMaskPattern.VectorCount16); - Vector data = Sve.LoadVector(mask16, ptr); - - Vector combined = Sve.CompareEqual(data, new Vector((byte)'"')) - | Sve.CompareEqual(data, new Vector((byte)'\\')) - | Sve.CompareLessThan(data, new Vector((byte)0x20)); - - if (Sve.TestAnyTrue(mask16, combined)) - { - // brkb: sets predicate bits BEFORE the first match - // cntp: counts those bits = index of first match - Vector beforeFirst = Sve.CreateBreakBeforeMask(mask16, combined); - return (int)Sve.GetActiveElementCount(mask16, beforeFirst); - } - } - } - } -#pragma warning restore SYSLIB5003 // SVE is experimental - Vector128 vec = Vector128.Create(span); // Any control character (i.e. 0 to 31) or '"' or '\' From 70d3f5a6e8a98cb5d05a062a0a9c474226d646f6 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Mon, 13 Apr 2026 17:41:28 +0200 Subject: [PATCH 10/11] Update JsonReaderHelper.net8.cs --- .../Text/Json/Reader/JsonReaderHelper.net8.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 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 5ceb9f84a5c162..a684c2f2cafcf3 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 @@ -36,19 +36,9 @@ public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan sp Vector128.Equals(vec, Vector128.Create(JsonConstants.Quote)) | Vector128.Equals(vec, Vector128.Create(JsonConstants.BackSlash)); - // TODO: this really should be just Vector128.IndexOfWhereAllBitsSet - // but that is not currently optimized in JIT for ARM64, so we do it manually here. - if (AdvSimd.IsSupported) + if (cmp != Vector128.Zero) { - ulong mask = AdvSimd.ShiftRightLogicalNarrowingLower(cmp.AsUInt16(), 4).AsUInt64().ToScalar(); - if (mask != 0) - return BitOperations.TrailingZeroCount(mask) >> 2; - } - else - { - uint mask = cmp.ExtractMostSignificantBits(); - if (mask != 0) - return BitOperations.TrailingZeroCount(mask); + return Vector128.IndexOfWhereAllBitsSet(cmp); } int fallbackIndex = IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span.Slice(Vector128.Count)); From 5ffb21457cfa4bd96275772c33633e24ceb1ac87 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Mon, 13 Apr 2026 18:17:49 +0200 Subject: [PATCH 11/11] Update JsonReaderHelper.net8.cs --- .../src/System/Text/Json/Reader/JsonReaderHelper.net8.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 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 a684c2f2cafcf3..cd761c9a3f4e7d 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 @@ -36,9 +36,10 @@ public static int IndexOfQuoteOrAnyControlOrBackSlash(this ReadOnlySpan sp Vector128.Equals(vec, Vector128.Create(JsonConstants.Quote)) | Vector128.Equals(vec, Vector128.Create(JsonConstants.BackSlash)); - if (cmp != Vector128.Zero) + int idx = Vector128.IndexOfWhereAllBitsSet(cmp); + if (idx >= 0) { - return Vector128.IndexOfWhereAllBitsSet(cmp); + return idx; } int fallbackIndex = IndexOfQuoteOrAnyControlOrBackSlash_Fallback(span.Slice(Vector128.Count));