Skip to content

Use safe span slice-advance loop in Number digit parser#127388

Closed
EgorBo wants to merge 2 commits intodotnet:mainfrom
EgorBo:number-digit-parser-safe-loop
Closed

Use safe span slice-advance loop in Number digit parser#127388
EgorBo wants to merge 2 commits intodotnet:mainfrom
EgorBo:number-digit-parser-safe-loop

Conversation

@EgorBo
Copy link
Copy Markdown
Member

@EgorBo EgorBo commented Apr 24, 2026

Note

This PR was authored with assistance from GitHub Copilot.

Replaces the raw byte* + Unsafe.ReadUnaligned<ulong> SWAR digit-parsing path in Number.NumberToFloatingPointBits.cs with the safe span-based "slice-advance" loop pattern that the JIT now recognizes and bounds-check elides.

This is the same pattern PR #127381 (NumericsHelpers) and #127382 (HashCode) use:

while (data.Length >= CONST)
{
    ...
    data = data.Slice(CONST);
}

Changes:

  • ParseEightDigitsUnrolled(byte*)ParseEightDigitsUnrolled(ReadOnlySpan<byte>). Uses BinaryPrimitives.ReadUInt64LittleEndian directly, which removes the manual if (!BitConverter.IsLittleEndian) val = ReverseEndianness(val); branch on big-endian.
  • DigitsToUInt32(byte*, int) / DigitsToUInt64(byte*, int)DigitsToUInt32(ReadOnlySpan<byte>) / DigitsToUInt64(ReadOnlySpan<byte>) with the JIT-friendly while (digits.Length >= 8) { …; digits = digits.Slice(8); } chunked shape and a safe foreach byte tail.
  • [RequiresUnsafe] removed from all three.
  • Updated the two callers (AccumulateDecimalDigitsIntoBigInteger and the ≤19-digit fast path in NumberToFloatingPointBits<TFloat>) to construct a ReadOnlySpan<byte> over number.DigitsPtr.

Codegen diff: TODO

Validation: full System.Runtime.Tests run — only the same 8 unrelated pre-existing failures (TimeZoneInfo NearMaxValue/NearMinValue, UnionAttributes); 7089 Single/Double/Half/Decimal parse tests pass.

Replaces the raw byte* + Unsafe.ReadUnaligned<ulong> SWAR digit-parsing path
with safe ReadOnlySpan<byte> using the JIT-friendly chunked
'while (data.Length >= CONST) { ...; data = data.Slice(CONST); }' pattern.

- ParseEightDigitsUnrolled now takes ReadOnlySpan<byte> and uses
  BinaryPrimitives.ReadUInt64LittleEndian, dropping the manual BE swap.
- DigitsToUInt32 / DigitsToUInt64 take ReadOnlySpan<byte> and use the safe
  chunked loop with a foreach byte tail.
- [RequiresUnsafe] removed from all three.
- Callers updated to construct ReadOnlySpan<byte> from number.DigitsPtr.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 24, 2026 13:44
@EgorBo
Copy link
Copy Markdown
Member Author

EgorBo commented Apr 24, 2026

@MihuBot

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the low-level decimal digit parsing logic used by NumberToFloatingPointBits to avoid raw pointer-walking + Unsafe.ReadUnaligned in favor of ReadOnlySpan<byte> and a JIT-friendly while (span.Length >= CONST) { …; span = span.Slice(CONST); } loop shape, aiming to preserve bounds-check elision while removing [RequiresUnsafe] usage from the helpers.

Changes:

  • Convert ParseEightDigitsUnrolled, DigitsToUInt32, and DigitsToUInt64 to ReadOnlySpan<byte>-based implementations.
  • Update AccumulateDecimalDigitsIntoBigInteger and the ≤19-digit fast path in NumberToFloatingPointBits<TFloat> to construct ReadOnlySpan<byte> over number.DigitsPtr.
  • Use BinaryPrimitives.ReadUInt64LittleEndian directly to handle endianness.

Address codegen regression in AccumulateDecimalDigitsIntoBigInteger reported
by jit-diff: the previous variable-count Slice(0, count) couldn't be
bounds-check elided because the JIT cannot reason through Math.Min(...).

Split the loop into a constant-step (9) chunked path that matches the JIT's
slice-advance pattern exactly, plus a single tail call for the 1..8 byte
remainder. Both Slice calls now have constants the JIT can reason about.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@EgorBo EgorBo marked this pull request as draft April 24, 2026 13:59
@EgorBo EgorBo marked this pull request as draft April 24, 2026 13:59
@EgorBo
Copy link
Copy Markdown
Member Author

EgorBo commented Apr 24, 2026

@MihuBot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants