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 f31686429527d5..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,17 +1354,10 @@ private ConsumeNumberResult ConsumeZeroMultiSegment(ref ReadOnlySpan data, private ConsumeNumberResult ConsumeIntegerDigitsMultiSegment(ref ReadOnlySpan data, scoped ref int i) { - byte nextByte = default; - int counter = 0; - for (; i < data.Length; i++) - { - nextByte = data[i]; - if (!JsonHelpers.IsDigit(nextByte)) - { - break; - } - counter++; - } + byte nextByte; + int startIndex = i; + JsonHelpers.SkipDigits(data, ref i, out nextByte); + int counter = i - startIndex; if (i >= data.Length) { if (IsLastSpan) @@ -1394,14 +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 db764ccc153875..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 @@ -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,15 +1638,7 @@ private ConsumeNumberResult ConsumeZero(ref ReadOnlySpan data, scoped ref private ConsumeNumberResult ConsumeIntegerDigits(ref ReadOnlySpan data, scoped ref int i) { - byte nextByte = default; - for (; i < data.Length; i++) - { - nextByte = data[i]; - if (!JsonHelpers.IsDigit(nextByte)) - { - break; - } - } + JsonHelpers.SkipDigits(data, ref i, out byte nextByte); if (i >= data.Length) { if (IsLastSpan)