Skip to content

Remove unsafe code from number parsing and formatting - #131913

Draft
tannergooding wants to merge 3 commits into
dotnet:mainfrom
tannergooding:tannergooding-remove-unsafe-number-parsing
Draft

Remove unsafe code from number parsing and formatting#131913
tannergooding wants to merge 3 commits into
dotnet:mainfrom
tannergooding:tannergooding-remove-unsafe-number-parsing

Conversation

@tannergooding

Copy link
Copy Markdown
Member

Replaces pointer-based parsing and formatting with span-based implementations across NumberBuffer, numeric types, DateTime, TimeSpan, decimal floating-point types, and BigInteger.

The replacement paths preserve direct writes while avoiding additional allocations, temporary copies, and per-element destination bounds checks.

Performance

Ratio is candidate / baseline; lower is better.

Area Result
Double parsing 0.86-0.91x
Exponent formatting 0.91-0.95x
Hex float formatting 0.71-0.78x
DateOnly R formatting 0.66x
BigInteger parsing/formatting generally 0.94-0.97x
Default integer decimal formatting neutral to approximately 24% faster
Explicit integer D/X/B formats neutral or faster; UInt128 B remained within approximately 1% noise
Grouped/custom span formatting up to approximately 1.10x
TimeSpan standard formatting approximately 1.03-1.07x

There were no allocation increases in the 82-benchmark matrix.


The remaining low-level operations are narrow, explicitly audited exceptions:

  • Integer ToString uses FastAllocateString and creates a writable span over the fresh exact-length string. The string cannot escape before every character is initialized.
  • Generic UTF-8/UTF-16 helpers use type- and size-proven Unsafe.BitCast operations.
  • Two-digit writers use MemoryMarshal.Write and a two-byte-aligned packed digit-pair representation to retain one store per pair without pointer arithmetic.
  • Fixed scratch buffers use measured Unsafe.SkipInit paths where all consumed elements are initialized before being read.
  • Existing RVA-table and BigInteger arithmetic reinterpretations remain where their sizes and representations are fixed.

No executable unsafe, pointer writers, pinning, or fixed statements remain in the affected paths.

The final baseline/candidate harness produced byte-identical output for 2,266 cases. System.Runtime.Tests passed 76,641 tests and System.Runtime.Numerics.Tests passed 8,422 tests.

Fixes #10397

Note

This description was drafted by Copilot.

Replace pointer-based parsing and formatting with span-based implementations while preserving direct writes and hot-path performance.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@tannergooding tannergooding added this to the 12.0.0 milestone Aug 5, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 removes remaining pointer-based/unsafe parsing and formatting across core number/date/time codepaths by switching to Span-based digit buffers and helper routines, aiming to keep the same output while improving safety and maintaining performance.

Changes:

  • Replaces byte* / fixed-based digit walking and writing with ReadOnlySpan<byte> / Span<TChar> implementations (including new two-digit writers and direct string-span initialization).
  • Refactors several formatting/parsing helpers (e.g., TryParseNumber, DigitsToUInt32/UInt64, BigInteger formatting) to avoid pinning and pointer arithmetic.
  • Updates DateTime/TimeSpan formatting routines to use span slices and builder copy patterns instead of raw pointers.
Show a summary per file
File Description
src/libraries/System.Runtime.Numerics/src/System/Number.Polyfill.cs Simplifies UTF-8/UTF-16 decode branches and span casts.
src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs Removes unsafe from BigInteger parse/format paths; introduces span-based digit writing helpers.
src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs Switches significand extraction from pointer to digit span slicing.
src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs Switches significand extraction from pointer to digit span slicing.
src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs Switches significand extraction from pointer to digit span slicing.
src/libraries/System.Private.CoreLib/src/System/Number.Rounding.cs Uses stackalloc-backed NumberBuffer span constructor instead of pointer-based buffer.
src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs Converts internal integer/decimal/float parsing helpers to span-based digit access.
src/libraries/System.Private.CoreLib/src/System/Number.NumberToFloatingPointBits.cs Replaces pointer-based digit parsing with span slicing and BinaryPrimitives-based reads.
src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs Reworks integer/decimal/float formatting to avoid pointers; adds direct writable string spans and digit-pair optimizations.
src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs Replaces Unsafe.SizeOf<T> with sizeof(T) for exponent bit computations.
src/libraries/System.Private.CoreLib/src/System/Number.BigInteger.cs Addresses scoped ref-safety and out-param constraints; avoids full buffer zeroing via SkipInit.
src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs Removes pointer writes; writes prefix/suffix via span slices.
src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs Simplifies UTF-8 cache span creation using AsSpan.
src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs Removes unsafe from ParseByFormat signature.
src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs Removes pointer writes; uses spans/builders for formatting.
src/libraries/Common/src/System/Number.Parsing.Common.cs Rewrites core number parsing from pointer range to span+index scanning.
src/libraries/Common/src/System/Number.NumberBuffer.cs Makes NumberBuffer non-unsafe; removes DigitsPtr and pointer-based ctor.
src/libraries/Common/src/System/Number.Formatting.Common.cs Adds span-based two-digit writers/tables and converts formatting helpers away from pointers.

Copilot's findings

  • Files reviewed: 18/18 changed files
  • Comments generated: 2

0x00300037u, 0x00310037u, 0x00320037u, 0x00330037u, 0x00340037u, 0x00350037u, 0x00360037u, 0x00370037u, 0x00380037u, 0x00390037u,
0x00300038u, 0x00310038u, 0x00320038u, 0x00330038u, 0x00340038u, 0x00350038u, 0x00360038u, 0x00370038u, 0x00380038u, 0x00390038u,
0x00300039u, 0x00310039u, 0x00320039u, 0x00330039u, 0x00340039u, 0x00350039u, 0x00360039u, 0x00370039u, 0x00380039u, 0x00390039u,
];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it's not possible to keep the previous readable strings/utf8 literals?

        private static ReadOnlySpan<byte> TwoDigitsCharsAsBytes =>
            MemoryMarshal.AsBytes<char>("00010203040506070809" +
                                        "10111213141516171819" +
                                        "20212223242526272829" +
                                        "30313233343536373839" +
                                        "40414243444546474849" +
                                        "50515253545556575859" +
                                        "60616263646566676869" +
                                        "70717273747576777879" +
                                        "80818283848586878889" +
                                        "90919293949596979899");
        private static ReadOnlySpan<byte> TwoDigitsBytes =>
                                        "00010203040506070809"u8 +
                                        "10111213141516171819"u8 +
                                        "20212223242526272829"u8 +
                                        "30313233343536373839"u8 +
                                        "40414243444546474849"u8 +
                                        "50515253545556575859"u8 +
                                        "60616263646566676869"u8 +
                                        "70717273747576777879"u8 +
                                        "80818283848586878889"u8 +
                                        "90919293949596979899"u8;

tannergooding and others added 2 commits August 5, 2026 17:13
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings August 6, 2026 00:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

Suppressed comments (1)

src/libraries/Common/src/System/Number.NumberBuffer.cs:36

  • The XML doc for the digits parameter still states the span must refer to unmovable memory (stack / pinned), but NumberBuffer no longer stores a raw pointer (DigitsPtr was removed) and all accesses are span-based. Callers can now safely pass spans backed by movable arrays (e.g., ArrayPool rentals), so this documentation is misleading/outdated.
            /// <param name="digits">The digits scratch space. The referenced memory must not be moveable, e.g. stack memory, pinned array, etc.</param>
  • Files reviewed: 18/18 changed files
  • Comments generated: 0 new

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.

Remove unsafe code from number parsing

4 participants