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)