From 82f19fe517375b4886f4b35185777bf290fd0549 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:43:12 +0000 Subject: [PATCH 1/3] Initial plan From d76d13049fed7b975711375b767d3b0e50419386 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:46:39 +0000 Subject: [PATCH 2/3] Vectorize SkipWhiteSpace and ConsumeIntegerDigits in Utf8JsonReader Use SearchValues + IndexOfAnyExcept for whitespace skipping and IndexOfAnyExceptInRange for digit scanning, with scalar fallbacks for non-.NETCoreApp targets. Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --- .../Reader/Utf8JsonReader.MultiSegment.cs | 28 +++++++++++ .../System/Text/Json/Reader/Utf8JsonReader.cs | 50 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs index f31686429527d5..bbc686e7e887be 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs @@ -1356,6 +1356,20 @@ private ConsumeNumberResult ConsumeIntegerDigitsMultiSegment(ref ReadOnlySpan= data.Length) { if (IsLastSpan) @@ -1394,6 +1409,18 @@ private ConsumeNumberResult ConsumeIntegerDigitsMultiSegment(ref ReadOnlySpan= data.Length) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs index db764ccc153875..c0fc09e8ad6617 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs @@ -48,6 +48,10 @@ public ref partial struct Utf8JsonReader private readonly bool IsLastSpan => _isFinalBlock && (!_isMultiSegment || _isLastSegment); +#if NET + private static readonly SearchValues s_whitespaceLookup = SearchValues.Create(" \t\n\r"u8); +#endif + internal readonly ReadOnlySequence OriginalSequence => _sequence; internal readonly ReadOnlySpan OriginalSpan => _sequence.IsEmpty ? _buffer : default; @@ -1008,6 +1012,36 @@ private void SkipWhiteSpace() { // Create local copy to avoid bounds checks. ReadOnlySpan localBuffer = _buffer; + +#if NET + // Use vectorized search to find the first non-whitespace byte. + // JSON RFC 8259 section 2 says only these 4 characters count as whitespace. + ReadOnlySpan remaining = localBuffer.Slice(_consumed); + int idx = remaining.IndexOfAnyExcept(s_whitespaceLookup); + if (idx < 0) + { + idx = remaining.Length; + } + + if (idx > 0) + { + ReadOnlySpan whitespace = remaining.Slice(0, idx); + int newLineCount = whitespace.Count(JsonConstants.LineFeed); + + if (newLineCount > 0) + { + _lineNumber += newLineCount; + int lastLF = whitespace.LastIndexOf(JsonConstants.LineFeed); + _bytePositionInLine = idx - lastLF - 1; + } + else + { + _bytePositionInLine += idx; + } + + _consumed += idx; + } +#else for (; _consumed < localBuffer.Length; _consumed++) { byte val = localBuffer[_consumed]; @@ -1031,6 +1065,7 @@ not JsonConstants.LineFeed and _bytePositionInLine++; } } +#endif } /// @@ -1603,6 +1638,20 @@ private ConsumeNumberResult ConsumeZero(ref ReadOnlySpan data, scoped ref private ConsumeNumberResult ConsumeIntegerDigits(ref ReadOnlySpan data, scoped ref int i) { +#if NET + int nonDigitOffset = data.Slice(i).IndexOfAnyExceptInRange((byte)'0', (byte)'9'); + byte nextByte; + if (nonDigitOffset < 0) + { + i = data.Length; + nextByte = default; + } + else + { + i += nonDigitOffset; + nextByte = data[i]; + } +#else byte nextByte = default; for (; i < data.Length; i++) { @@ -1612,6 +1661,7 @@ private ConsumeNumberResult ConsumeIntegerDigits(ref ReadOnlySpan data, sc break; } } +#endif if (i >= data.Length) { if (IsLastSpan) From 3cfb1664fa4a722331d01288f885d0cf85261cf1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 08:41:12 +0000 Subject: [PATCH 3/3] Extract SkipDigits helper to reduce repetition in digit scanning Extracts the repeated IndexOfAnyExceptInRange pattern into a shared JsonHelpers.SkipDigits helper method marked AggressiveInlining, eliminating the duplicated #if NET blocks across 3 call sites. Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --- .../src/System/Text/Json/JsonHelpers.cs | 33 ++++++++++++ .../Reader/Utf8JsonReader.MultiSegment.cs | 52 ++----------------- .../System/Text/Json/Reader/Utf8JsonReader.cs | 25 +-------- 3 files changed, 39 insertions(+), 71 deletions(-) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs index b551ef4dee1a56..eb356f88a0b03b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs @@ -163,6 +163,39 @@ public static bool IsInRangeInclusive(JsonTokenType value, JsonTokenType lowerBo /// public static bool IsDigit(byte value) => (uint)(value - '0') <= '9' - '0'; + /// + /// Advances past any consecutive ASCII digit bytes ('0'..'9') in . + /// On return, contains the first non-digit byte, or if the + /// end of the span was reached. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SkipDigits(ReadOnlySpan data, ref int i, out byte nextByte) + { +#if NET + int nonDigitOffset = data.Slice(i).IndexOfAnyExceptInRange((byte)'0', (byte)'9'); + if (nonDigitOffset < 0) + { + i = data.Length; + nextByte = default; + } + else + { + i += nonDigitOffset; + nextByte = data[i]; + } +#else + nextByte = default; + for (; i < data.Length; i++) + { + nextByte = data[i]; + if (!IsDigit(nextByte)) + { + break; + } + } +#endif + } + /// /// Perform a Read() with a Debug.Assert verifying the reader did not return false. /// This should be called when the Read() return value is not used, such as non-Stream cases where there is only one buffer. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs index bbc686e7e887be..6a1801c893a0cf 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs @@ -1354,32 +1354,10 @@ private ConsumeNumberResult ConsumeZeroMultiSegment(ref ReadOnlySpan data, private ConsumeNumberResult ConsumeIntegerDigitsMultiSegment(ref ReadOnlySpan data, scoped ref int i) { - byte nextByte = default; - int counter = 0; -#if NET - int nonDigitOffset = data.Slice(i).IndexOfAnyExceptInRange((byte)'0', (byte)'9'); - if (nonDigitOffset < 0) - { - counter = data.Length - i; - i = data.Length; - } - else - { - counter = nonDigitOffset; - i += nonDigitOffset; - nextByte = data[i]; - } -#else - for (; i < data.Length; i++) - { - nextByte = data[i]; - if (!JsonHelpers.IsDigit(nextByte)) - { - break; - } - counter++; - } -#endif + byte nextByte; + int startIndex = i; + JsonHelpers.SkipDigits(data, ref i, out nextByte); + int counter = i - startIndex; if (i >= data.Length) { if (IsLastSpan) @@ -1409,27 +1387,7 @@ private ConsumeNumberResult ConsumeIntegerDigitsMultiSegment(ref ReadOnlySpan= data.Length) { diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs index c0fc09e8ad6617..26d75bc8dedffd 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs @@ -1638,30 +1638,7 @@ private ConsumeNumberResult ConsumeZero(ref ReadOnlySpan data, scoped ref private ConsumeNumberResult ConsumeIntegerDigits(ref ReadOnlySpan data, scoped ref int i) { -#if NET - int nonDigitOffset = data.Slice(i).IndexOfAnyExceptInRange((byte)'0', (byte)'9'); - byte nextByte; - if (nonDigitOffset < 0) - { - i = data.Length; - nextByte = default; - } - else - { - i += nonDigitOffset; - nextByte = data[i]; - } -#else - byte nextByte = default; - for (; i < data.Length; i++) - { - nextByte = data[i]; - if (!JsonHelpers.IsDigit(nextByte)) - { - break; - } - } -#endif + JsonHelpers.SkipDigits(data, ref i, out byte nextByte); if (i >= data.Length) { if (IsLastSpan)