Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ internal static partial class JsonConstants
public static ReadOnlySpan<byte> NegativeInfinityValue => "-Infinity"u8;
public const int MaximumFloatingPointConstantLength = 9;

// On Mono/WASM AOT (browser), SkipWhiteSpace scans up to this many leading bytes with a
// scalar loop before promoting to a vectorized search, since the fixed per-call SIMD entry
// cost is high there relative to the short inter-token whitespace runs typical of indented
// JSON. Short runs (the common case) stay at scalar cost; longer runs promote to the
// vectorized path where it amortizes. Only consulted on the browser code path (see
// Utf8JsonReader.SkipWhiteSpace); other runtimes go straight to the vectorized search.
public const int MaxScalarWhiteSpaceScanLength = 16;
Comment on lines +48 to +54

// Used to search for the end of a number
public static ReadOnlySpan<byte> Delimiters => ",}] \n\r\t/"u8;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1009,29 +1009,23 @@ private void SkipWhiteSpace()
// Create local copy to avoid bounds checks.
ReadOnlySpan<byte> localBuffer = _buffer;
#if NET
// Vectorized scan to the first non-whitespace byte. The SearchValues-based
// IndexOfAnyExcept already handles short and long runs efficiently, so there is no
// need to special-case small inputs with a scalar pre-scan.
ReadOnlySpan<byte> remaining = localBuffer.Slice(_consumed);
int idx = remaining.IndexOfFirstNonWhiteSpace();
if (idx > 0)
{
// Reproduce the scalar loop's line/byte-position bookkeeping for the skipped run.
(int newLines, int lastLineFeedIndex) = JsonReaderHelper.CountNewLines(remaining.Slice(0, idx));
_lineNumber += newLines;
if (lastLineFeedIndex >= 0)
{
// Byte positions on the current line start after the last line feed character.
_bytePositionInLine = idx - lastLineFeedIndex - 1;
}
else
{
_bytePositionInLine += idx;
}

_consumed += idx;
// On most runtimes a vectorized SearchValues-based scan to the first non-whitespace byte
// is fastest across the board, so we go straight to it. On Mono/WASM AOT (browser) the
Comment on lines 1009 to +1013
// fixed per-call SIMD entry cost is high relative to the short inter-token whitespace runs
// typical of indented JSON, and reconstructing the line/byte-position bookkeeping requires
// up to two additional vectorized passes (CountNewLines). There a scalar pre-scan handles
// the common short runs in a single fused pass and only promotes to the vectorized search
// once a run is long enough to amortize that cost. OperatingSystem.IsBrowser() folds to a
// constant per target, so the unused branch is eliminated and non-browser codegen is
// unchanged from a direct vectorized scan.
if (!OperatingSystem.IsBrowser())
{
SkipWhiteSpaceVectorized(_consumed);
return;
}
#else

int whiteSpaceRun = 0;
#endif
for (; _consumed < localBuffer.Length; _consumed++)
{
byte val = localBuffer[_consumed];
Expand All @@ -1054,10 +1048,54 @@ not JsonConstants.LineFeed and
{
_bytePositionInLine++;
}
}

#if NET
// Browser only: the scalar loop above tracks the length of the current whitespace run.
// Once it reaches MaxScalarWhiteSpaceScanLength, the run is long enough that promoting
// to the vectorized search pays off, so hand the remainder of the buffer to it. Short
// runs (the common case) never reach this point and stay entirely scalar. The check
// lives inside the whitespace branch so it is never reached when the next token
// immediately follows (e.g. minified JSON).
if (++whiteSpaceRun == JsonConstants.MaxScalarWhiteSpaceScanLength)
{
SkipWhiteSpaceVectorized(_consumed + 1);
return;
}
#endif
}
}

#if NET
/// <summary>
/// Scans from <paramref name="startIndex"/> to the first non-whitespace byte using a
/// vectorized SearchValues-based search, advancing <see cref="_consumed"/> and updating the
/// line number and byte-position bookkeeping for the skipped whitespace run.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SkipWhiteSpaceVectorized(int startIndex)
{
_consumed = startIndex;
ReadOnlySpan<byte> remaining = _buffer.Slice(startIndex);
int idx = remaining.IndexOfFirstNonWhiteSpace();
if (idx > 0)
{
(int newLines, int lastLineFeedIndex) = JsonReaderHelper.CountNewLines(remaining.Slice(0, idx));
_lineNumber += newLines;
if (lastLineFeedIndex >= 0)
{
// Byte positions on the current line start after the last line feed character.
_bytePositionInLine = idx - lastLineFeedIndex - 1;
}
else
{
_bytePositionInLine += idx;
}

_consumed += idx;
}
}
#endif

/// <summary>
/// This method contains the logic for processing the next value token and determining
/// what type of data it is.
Expand Down
Loading