From 0944f2b8576c7186c06936cd518dec52e2a6beac Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Wed, 5 Aug 2026 16:37:44 -0700 Subject: [PATCH 1/3] Remove unsafe code from number parsing and formatting 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> --- .../src/System/Number.Formatting.Common.cs | 796 ++++++++----- .../Common/src/System/Number.NumberBuffer.cs | 12 +- .../src/System/Number.Parsing.Common.cs | 179 +-- .../System/Globalization/DateTimeFormat.cs | 412 +++---- .../src/System/Globalization/DateTimeParse.cs | 2 +- .../System/Globalization/NumberFormatInfo.cs | 3 +- .../System/Globalization/TimeSpanFormat.cs | 130 ++- .../src/System/Number.BigInteger.cs | 28 +- .../src/System/Number.DecimalIeee754.cs | 4 +- .../src/System/Number.Formatting.cs | 1024 +++++++---------- .../Number.NumberToFloatingPointBits.cs | 63 +- .../src/System/Number.Parsing.cs | 66 +- .../src/System/Number.Rounding.cs | 7 +- .../src/System/Numerics/Decimal128.cs | 4 +- .../src/System/Numerics/Decimal32.cs | 4 +- .../src/System/Numerics/Decimal64.cs | 4 +- .../src/System/Number.BigInteger.cs | 189 +-- .../src/System/Number.Polyfill.cs | 20 +- 18 files changed, 1452 insertions(+), 1495 deletions(-) diff --git a/src/libraries/Common/src/System/Number.Formatting.Common.cs b/src/libraries/Common/src/System/Number.Formatting.Common.cs index e24c971efc1c93..96f9be7ec864df 100644 --- a/src/libraries/Common/src/System/Number.Formatting.Common.cs +++ b/src/libraries/Common/src/System/Number.Formatting.Common.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -16,7 +17,6 @@ internal static partial class Number private const int DefaultPrecisionExponentialFormat = 6; - private const int MaxUInt32DecDigits = 10; private const string PosNumberFormat = "#"; private static readonly string[] s_posCurrencyFormats = @@ -122,24 +122,154 @@ internal static char ParseFormatSpecifier(ReadOnlySpan format, out int dig '\0'; } -#if !SYSTEM_PRIVATE_CORELIB + // Optimizations using "TwoDigits" inspired by: + // https://engineering.fb.com/2013/03/15/developer-tools/three-optimization-tips-for-c/ + // entry[v] = (byte)('0' + v/10) | ((byte)('0' + v%10) << 8), for writing two UTF-8 bytes as a single 2-byte store + private static ReadOnlySpan TwoDigitsBytesTable => + [ + 0x3030, 0x3130, 0x3230, 0x3330, 0x3430, 0x3530, 0x3630, 0x3730, 0x3830, 0x3930, + 0x3031, 0x3131, 0x3231, 0x3331, 0x3431, 0x3531, 0x3631, 0x3731, 0x3831, 0x3931, + 0x3032, 0x3132, 0x3232, 0x3332, 0x3432, 0x3532, 0x3632, 0x3732, 0x3832, 0x3932, + 0x3033, 0x3133, 0x3233, 0x3333, 0x3433, 0x3533, 0x3633, 0x3733, 0x3833, 0x3933, + 0x3034, 0x3134, 0x3234, 0x3334, 0x3434, 0x3534, 0x3634, 0x3734, 0x3834, 0x3934, + 0x3035, 0x3135, 0x3235, 0x3335, 0x3435, 0x3535, 0x3635, 0x3735, 0x3835, 0x3935, + 0x3036, 0x3136, 0x3236, 0x3336, 0x3436, 0x3536, 0x3636, 0x3736, 0x3836, 0x3936, + 0x3037, 0x3137, 0x3237, 0x3337, 0x3437, 0x3537, 0x3637, 0x3737, 0x3837, 0x3937, + 0x3038, 0x3138, 0x3238, 0x3338, 0x3438, 0x3538, 0x3638, 0x3738, 0x3838, 0x3938, + 0x3039, 0x3139, 0x3239, 0x3339, 0x3439, 0x3539, 0x3639, 0x3739, 0x3839, 0x3939, + ]; + + // entry[v] = (char)('0' + v/10) | ((char)('0' + v%10) << 16), for writing two UTF-16 chars as a single 4-byte store + private static ReadOnlySpan TwoDigitsCharsTable => + [ + 0x00300030u, 0x00310030u, 0x00320030u, 0x00330030u, 0x00340030u, 0x00350030u, 0x00360030u, 0x00370030u, 0x00380030u, 0x00390030u, + 0x00300031u, 0x00310031u, 0x00320031u, 0x00330031u, 0x00340031u, 0x00350031u, 0x00360031u, 0x00370031u, 0x00380031u, 0x00390031u, + 0x00300032u, 0x00310032u, 0x00320032u, 0x00330032u, 0x00340032u, 0x00350032u, 0x00360032u, 0x00370032u, 0x00380032u, 0x00390032u, + 0x00300033u, 0x00310033u, 0x00320033u, 0x00330033u, 0x00340033u, 0x00350033u, 0x00360033u, 0x00370033u, 0x00380033u, 0x00390033u, + 0x00300034u, 0x00310034u, 0x00320034u, 0x00330034u, 0x00340034u, 0x00350034u, 0x00360034u, 0x00370034u, 0x00380034u, 0x00390034u, + 0x00300035u, 0x00310035u, 0x00320035u, 0x00330035u, 0x00340035u, 0x00350035u, 0x00360035u, 0x00370035u, 0x00380035u, 0x00390035u, + 0x00300036u, 0x00310036u, 0x00320036u, 0x00330036u, 0x00340036u, 0x00350036u, 0x00360036u, 0x00370036u, 0x00380036u, 0x00390036u, + 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, + ]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort GetTwoDigitsBytes(uint value) + { + ushort pair = TwoDigitsBytesTable[(int)value]; + if (!BitConverter.IsLittleEndian) + { + pair = (ushort)((pair << 8) | (pair >> 8)); + } + return pair; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint GetTwoDigitsChars(uint value) + { + uint pair = TwoDigitsCharsTable[(int)value]; + if (!BitConverter.IsLittleEndian) + { + pair = uint.RotateRight(pair, 16); + } + return pair; + } + + /// Writes a value [ 00 .. 99 ] to the start of a pre-sliced 2-element span, using a single store. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void WriteTwoDigits(uint value, Span destination) where TChar : unmanaged, IUtfChar + { + Debug.Assert(value <= 99); + Debug.Assert(destination.Length >= 2); + Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); + + if (sizeof(TChar) == sizeof(char)) + { + // TwoDigitsCharsTable[v] = (char)('0'+v/10) | ((char)('0'+v%10) << 16) — write both chars as one 4-byte store. + uint pair = GetTwoDigitsChars(value); + MemoryMarshal.Write(MemoryMarshal.AsBytes(Unsafe.BitCast, Span>(destination)), in pair); + } + else + { + // Write both bytes as a single 2-byte store. + ushort pair = GetTwoDigitsBytes(value); + MemoryMarshal.Write(Unsafe.BitCast, Span>(destination), in pair); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void WriteTwoDigits(uint value, Span buffer, int index) where TChar : unmanaged, IUtfChar + { + Debug.Assert(value <= 99); + WriteTwoDigits(value, buffer.Slice(index, 2)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void CopyNegativeSign(ReadOnlySpan sign, Span destination) + { + if (sign.Length == 1) + { + destination[0] = sign[0]; + } + else + { + sign.CopyTo(destination); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe TChar* UInt32ToDecChars(TChar* bufferEnd, uint value, int digits) where TChar : unmanaged, IUtfChar + internal static int UInt32ToDecChars(Span buffer, int index, uint value) where TChar : unmanaged, IUtfChar { - // TODO: Consider to bring optimized implementation from CoreLib + Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); + + if (value >= 10) + { + // Handle all values >= 100 two-digits at a time so as to avoid expensive integer division operations. + while (value >= 100) + { + index -= 2; + (value, uint remainder) = Math.DivRem(value, 100); + WriteTwoDigits(remainder, buffer, index); + } + + // If there are two digits remaining, store them. + if (value >= 10) + { + index -= 2; + WriteTwoDigits(value, buffer, index); + return index; + } + } + + // Otherwise, store the single digit remaining. + buffer[--index] = TChar.CastFrom(value + '0'); + return index; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int UInt32ToDecChars(Span buffer, int index, uint value, int digits) where TChar : unmanaged, IUtfChar + { + Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); + uint remainder; + while (value >= 100) + { + index -= 2; + digits -= 2; + (value, remainder) = Math.DivRem(value, 100); + WriteTwoDigits(remainder, buffer, index); + } while (value != 0 || digits > 0) { digits--; - (value, uint remainder) = Math.DivRem(value, 10); - *(--bufferEnd) = TChar.CastFrom(remainder + '0'); + (value, remainder) = Math.DivRem(value, 10); + buffer[--index] = TChar.CastFrom(remainder + '0'); } - - return bufferEnd; + return index; } -#endif - internal static unsafe void NumberToString(ref ValueListBuilder vlb, ref NumberBuffer number, char format, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar + internal static void NumberToString(ref ValueListBuilder vlb, ref NumberBuffer number, char format, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); @@ -288,7 +418,7 @@ internal static unsafe void NumberToString(ref ValueListBuilder vl } } - internal static unsafe void NumberToStringFormat(ref ValueListBuilder vlb, ref NumberBuffer number, ReadOnlySpan format, NumberFormatInfo info) where TChar : unmanaged, IUtfChar + internal static void NumberToStringFormat(ref ValueListBuilder vlb, ref NumberBuffer number, ReadOnlySpan format, NumberFormatInfo info) where TChar : unmanaged, IUtfChar { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); @@ -308,10 +438,9 @@ internal static unsafe void NumberToStringFormat(ref ValueListBuilder(ref ValueListBuilder 0 && decimalPos < 0) + case ',': + if (digitCount > 0 && decimalPos < 0) + { + if (thousandPos >= 0) { - if (thousandPos >= 0) + if (thousandPos == digitCount) { - if (thousandPos == digitCount) - { - thousandCount++; - break; - } - thousandSeps = true; + thousandCount++; + break; } - thousandPos = digitCount; - thousandCount = 1; + thousandSeps = true; } - break; + thousandPos = digitCount; + thousandCount = 1; + } + break; - case '%': - scaleAdjust += 2; - break; + case '%': + scaleAdjust += 2; + break; - case '\x2030': - scaleAdjust += 3; - break; + case '\x2030': + scaleAdjust += 3; + break; - case '\'': - case '"': - while (src < format.Length && pFormat[src] != 0 && pFormat[src++] != ch) ; - break; + case '\'': + case '"': + while (src < format.Length && format[src] != 0 && format[src++] != ch) ; + break; - case '\\': - if (src < format.Length && pFormat[src] != 0) - { - src++; - } - break; + case '\\': + if (src < format.Length && format[src] != 0) + { + src++; + } + break; - case 'E': - case 'e': - if ((src < format.Length && pFormat[src] == '0') || - (src + 1 < format.Length && (pFormat[src] == '+' || pFormat[src] == '-') && pFormat[src + 1] == '0')) - { - while (++src < format.Length && pFormat[src] == '0') ; - scientific = true; - } - break; - } + case 'E': + case 'e': + if ((src < format.Length && format[src] == '0') || + (src + 1 < format.Length && (format[src] == '+' || format[src] == '-') && format[src + 1] == '0')) + { + while (++src < format.Length && format[src] == '0') ; + scientific = true; + } + break; } } @@ -418,12 +544,12 @@ internal static unsafe void NumberToStringFormat(ref ValueListBuilder(ref ValueListBuilder digits = number.Digits; + digits = digits.Slice(0, Math.Min(number.DigitsCount, digits.Length)); + int curIndex = 0; - while (src < format.Length && (ch = pFormat[src++]) != 0 && ch != ';') + while (src < format.Length && (ch = format[src++]) != 0 && ch != ';') + { + if (adjust > 0) { - if (adjust > 0) - { - switch (ch) - { - case '#': - case '0': - case '.': - while (adjust > 0) - { - // digPos will be one greater than thousandsSepPos[thousandsSepCtr] since we are at - // the character after which the groupSeparator needs to be appended. - vlb.Append(TChar.CastFrom(*cur != 0 ? (char)(*cur++) : '0')); - if (thousandSeps && digPos > 1 && thousandsSepCtr >= 0) - { - if (digPos == thousandsSepPos[thousandsSepCtr] + 1) - { - vlb.Append(info.NumberGroupSeparatorTChar()); - thousandsSepCtr--; - } - } - digPos--; - adjust--; - } - break; - } - } - switch (ch) { case '#': case '0': + case '.': + // Emit real digits for the first min(adjust, digits.Length) positions, + // then '0' padding for any remaining. The adjust loop always fires before + // any main-switch digit consumption (curIndex == 0 at entry), so + // Math.Min(adjust, digits.Length) is the tight bound, and iterating the + // slice itself lets the JIT eliminate the per-element bounds checks. + ReadOnlySpan adjustDigits = digits.Slice(0, Math.Min(adjust, digits.Length)); + for (int i = 0; i < adjustDigits.Length; i++) { - if (adjust < 0) - { - adjust++; - ch = digPos <= firstDigit ? '0' : '\0'; - } - else + // digPos will be one greater than thousandsSepPos[thousandsSepCtr] since we are at + // the character after which the groupSeparator needs to be appended. + vlb.Append(TChar.CastFrom((char)adjustDigits[i])); + if (thousandSeps && digPos > 1 && thousandsSepCtr >= 0) { - ch = *cur != 0 ? (char)(*cur++) : digPos > lastDigit ? '0' : '\0'; + if (digPos == thousandsSepPos[thousandsSepCtr] + 1) + { + vlb.Append(info.NumberGroupSeparatorTChar()); + thousandsSepCtr--; + } } - - if (ch != 0) + digPos--; + adjust--; + } + curIndex = adjustDigits.Length; + while (adjust > 0) + { + vlb.Append(TChar.CastFrom('0')); + if (thousandSeps && digPos > 1 && thousandsSepCtr >= 0) { - vlb.Append(TChar.CastFrom(ch)); - if (thousandSeps && digPos > 1 && thousandsSepCtr >= 0) + if (digPos == thousandsSepPos[thousandsSepCtr] + 1) { - if (digPos == thousandsSepPos[thousandsSepCtr] + 1) - { - vlb.Append(info.NumberGroupSeparatorTChar()); - thousandsSepCtr--; - } + vlb.Append(info.NumberGroupSeparatorTChar()); + thousandsSepCtr--; } } - digPos--; - break; + adjust--; } + break; + } + } - case '.': + switch (ch) + { + case '#': + case '0': + { + if (adjust < 0) { - if (digPos != 0 || decimalWritten) - { - // For compatibility, don't echo repeated decimals - break; - } + adjust++; + ch = digPos <= firstDigit ? '0' : '\0'; + } + else if (curIndex < digits.Length) + { + ch = (char)digits[curIndex++]; + } + else + { + ch = digPos > lastDigit ? '0' : '\0'; + } - // If the format has trailing zeros or the format has a decimal and digits remain - if (lastDigit < 0 || (decimalPos < digitCount && *cur != 0)) + if (ch != 0) + { + vlb.Append(TChar.CastFrom(ch)); + if (thousandSeps && digPos > 1 && thousandsSepCtr >= 0) { - vlb.Append(info.NumberDecimalSeparatorTChar()); - decimalWritten = true; + if (digPos == thousandsSepPos[thousandsSepCtr] + 1) + { + vlb.Append(info.NumberGroupSeparatorTChar()); + thousandsSepCtr--; + } } - break; } - case '\x2030': - vlb.Append(info.PerMilleSymbolTChar()); - break; - - case '%': - vlb.Append(info.PercentSymbolTChar()); - break; - - case ',': + digPos--; break; + } - case '\'': - case '"': - while (src < format.Length && pFormat[src] != 0 && pFormat[src] != ch) + case '.': + { + if (digPos != 0 || decimalWritten) { - AppendUnknownChar(ref vlb, pFormat[src++]); + // For compatibility, don't echo repeated decimals + break; } - if (src < format.Length && pFormat[src] != 0) + // If the format has trailing zeros or the format has a decimal and digits remain + if (lastDigit < 0 || (decimalPos < digitCount && curIndex < digits.Length)) { - src++; + vlb.Append(info.NumberDecimalSeparatorTChar()); + decimalWritten = true; } break; + } - case '\\': - if (src < format.Length && pFormat[src] != 0) + case '\x2030': + vlb.Append(info.PerMilleSymbolTChar()); + break; + + case '%': + vlb.Append(info.PercentSymbolTChar()); + break; + + case ',': + break; + + case '\'': + case '"': + while (src < format.Length) + { + char quoted = format[src]; + if (quoted == 0 || quoted == ch) { - AppendUnknownChar(ref vlb, pFormat[src++]); + break; } - break; + src++; + AppendUnknownChar(ref vlb, quoted); + } - case 'E': - case 'e': - { - bool positiveSign = false; - int i = 0; - if (scientific) - { - if (src < format.Length && pFormat[src] == '0') - { - // Handles E0, which should format the same as E-0 - i++; - } - else if (src + 1 < format.Length && pFormat[src] == '+' && pFormat[src + 1] == '0') - { - // Handles E+0 - positiveSign = true; - } - else if (src + 1 < format.Length && pFormat[src] == '-' && pFormat[src + 1] == '0') - { - // Handles E-0 - // Do nothing, this is just a place holder s.t. we don't break out of the loop. - } - else - { - vlb.Append(TChar.CastFrom(ch)); - break; - } + if (src < format.Length && format[src] != 0) + { + src++; + } + break; - while (++src < format.Length && pFormat[src] == '0') - { - i++; - } + case '\\': + if (src < format.Length && format[src] != 0) + { + AppendUnknownChar(ref vlb, format[src++]); + } + break; - if (i > 10) - { - i = 10; - } + case 'E': + case 'e': + { + bool positiveSign = false; + int i = 0; + if (scientific) + { + char exponentChar = src < format.Length ? format[src] : '\0'; + char exponentNext = src + 1 < format.Length ? format[src + 1] : '\0'; - int exp = dig[0] == 0 ? 0 : number.Scale - decimalPos; - FormatExponent(ref vlb, info, exp, ch, i, positiveSign); - scientific = false; + if (exponentChar == '0') + { + // Handles E0, which should format the same as E-0 + i++; + } + else if (exponentChar is '+' or '-' && exponentNext == '0') + { + // Handles E+0 and E-0; only E+0 emits a sign for positive exponents + positiveSign = exponentChar == '+'; } else { vlb.Append(TChar.CastFrom(ch)); - if (src < format.Length) + break; + } + + while (++src < format.Length && format[src] == '0') + { + i++; + } + + if (i > 10) + { + i = 10; + } + + int exp = number.Digits[0] == 0 ? 0 : number.Scale - decimalPos; + FormatExponent(ref vlb, info, exp, ch, i, positiveSign); + scientific = false; + } + else + { + vlb.Append(TChar.CastFrom(ch)); + if (src < format.Length) + { + if (format[src] is '+' or '-') { - if (pFormat[src] is '+' or '-') - { - AppendUnknownChar(ref vlb, pFormat[src++]); - } - - while (src < format.Length && pFormat[src] == '0') - { - AppendUnknownChar(ref vlb, pFormat[src++]); - } + AppendUnknownChar(ref vlb, format[src++]); + } + + while (src < format.Length && format[src] == '0') + { + AppendUnknownChar(ref vlb, format[src++]); } } - break; } - - default: - AppendUnknownChar(ref vlb, ch); break; - } + } + + default: + AppendUnknownChar(ref vlb, ch); + break; } } @@ -719,7 +877,7 @@ internal static unsafe void NumberToStringFormat(ref ValueListBuilder(ref ValueListBuilder vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar + private static void FormatCurrency(ref ValueListBuilder vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); @@ -750,7 +908,7 @@ private static unsafe void FormatCurrency(ref ValueListBuilder vlb } } - private static unsafe void FormatFixed( + private static void FormatFixed( ref ValueListBuilder vlb, ref NumberBuffer number, int nMaxDigits, int[]? groupDigits, ReadOnlySpan sDecimal, ReadOnlySpan sGroup) where TChar : unmanaged, IUtfChar @@ -758,7 +916,9 @@ private static unsafe void FormatFixed( Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); int digPos = number.Scale; - byte* dig = number.DigitsPtr; + ReadOnlySpan dig = number.Digits; + dig = dig.Slice(0, Math.Min(number.DigitsCount, dig.Length)); + int digIndex = 0; if (digPos > 0) { @@ -795,47 +955,68 @@ private static unsafe void FormatFixed( } groupSizeIndex = 0; - int digitCount = 0; - int digLength = number.DigitsCount; - int digStart = (digPos < digLength) ? digPos : digLength; - fixed (TChar* spanPtr = &MemoryMarshal.GetReference(vlb.AppendSpan(bufferSize))) + ReadOnlySpan intDigits = dig.Slice(0, Math.Min(digPos, dig.Length)); + Span buffer = vlb.AppendSpan(bufferSize); + int writePos = bufferSize; + int remainingDigits = digPos; + + while (remainingDigits > 0) { - TChar* p = spanPtr + bufferSize - 1; - for (int i = digPos - 1; i >= 0; i--) + int digitsInGroup = (groupSize > 0) ? Math.Min(groupSize, remainingDigits) : remainingDigits; + int groupStartDigit = remainingDigits - digitsInGroup; + int groupStartWrite = writePos - digitsInGroup; + + Span groupBuffer = buffer.Slice(groupStartWrite, digitsInGroup); + for (int j = 0; j < groupBuffer.Length; j++) { - *(p--) = TChar.CastFrom((i < digStart) ? (char)dig[i] : '0'); + int digitIndex = groupStartDigit + j; + groupBuffer[j] = TChar.CastFrom((uint)digitIndex < (uint)intDigits.Length ? (char)intDigits[digitIndex] : '0'); + } - if (groupSize > 0) + writePos = groupStartWrite; + remainingDigits -= digitsInGroup; + + if ((remainingDigits > 0) && (groupSize > 0)) + { + if (sGroup.Length == 1) { - digitCount++; - if ((digitCount == groupSize) && (i != 0)) - { - for (int j = sGroup.Length - 1; j >= 0; j--) - { - *(p--) = sGroup[j]; - } + writePos--; + buffer[writePos] = sGroup[0]; + } + else + { + writePos -= sGroup.Length; + sGroup.CopyTo(buffer.Slice(writePos, sGroup.Length)); + } - if (groupSizeIndex < groupDigits.Length - 1) - { - groupSizeIndex++; - groupSize = groupDigits[groupSizeIndex]; - } - digitCount = 0; - } + if (groupSizeIndex < groupDigits.Length - 1) + { + groupSizeIndex++; + groupSize = groupDigits[groupSizeIndex]; } } - - Debug.Assert(p >= spanPtr - 1, "Underflow"); - dig += digStart; } + + Debug.Assert(writePos == 0, "Underflow"); + digIndex = intDigits.Length; } else { - do + // Emit actual digits first, then trailing zeros. + // Split into two unconditional loops so the JIT can prove bounds safety + // for the digit loop (span iteration) and fully optimize the zero loop. + int actualDigits = Math.Min(digPos, dig.Length); + foreach (byte d in dig.Slice(0, actualDigits)) { - vlb.Append(TChar.CastFrom(*dig != 0 ? (char)(*dig++) : '0')); + vlb.Append(TChar.CastFrom((char)d)); + } + digIndex = actualDigits; + digPos -= actualDigits; + while (digPos > 0) + { + vlb.Append(TChar.CastFrom('0')); + digPos--; } - while (--digPos > 0); } } else @@ -853,13 +1034,19 @@ private static unsafe void FormatFixed( { vlb.Append(TChar.CastFrom('0')); } - digPos += zeroes; nMaxDigits -= zeroes; } + int remainingDig = dig.Length - digIndex; + int decActual = Math.Min(nMaxDigits, remainingDig); + foreach (byte d in dig.Slice(digIndex, decActual)) + { + vlb.Append(TChar.CastFrom((char)d)); + } + nMaxDigits -= decActual; while (nMaxDigits > 0) { - vlb.Append(TChar.CastFrom((*dig != 0) ? (char)(*dig++) : '0')); + vlb.Append(TChar.CastFrom('0')); nMaxDigits--; } } @@ -868,7 +1055,7 @@ private static unsafe void FormatFixed( /// Appends a char to the builder when the char is not known to be ASCII. /// This requires a helper as if the character isn't ASCII, for UTF-8 encoding it will result in multiple bytes added. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe void AppendUnknownChar(ref ValueListBuilder vlb, char ch) where TChar : unmanaged, IUtfChar + private static void AppendUnknownChar(ref ValueListBuilder vlb, char ch) where TChar : unmanaged, IUtfChar { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); @@ -885,11 +1072,11 @@ private static unsafe void AppendUnknownChar(ref ValueListBuilder static void AppendNonAsciiBytes(ref ValueListBuilder vlb, char ch) { var r = new Rune(ch); - r.EncodeToUtf8(MemoryMarshal.AsBytes(vlb.AppendSpan(r.Utf8SequenceLength))); + r.EncodeToUtf8(Unsafe.BitCast, Span>(vlb.AppendSpan(r.Utf8SequenceLength))); } } - private static unsafe void FormatNumber(ref ValueListBuilder vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar + private static void FormatNumber(ref ValueListBuilder vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); @@ -916,29 +1103,41 @@ private static unsafe void FormatNumber(ref ValueListBuilder vlb, } } - private static unsafe void FormatScientific(ref ValueListBuilder vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info, char expChar) where TChar : unmanaged, IUtfChar + private static void FormatScientific(ref ValueListBuilder vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info, char expChar) where TChar : unmanaged, IUtfChar { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); - byte* dig = number.DigitsPtr; + ReadOnlySpan dig = number.Digits; + dig = dig.Slice(0, Math.Min(number.DigitsCount, dig.Length)); - vlb.Append(TChar.CastFrom((*dig != 0) ? (char)(*dig++) : '0')); + // Emit the leading digit, or '0' when the value has no digits. + vlb.Append(TChar.CastFrom(!dig.IsEmpty ? (char)dig[0] : '0')); if (nMaxDigits != 1) // For E0 we would like to suppress the decimal point { vlb.Append(info.NumberDecimalSeparatorTChar()); } - while (--nMaxDigits > 0) + // Emit the remaining nMaxDigits - 1 digits, padding with '0' once exhausted. + int emitted = 1; + if (dig.Length > 1) { - vlb.Append(TChar.CastFrom((*dig != 0) ? (char)(*dig++) : '0')); + foreach (byte b in dig.Slice(1, Math.Min(nMaxDigits - 1, dig.Length - 1))) + { + vlb.Append(TChar.CastFrom((char)b)); + emitted++; + } + } + for (; emitted < nMaxDigits; emitted++) + { + vlb.Append(TChar.CastFrom('0')); } int e = number.Digits[0] == 0 ? 0 : number.Scale - 1; FormatExponent(ref vlb, info, e, expChar, 3, true); } - private static unsafe void FormatExponent(ref ValueListBuilder vlb, NumberFormatInfo info, int value, char expChar, int minDigits, bool positiveSign) where TChar : unmanaged, IUtfChar + private static void FormatExponent(ref ValueListBuilder vlb, NumberFormatInfo info, int value, char expChar, int minDigits, bool positiveSign) where TChar : unmanaged, IUtfChar { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); @@ -957,12 +1156,13 @@ private static unsafe void FormatExponent(ref ValueListBuilder vlb } } - TChar* digits = stackalloc TChar[MaxUInt32DecDigits]; - TChar* p = UInt32ToDecChars(digits + MaxUInt32DecDigits, (uint)value, minDigits); - vlb.Append(new ReadOnlySpan(p, (int)(digits + MaxUInt32DecDigits - p))); + int digitCount = Math.Max(minDigits, FormattingHelpers.CountDigits((uint)value)); + Span digits = vlb.AppendSpan(digitCount); + int pos = UInt32ToDecChars(digits, digitCount, (uint)value, minDigits); + Debug.Assert(pos == 0); } - private static unsafe void FormatGeneral(ref ValueListBuilder vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info, char expChar, bool suppressScientific) where TChar : unmanaged, IUtfChar + private static void FormatGeneral(ref ValueListBuilder vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info, char expChar, bool suppressScientific) where TChar : unmanaged, IUtfChar { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); @@ -979,22 +1179,29 @@ private static unsafe void FormatGeneral(ref ValueListBuilder vlb, } } - byte* dig = number.DigitsPtr; + ReadOnlySpan dig = number.Digits; + dig = dig.Slice(0, Math.Min(number.DigitsCount, dig.Length)); if (digPos > 0) { - do + // Emit the available integer digits, then pad with '0' up to digPos. + int intCount = Math.Min(digPos, dig.Length); + foreach (byte b in dig.Slice(0, intCount)) + { + vlb.Append(TChar.CastFrom((char)b)); + } + for (int i = intCount; i < digPos; i++) { - vlb.Append(TChar.CastFrom((*dig != 0) ? (char)(*dig++) : '0')); + vlb.Append(TChar.CastFrom('0')); } - while (--digPos > 0); + dig = dig.Slice(intCount); } else { vlb.Append(TChar.CastFrom('0')); } - if (*dig != 0 || digPos < 0) + if (!dig.IsEmpty || digPos < 0) { vlb.Append(info.NumberDecimalSeparatorTChar()); @@ -1004,9 +1211,9 @@ private static unsafe void FormatGeneral(ref ValueListBuilder vlb, digPos++; } - while (*dig != 0) + foreach (byte b in dig) { - vlb.Append(TChar.CastFrom(*dig++)); + vlb.Append(TChar.CastFrom((char)b)); } } @@ -1016,7 +1223,7 @@ private static unsafe void FormatGeneral(ref ValueListBuilder vlb, } } - private static unsafe void FormatPercent(ref ValueListBuilder vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar + private static void FormatPercent(ref ValueListBuilder vlb, ref NumberBuffer number, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar { Debug.Assert(sizeof(TChar) is sizeof(char) or sizeof(byte)); @@ -1047,9 +1254,9 @@ private static unsafe void FormatPercent(ref ValueListBuilder vlb, } } - internal static unsafe void RoundNumber(ref NumberBuffer number, int pos, bool isCorrectlyRounded) + internal static void RoundNumber(ref NumberBuffer number, int pos, bool isCorrectlyRounded) { - byte* dig = number.DigitsPtr; + Span dig = number.Digits; int i = 0; while (i < pos && dig[i] != '\0') @@ -1097,7 +1304,7 @@ internal static unsafe void RoundNumber(ref NumberBuffer number, int pos, bool i number.DigitsCount = i; number.CheckConsistency(); - static bool ShouldRoundUp(byte* dig, int i, NumberBufferKind numberKind, bool isCorrectlyRounded) + static bool ShouldRoundUp(ReadOnlySpan dig, int i, NumberBufferKind numberKind, bool isCorrectlyRounded) { // We only want to round up if the digit is greater than or equal to 5 and we are // not rounding a floating-point number. If we are rounding a floating-point number @@ -1159,7 +1366,7 @@ static bool ShouldRoundUp(byte* dig, int i, NumberBufferKind numberKind, bool is // non-zero result reliably indicates the format defines a dedicated negative section. private static bool HasNegativeSection(ReadOnlySpan format) => FindSection(format, 1) != 0; - private static unsafe int FindSection(ReadOnlySpan format, int section) + private static int FindSection(ReadOnlySpan format, int section) { int src; char ch; @@ -1169,45 +1376,42 @@ private static unsafe int FindSection(ReadOnlySpan format, int section) return 0; } - fixed (char* pFormat = &MemoryMarshal.GetReference(format)) + src = 0; + while (true) { - src = 0; - while (true) + if (src >= format.Length) { - if (src >= format.Length) - { - return 0; - } + return 0; + } - switch (ch = pFormat[src++]) - { - case '\'': - case '"': - while (src < format.Length && pFormat[src] != 0 && pFormat[src++] != ch) ; - break; + switch (ch = format[src++]) + { + case '\'': + case '"': + while (src < format.Length && format[src] != 0 && format[src++] != ch) ; + break; - case '\\': - if (src < format.Length && pFormat[src] != 0) - { - src++; - } - break; + case '\\': + if (src < format.Length && format[src] != 0) + { + src++; + } + break; - case ';': - if (--section != 0) - { - break; - } + case ';': + if (--section != 0) + { + break; + } - if (src < format.Length && pFormat[src] != 0 && pFormat[src] != ';') - { - return src; - } - goto case '\0'; + if (src < format.Length && format[src] is not ('\0' or ';')) + { + return src; + } + goto case '\0'; - case '\0': - return 0; - } + case '\0': + return 0; } } } diff --git a/src/libraries/Common/src/System/Number.NumberBuffer.cs b/src/libraries/Common/src/System/Number.NumberBuffer.cs index 4a32dfdd224384..8f8db63afde5f0 100644 --- a/src/libraries/Common/src/System/Number.NumberBuffer.cs +++ b/src/libraries/Common/src/System/Number.NumberBuffer.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text; namespace System @@ -25,7 +23,7 @@ internal static partial class Number internal const int Decimal64NumberBufferLength = 16 + 1 + 1; // 16 for the longest input + 1 for rounding internal const int Decimal128NumberBufferLength = 34 + 1 + 1; // 34 for the longest input + 1 for rounding - internal unsafe ref struct NumberBuffer + internal ref struct NumberBuffer { public int DigitsCount; public int Scale; @@ -33,14 +31,6 @@ internal unsafe ref struct NumberBuffer public bool HasNonZeroTail; public NumberBufferKind Kind; public Span Digits; - /// Converts the ref to Digits into a pointer value via Unsafe.AsPointer and returns it without dereferencing; the result is not GC-tracked, so any use must be in an unsafe context that establishes Digits still refers to unmovable memory. - public readonly byte* DigitsPtr => (byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(Digits)); // safe since constructor expects Digits to refer to unmovable memory - - public NumberBuffer(NumberBufferKind kind, byte* digits, int digitsLength) : this(kind, new Span(digits, digitsLength)) - { - Debug.Assert(digits != null); - } - /// Initializes the NumberBuffer. /// The kind of the buffer. /// The digits scratch space. The referenced memory must not be moveable, e.g. stack memory, pinned array, etc. diff --git a/src/libraries/Common/src/System/Number.Parsing.Common.cs b/src/libraries/Common/src/System/Number.Parsing.Common.cs index c7203943c0eeba..64cae26693b115 100644 --- a/src/libraries/Common/src/System/Number.Parsing.Common.cs +++ b/src/libraries/Common/src/System/Number.Parsing.Common.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace System { @@ -16,14 +15,9 @@ internal static partial class Number // highest bit (0x8000_0000) so public flags can keep growing upward without stomping it; see NumberStyles. internal const NumberStyles AllowTrailingInvalidCharacters = unchecked((NumberStyles)0x80000000); - private static unsafe bool TryParseNumber(TChar* str, TChar* strEnd, NumberStyles styles, ref NumberBuffer number, NumberFormatInfo info, out int elementsConsumed) + private static bool TryParseNumber(ReadOnlySpan value, NumberStyles styles, ref NumberBuffer number, NumberFormatInfo info, out int elementsConsumed) where TChar : unmanaged, IUtfChar { - // str/strEnd may be null when the input is an empty span (e.g. default(ReadOnlySpan) - // originating from a null string), in which case they are both null and the range is empty. - Debug.Assert((str != null) || (str == strEnd)); - Debug.Assert((strEnd != null) || (str == strEnd)); - Debug.Assert(str <= strEnd); Debug.Assert((styles & (NumberStyles.AllowHexSpecifier | NumberStyles.AllowBinarySpecifier)) == 0); const int StateSign = 0x0001; @@ -62,9 +56,8 @@ private static unsafe bool TryParseNumber(TChar* str, TChar* strEnd, Numb } int state = 0; - TChar* p = str; - uint ch = (p < strEnd) ? TChar.CastToUInt32(*p) : '\0'; - TChar* next; + int index = 0; + uint ch = index < value.Length ? TChar.CastToUInt32(value[index]) : '\0'; while (true) { @@ -72,30 +65,38 @@ private static unsafe bool TryParseNumber(TChar* str, TChar* strEnd, Numb // "-Kr 1231.47" is legal but "- 1231.47" is not. if (!IsWhite(ch) || (styles & NumberStyles.AllowLeadingWhite) == 0 || ((state & StateSign) != 0 && (state & StateCurrency) == 0 && info.NumberNegativePattern != 2)) { - if (((styles & NumberStyles.AllowLeadingSign) != 0) && (state & StateSign) == 0 && ((next = MatchChars(p, strEnd, info.PositiveSignTChar())) != null || ((next = MatchNegativeSignChars(p, strEnd, info)) != null && (number.IsNegative = true)))) + int nextIndex; + + if (((styles & NumberStyles.AllowLeadingSign) != 0) && (state & StateSign) == 0 && (((nextIndex = MatchChars(value, index, info.PositiveSignTChar())) >= 0) || (((nextIndex = MatchNegativeSignChars(value, index, info)) >= 0) && (number.IsNegative = true)))) { state |= StateSign; - p = next - 1; + index = nextIndex; } else if (ch == '(' && ((styles & NumberStyles.AllowParentheses) != 0) && ((state & StateSign) == 0)) { state |= StateSign | StateParens; number.IsNegative = true; + index++; } - else if (!currSymbol.IsEmpty && (next = MatchChars(p, strEnd, currSymbol)) != null) + else if (!currSymbol.IsEmpty && (nextIndex = MatchChars(value, index, currSymbol)) >= 0) { state |= StateCurrency; currSymbol = ReadOnlySpan.Empty; // We already found the currency symbol. There should not be more currency symbols. Set // currSymbol to NULL so that we won't search it again in the later code path. - p = next - 1; + index = nextIndex; } else { break; } } - ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0'; + else + { + index++; + } + + ch = index < value.Length ? TChar.CastToUInt32(value[index]) : '\0'; } int digCount = 0; @@ -156,20 +157,30 @@ private static unsafe bool TryParseNumber(TChar* str, TChar* strEnd, Numb number.Scale--; } } - else if (((styles & NumberStyles.AllowDecimalPoint) != 0) && ((state & StateDecimal) == 0) && ((next = MatchChars(p, strEnd, decSep)) != null || (parsingCurrency && (state & StateCurrency) == 0 && (next = MatchChars(p, strEnd, info.NumberDecimalSeparatorTChar())) != null))) - { - state |= StateDecimal; - p = next - 1; - } - else if (((styles & NumberStyles.AllowThousands) != 0) && ((state & StateDigits) != 0) && ((state & StateDecimal) == 0) && ((next = MatchChars(p, strEnd, groupSep)) != null || (parsingCurrency && (state & StateCurrency) == 0 && (next = MatchChars(p, strEnd, info.NumberGroupSeparatorTChar())) != null))) + else { - p = next - 1; + int nextIndex; + + if (((styles & NumberStyles.AllowDecimalPoint) != 0) && ((state & StateDecimal) == 0) && ((nextIndex = MatchChars(value, index, decSep)) >= 0 || (parsingCurrency && (state & StateCurrency) == 0 && (nextIndex = MatchChars(value, index, info.NumberDecimalSeparatorTChar())) >= 0))) + { + state |= StateDecimal; + index = nextIndex; + } + else if (((styles & NumberStyles.AllowThousands) != 0) && ((state & StateDigits) != 0) && ((state & StateDecimal) == 0) && ((nextIndex = MatchChars(value, index, groupSep)) >= 0 || (parsingCurrency && (state & StateCurrency) == 0 && (nextIndex = MatchChars(value, index, info.NumberGroupSeparatorTChar())) >= 0))) + { + index = nextIndex; + } + else + { + break; + } } - else + if (IsDigit(ch)) { - break; + index++; } - ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0'; + + ch = index < value.Length ? TChar.CastToUInt32(value[index]) : '\0'; } bool negExp = false; @@ -179,17 +190,23 @@ private static unsafe bool TryParseNumber(TChar* str, TChar* strEnd, Numb { if ((ch == 'E' || ch == 'e') && ((styles & NumberStyles.AllowExponent) != 0)) { - TChar* temp = p; - ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0'; - if ((next = MatchChars(p, strEnd, info.PositiveSignTChar())) != null) + int exponentIndex = index; + index++; + ch = index < value.Length ? TChar.CastToUInt32(value[index]) : '\0'; + + int nextIndex = MatchChars(value, index, info.PositiveSignTChar()); + if (nextIndex >= 0) { - ch = (p = next) < strEnd ? TChar.CastToUInt32(*p) : '\0'; + index = nextIndex; + ch = index < value.Length ? TChar.CastToUInt32(value[index]) : '\0'; } - else if ((next = MatchNegativeSignChars(p, strEnd, info)) != null) + else if ((nextIndex = MatchNegativeSignChars(value, index, info)) >= 0) { - ch = (p = next) < strEnd ? TChar.CastToUInt32(*p) : '\0'; + index = nextIndex; + ch = index < value.Length ? TChar.CastToUInt32(value[index]) : '\0'; negExp = true; } + if (IsDigit(ch)) { int exp = 0; @@ -205,13 +222,15 @@ private static unsafe bool TryParseNumber(TChar* str, TChar* strEnd, Numb // Finish parsing the number, a FormatException could still occur later on. while (IsDigit(ch)) { - ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0'; + index++; + ch = index < value.Length ? TChar.CastToUInt32(value[index]) : '\0'; } break; } exp = (exp * 10) + (int)(ch - '0'); - ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0'; + index++; + ch = index < value.Length ? TChar.CastToUInt32(value[index]) : '\0'; } while (IsDigit(ch)); if (negExp) { @@ -221,8 +240,8 @@ private static unsafe bool TryParseNumber(TChar* str, TChar* strEnd, Numb } else { - p = temp; - ch = p < strEnd ? TChar.CastToUInt32(*p) : '\0'; + index = exponentIndex; + ch = TChar.CastToUInt32(value[index]); } } @@ -243,26 +262,35 @@ private static unsafe bool TryParseNumber(TChar* str, TChar* strEnd, Numb { if (!IsWhite(ch) || (styles & NumberStyles.AllowTrailingWhite) == 0) { - if ((styles & NumberStyles.AllowTrailingSign) != 0 && ((state & StateSign) == 0) && ((next = MatchChars(p, strEnd, info.PositiveSignTChar())) != null || (((next = MatchNegativeSignChars(p, strEnd, info)) != null) && (number.IsNegative = true)))) + int nextIndex; + + if ((styles & NumberStyles.AllowTrailingSign) != 0 && ((state & StateSign) == 0) && (((nextIndex = MatchChars(value, index, info.PositiveSignTChar())) >= 0) || ((((nextIndex = MatchNegativeSignChars(value, index, info)) >= 0)) && (number.IsNegative = true)))) { state |= StateSign; - p = next - 1; + index = nextIndex; } else if (ch == ')' && ((state & StateParens) != 0)) { state &= ~StateParens; + index++; } - else if (!currSymbol.IsEmpty && (next = MatchChars(p, strEnd, currSymbol)) != null) + else if (!currSymbol.IsEmpty && (nextIndex = MatchChars(value, index, currSymbol)) >= 0) { currSymbol = ReadOnlySpan.Empty; - p = next - 1; + index = nextIndex; } else { break; } } - ch = ++p < strEnd ? TChar.CastToUInt32(*p) : '\0'; + + if (IsWhite(ch)) + { + index++; + } + + ch = index < value.Length ? TChar.CastToUInt32(value[index]) : '\0'; } if ((state & StateParens) == 0) { @@ -278,9 +306,6 @@ private static unsafe bool TryParseNumber(TChar* str, TChar* strEnd, Numb } } - int index = (int)(p - str); - var value = new ReadOnlySpan(str, (int)(strEnd - str)); - // For compatibility we still need to process any trailing // nulls that exist and report them as having been consumed. @@ -298,17 +323,14 @@ private static unsafe bool TryParseNumber(TChar* str, TChar* strEnd, Numb return false; } - internal static unsafe bool TryStringToNumber(ReadOnlySpan value, NumberStyles styles, ref NumberBuffer number, NumberFormatInfo info, out int elementsConsumed) + internal static bool TryStringToNumber(ReadOnlySpan value, NumberStyles styles, ref NumberBuffer number, NumberFormatInfo info, out int elementsConsumed) where TChar : unmanaged, IUtfChar { Debug.Assert(info != null); - fixed (TChar* stringPointer = &MemoryMarshal.GetReference(value)) - { - bool succeeded = TryParseNumber(stringPointer, stringPointer + value.Length, styles, ref number, info, out elementsConsumed); - number.CheckConsistency(); - return succeeded; - } + bool succeeded = TryParseNumber(value, styles, ref number, info, out elementsConsumed); + number.CheckConsistency(); + return succeeded; } private static int ConsumeTrailingNulls(ReadOnlySpan value, int index) @@ -338,63 +360,46 @@ internal enum ParsingStatus private static uint NormalizeSpaceReplacingChar(uint c) => IsSpaceReplacingChar(c) ? '\u0020' : c; [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe TChar* MatchNegativeSignChars(TChar* p, TChar* pEnd, NumberFormatInfo info) + private static int MatchNegativeSignChars(ReadOnlySpan value, int index, NumberFormatInfo info) where TChar : unmanaged, IUtfChar { - TChar* ret = MatchChars(p, pEnd, info.NegativeSignTChar()); + int nextIndex = MatchChars(value, index, info.NegativeSignTChar()); - if ((ret is null) && info.AllowHyphenDuringParsing() && (p < pEnd) && (TChar.CastToUInt32(*p) == '-')) + if ((nextIndex < 0) && info.AllowHyphenDuringParsing() && ((uint)index < (uint)value.Length) && (TChar.CastToUInt32(value[index]) == '-')) { - ret = p + 1; + nextIndex = index + 1; } - return ret; + return nextIndex; } - private static unsafe TChar* MatchChars(TChar* p, TChar* pEnd, ReadOnlySpan value) + private static int MatchChars(ReadOnlySpan source, int index, ReadOnlySpan value) where TChar : unmanaged, IUtfChar { - // p/pEnd may be null when the input being parsed is an empty span (e.g. from a null string), - // in which case they are both null and the range is empty; the length check below then rejects - // any non-empty pattern before the loop can dereference p. - Debug.Assert((p != null) || (p == pEnd)); - Debug.Assert((pEnd != null) || (p == pEnd)); - Debug.Assert(p <= pEnd); - // An empty pattern never matches, and one longer than the remaining input cannot match, so // the loop only has to bound itself by the pattern. - if (value.IsEmpty || (value.Length > (pEnd - p))) + if (value.IsEmpty || (value.Length > (source.Length - index))) { - return null; + return -1; } - fixed (TChar* stringPointer = &MemoryMarshal.GetReference(value)) + for (int i = 0; i < value.Length; i++) { - TChar* str = stringPointer; - TChar* strEnd = stringPointer + value.Length; - - do + uint cp = TChar.CastToUInt32(source[index + i]); + uint val = TChar.CastToUInt32(value[i]); + + // We only hurt the failure case + // This fix is for cultures that use NBSP (U+00A0) or narrow NBSP (U+202F) as group/decimal separators + // (e.g., French, Kazakh, Ukrainian). Since a user cannot easily type these characters, + // we accept regular space (U+0020) as equivalent. + // We also need to handle the reverse case where the input has NBSP and the format string has space. + if (cp != val && NormalizeSpaceReplacingChar(cp) != NormalizeSpaceReplacingChar(val)) { - uint cp = TChar.CastToUInt32(*p); - uint val = TChar.CastToUInt32(*str); - - // We only hurt the failure case - // This fix is for cultures that use NBSP (U+00A0) or narrow NBSP (U+202F) as group/decimal separators - // (e.g., French, Kazakh, Ukrainian). Since a user cannot easily type these characters, - // we accept regular space (U+0020) as equivalent. - // We also need to handle the reverse case where the input has NBSP and the format string has space. - if (cp != val && NormalizeSpaceReplacingChar(cp) != NormalizeSpaceReplacingChar(val)) - { - return null; - } - - p++; - str++; + return -1; } - while (str != strEnd); } - return p; + return index + value.Length; } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs index 13b6ceb1e35da2..ca4d00c6eb772b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics; @@ -161,7 +162,7 @@ internal static class DateTimeFormat /// /// The minimum length for formatted number. If the number of digits in the value is less than this length, it will be padded with leading zeros. /// - internal static unsafe void FormatDigits(ref ValueListBuilder outputBuffer, int value, int minimumLength) where TChar : unmanaged, IUtfChar + internal static void FormatDigits(ref ValueListBuilder outputBuffer, int value, int minimumLength) where TChar : unmanaged, IUtfChar { Debug.Assert(value >= 0, "DateTimeFormat.FormatDigits(): value >= 0"); Debug.Assert(minimumLength <= 16); @@ -173,23 +174,18 @@ internal static unsafe void FormatDigits(ref ValueListBuilder outp break; case 2 when value < 100: - fixed (TChar* ptr = &MemoryMarshal.GetReference(outputBuffer.AppendSpan(2))) - { - Number.WriteTwoDigits((uint)value, ptr); - } + Number.WriteTwoDigits((uint)value, outputBuffer.AppendSpan(2)); break; case 4 when value < 10000: - fixed (TChar* ptr = &MemoryMarshal.GetReference(outputBuffer.AppendSpan(4))) - { - Number.WriteFourDigits((uint)value, ptr); - } + Number.WriteFourDigits((uint)value, outputBuffer.AppendSpan(4)); break; default: - TChar* buffer = stackalloc TChar[16]; - TChar* p = Number.UInt32ToDecChars(buffer + 16, (uint)value, minimumLength); - outputBuffer.Append(new ReadOnlySpan(p, (int)(buffer + 16 - p))); + int digitCount = Math.Max(minimumLength, FormattingHelpers.CountDigits((uint)value)); + Span buffer = outputBuffer.AppendSpan(digitCount); + int pos = Number.UInt32ToDecChars(buffer, digitCount, (uint)value, minimumLength); + Debug.Assert(pos == 0); break; } } @@ -772,19 +768,27 @@ private static void AppendString(ref ValueListBuilder result, scop } } - internal static unsafe void FormatFraction(ref ValueListBuilder result, int fraction, ReadOnlySpan fractionFormat) where TChar : unmanaged, IUtfChar + internal static void FormatFraction(ref ValueListBuilder result, int fraction, ReadOnlySpan fractionFormat) where TChar : unmanaged, IUtfChar { - Span chars = stackalloc TChar[11]; + Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); + + // An inline array rather than a stackalloc: the latter's localloc keeps this method + // from being inlined into the FormatCustomized loop that calls it. + Unsafe.SkipInit(out InlineArray11 buffer); + Span chars = buffer; + int charCount; bool formatted = typeof(TChar) == typeof(char) ? fraction.TryFormat(Unsafe.BitCast, Span>(chars), out charCount, fractionFormat, CultureInfo.InvariantCulture) : fraction.TryFormat(Unsafe.BitCast, Span>(chars), out charCount, fractionFormat, CultureInfo.InvariantCulture); + Debug.Assert(formatted); Debug.Assert(charCount != 0); + result.Append(chars.Slice(0, charCount)); } // output the 'z' family of formats, which output a the offset from UTC, e.g. "-07:30" - private static unsafe void FormatCustomizedTimeZone(DateTime dateTime, TimeSpan offset, int tokenLen, bool timeOnly, ref ValueListBuilder result) where TChar : unmanaged, IUtfChar + private static void FormatCustomizedTimeZone(DateTime dateTime, TimeSpan offset, int tokenLen, bool timeOnly, ref ValueListBuilder result) where TChar : unmanaged, IUtfChar { // See if the instance already has an offset bool dateTimeFormat = offset.Ticks == NullOffset; @@ -831,25 +835,20 @@ private static unsafe void FormatCustomizedTimeZone(DateTime dateTime, Ti else if (tokenLen == 2) { // 'zz' format e.g "-07" - fixed (TChar* p = &MemoryMarshal.GetReference(result.AppendSpan(2))) - { - Number.WriteTwoDigits((uint)offset.Hours, p); - } + Number.WriteTwoDigits((uint)offset.Hours, result.AppendSpan(2)); } else { Debug.Assert(tokenLen >= 3); - fixed (TChar* p = &MemoryMarshal.GetReference(result.AppendSpan(5))) - { - Number.WriteTwoDigits((uint)offset.Hours, p); - p[2] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)offset.Minutes, p + 3); - } + Span offsetSpan = result.AppendSpan(5); + Number.WriteTwoDigits((uint)offset.Hours, offsetSpan.Slice(0, 2)); + offsetSpan[2] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)offset.Minutes, offsetSpan.Slice(3, 2)); } } // output the 'K' format, which is for round-tripping the data - private static unsafe void FormatCustomizedRoundripTimeZone(DateTime dateTime, TimeSpan offset, ref ValueListBuilder result) where TChar : unmanaged, IUtfChar + private static void FormatCustomizedRoundripTimeZone(DateTime dateTime, TimeSpan offset, ref ValueListBuilder result) where TChar : unmanaged, IUtfChar { // The objective of this format is to round trip the data in the type // For DateTime it should round-trip the Kind value and preserve the time zone. @@ -885,12 +884,10 @@ private static unsafe void FormatCustomizedRoundripTimeZone(DateTime date offset = offset.Negate(); } - fixed (TChar* hoursMinutes = &MemoryMarshal.GetReference(result.AppendSpan(5))) - { - Number.WriteTwoDigits((uint)offset.Hours, hoursMinutes); - hoursMinutes[2] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)offset.Minutes, hoursMinutes + 3); - } + Span hoursMinutes = result.AppendSpan(5); + Number.WriteTwoDigits((uint)offset.Hours, hoursMinutes.Slice(0, 2)); + hoursMinutes[2] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)offset.Minutes, hoursMinutes.Slice(3, 2)); } internal static string ExpandStandardFormatToCustomPattern(char format, DateTimeFormatInfo dtfi) => @@ -917,7 +914,7 @@ internal static string ExpandStandardFormatToCustomPattern(char format, DateTime internal static string Format(DateTime dateTime, string? format, IFormatProvider? provider) => Format(dateTime, format, provider, new TimeSpan(NullOffset)); - internal static unsafe string Format(DateTime dateTime, string? format, IFormatProvider? provider, TimeSpan offset) + internal static string Format(DateTime dateTime, string? format, IFormatProvider? provider, TimeSpan offset) { DateTimeFormatInfo dtfi; @@ -1141,22 +1138,7 @@ internal static bool TryFormat(DateTime dateTime, Span destination var vlb = new ValueListBuilder(destination); FormatCustomized(dateTime, format, dtfi, offset, ref vlb); - bool success = Unsafe.AreSame(ref MemoryMarshal.GetReference(destination), ref MemoryMarshal.GetReference(vlb.AsSpan())); - if (success) - { - // The reference inside of the builder is still the destination. That means the builder didn't need to grow to beyond - // the space in the destination, which means the formatting operation was successful and fully wrote the data to - // the destination. All we need to do now is store how much was written. - charsWritten = vlb.Length; - } - else - { - // The reference inside of the builder is no longer the destination. That means the builder needed to grow beyond - // the builder. However, it's possible it grew unnecessarily, e.g. when formatting a fraction it might grow but then - // realize it didn't need to write any data and remove a preceding period. As such, we need to try to copy the data - // just in case it does actually fit. - success = vlb.TryCopyTo(destination, out charsWritten); - } + bool success = vlb.TryCopyTo(destination, out charsWritten); vlb.Dispose(); return success; } @@ -1342,7 +1324,7 @@ internal static bool IsValidCustomTimeOnlyFormat(ReadOnlySpan format, bool // 012345678901234567890123456789012 // --------------------------------- // 05:30:45.7680000 - internal static unsafe bool TryFormatTimeOnlyO(TimeOnly value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryFormatTimeOnlyO(TimeOnly value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { if (destination.Length < 16) { @@ -1353,16 +1335,13 @@ internal static unsafe bool TryFormatTimeOnlyO(TimeOnly value, Span(TimeOnly value, Span(TimeOnly value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryFormatTimeOnlyR(TimeOnly value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { if (destination.Length < 8) { @@ -1381,14 +1360,11 @@ internal static unsafe bool TryFormatTimeOnlyR(TimeOnly value, Span(TimeOnly value, Span(DateOnly value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryFormatDateOnlyO(DateOnly value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { if (destination.Length < 10) { @@ -1408,14 +1384,11 @@ internal static unsafe bool TryFormatDateOnlyO(DateOnly value, Span(DateOnly value, Span(DateOnly value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryFormatDateOnlyR(DateOnly value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { if (destination.Length < 16) { @@ -1441,23 +1414,20 @@ internal static unsafe bool TryFormatDateOnlyR(DateOnly value, Span(DateOnly value, Span(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryFormatO(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { int charsRequired = FormatOMinLength; DateTimeKind kind = DateTimeKind.Local; @@ -1500,45 +1470,47 @@ internal static unsafe bool TryFormatO(DateTime dateTime, TimeSpan offset dateTime.GetDate(out int year, out int month, out int day); - fixed (TChar* dest = &MemoryMarshal.GetReference(destination)) - { - Number.WriteFourDigits((uint)year, dest); - dest[4] = TChar.CastFrom('-'); - Number.WriteTwoDigits((uint)month, dest + 5); - dest[7] = TChar.CastFrom('-'); - Number.WriteTwoDigits((uint)day, dest + 8); - dest[10] = TChar.CastFrom('T'); - dateTime.GetTimePrecise(out int hour, out int minute, out int second, out int tick); - Number.WriteTwoDigits((uint)hour, dest + 11); - dest[13] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)minute, dest + 14); - dest[16] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)second, dest + 17); - dest[19] = TChar.CastFrom('.'); - Number.WriteDigits((uint)tick, dest + 20, 7); - - if (kind == DateTimeKind.Local) + // Slicing to the constant minimum length lets the JIT drop the bounds checks on + // every fixed-offset write below; charsRequired is only known to be >= this. + Span dest = destination.Slice(0, FormatOMinLength); + + Number.WriteFourDigits((uint)year, dest.Slice(0, 4)); + dest[4] = TChar.CastFrom('-'); + Number.WriteTwoDigits((uint)month, dest.Slice(5, 2)); + dest[7] = TChar.CastFrom('-'); + Number.WriteTwoDigits((uint)day, dest.Slice(8, 2)); + dest[10] = TChar.CastFrom('T'); + dateTime.GetTimePrecise(out int hour, out int minute, out int second, out int tick); + Number.WriteTwoDigits((uint)hour, dest.Slice(11, 2)); + dest[13] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)minute, dest.Slice(14, 2)); + dest[16] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)second, dest.Slice(17, 2)); + dest[19] = TChar.CastFrom('.'); + Number.WriteDigits((uint)tick, dest.Slice(20, 7)); + + if (kind == DateTimeKind.Local) + { + int offsetTotalMinutes = (int)(offset.Ticks / TimeSpan.TicksPerMinute); + + char sign = '+'; + if (offsetTotalMinutes < 0) { - int offsetTotalMinutes = (int)(offset.Ticks / TimeSpan.TicksPerMinute); - - char sign = '+'; - if (offsetTotalMinutes < 0) - { - sign = '-'; - offsetTotalMinutes = -offsetTotalMinutes; - } + sign = '-'; + offsetTotalMinutes = -offsetTotalMinutes; + } - (int offsetHours, int offsetMinutes) = Math.DivRem(offsetTotalMinutes, 60); + (int offsetHours, int offsetMinutes) = Math.DivRem(offsetTotalMinutes, 60); - dest[27] = TChar.CastFrom(sign); - Number.WriteTwoDigits((uint)offsetHours, dest + 28); - dest[30] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)offsetMinutes, dest + 31); - } - else if (kind == DateTimeKind.Utc) - { - dest[27] = TChar.CastFrom('Z'); - } + Span suffix = destination.Slice(FormatOMinLength, 6); + suffix[0] = TChar.CastFrom(sign); + Number.WriteTwoDigits((uint)offsetHours, suffix.Slice(1, 2)); + suffix[3] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)offsetMinutes, suffix.Slice(4, 2)); + } + else if (kind == DateTimeKind.Utc) + { + destination[FormatOMinLength] = TChar.CastFrom('Z'); } return true; @@ -1548,7 +1520,7 @@ internal static unsafe bool TryFormatO(DateTime dateTime, TimeSpan offset // 012345678901234567890123456789012 // --------------------------------- // 2017-06-12T05:30:45 - internal static unsafe bool TryFormatS(DateTime dateTime, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryFormatS(DateTime dateTime, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { if (destination.Length < FormatSLength) { @@ -1560,21 +1532,18 @@ internal static unsafe bool TryFormatS(DateTime dateTime, Span des dateTime.GetDate(out int year, out int month, out int day); - fixed (TChar* dest = &MemoryMarshal.GetReference(destination)) - { - Number.WriteFourDigits((uint)year, dest); - dest[4] = TChar.CastFrom('-'); - Number.WriteTwoDigits((uint)month, dest + 5); - dest[7] = TChar.CastFrom('-'); - Number.WriteTwoDigits((uint)day, dest + 8); - dest[10] = TChar.CastFrom('T'); - dateTime.GetTime(out int hour, out int minute, out int second); - Number.WriteTwoDigits((uint)hour, dest + 11); - dest[13] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)minute, dest + 14); - dest[16] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)second, dest + 17); - } + Number.WriteFourDigits((uint)year, destination.Slice(0, 4)); + destination[4] = TChar.CastFrom('-'); + Number.WriteTwoDigits((uint)month, destination.Slice(5, 2)); + destination[7] = TChar.CastFrom('-'); + Number.WriteTwoDigits((uint)day, destination.Slice(8, 2)); + destination[10] = TChar.CastFrom('T'); + dateTime.GetTime(out int hour, out int minute, out int second); + Number.WriteTwoDigits((uint)hour, destination.Slice(11, 2)); + destination[13] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)minute, destination.Slice(14, 2)); + destination[16] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)second, destination.Slice(17, 2)); return true; } @@ -1583,7 +1552,7 @@ internal static unsafe bool TryFormatS(DateTime dateTime, Span des // 012345678901234567890123456789012 // --------------------------------- // 2017-06-12 05:30:45Z - internal static unsafe bool TryFormatu(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryFormatu(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { if (destination.Length < FormatuLength) { @@ -1600,22 +1569,19 @@ internal static unsafe bool TryFormatu(DateTime dateTime, TimeSpan offset dateTime.GetDate(out int year, out int month, out int day); - fixed (TChar* dest = &MemoryMarshal.GetReference(destination)) - { - Number.WriteFourDigits((uint)year, dest); - dest[4] = TChar.CastFrom('-'); - Number.WriteTwoDigits((uint)month, dest + 5); - dest[7] = TChar.CastFrom('-'); - Number.WriteTwoDigits((uint)day, dest + 8); - dest[10] = TChar.CastFrom(' '); - dateTime.GetTime(out int hour, out int minute, out int second); - Number.WriteTwoDigits((uint)hour, dest + 11); - dest[13] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)minute, dest + 14); - dest[16] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)second, dest + 17); - dest[19] = TChar.CastFrom('Z'); - } + Number.WriteFourDigits((uint)year, destination.Slice(0, 4)); + destination[4] = TChar.CastFrom('-'); + Number.WriteTwoDigits((uint)month, destination.Slice(5, 2)); + destination[7] = TChar.CastFrom('-'); + Number.WriteTwoDigits((uint)day, destination.Slice(8, 2)); + destination[10] = TChar.CastFrom(' '); + dateTime.GetTime(out int hour, out int minute, out int second); + Number.WriteTwoDigits((uint)hour, destination.Slice(11, 2)); + destination[13] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)minute, destination.Slice(14, 2)); + destination[16] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)second, destination.Slice(17, 2)); + destination[19] = TChar.CastFrom('Z'); return true; } @@ -1624,7 +1590,7 @@ internal static unsafe bool TryFormatu(DateTime dateTime, TimeSpan offset // 01234567890123456789012345678 // ----------------------------- // Tue, 03 Jan 2017 08:08:05 GMT - internal static unsafe bool TryFormatR(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryFormatR(DateTime dateTime, TimeSpan offset, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { if (destination.Length < FormatRLength) { @@ -1648,34 +1614,31 @@ internal static unsafe bool TryFormatR(DateTime dateTime, TimeSpan offset string monthAbbrev = s_invariantAbbreviatedMonthNames[month - 1]; Debug.Assert(monthAbbrev.Length == 3); - fixed (TChar* dest = &MemoryMarshal.GetReference(destination)) - { - char c = dayAbbrev[2]; // remove bounds checks on remaining dayAbbrev accesses - dest[0] = TChar.CastFrom(dayAbbrev[0]); - dest[1] = TChar.CastFrom(dayAbbrev[1]); - dest[2] = TChar.CastFrom(c); - dest[3] = TChar.CastFrom(','); - dest[4] = TChar.CastFrom(' '); - Number.WriteTwoDigits((uint)day, dest + 5); - dest[7] = TChar.CastFrom(' '); - c = monthAbbrev[2]; // remove bounds checks on remaining monthAbbrev accesses - dest[8] = TChar.CastFrom(monthAbbrev[0]); - dest[9] = TChar.CastFrom(monthAbbrev[1]); - dest[10] = TChar.CastFrom(c); - dest[11] = TChar.CastFrom(' '); - Number.WriteFourDigits((uint)year, dest + 12); - dest[16] = TChar.CastFrom(' '); - dateTime.GetTime(out int hour, out int minute, out int second); - Number.WriteTwoDigits((uint)hour, dest + 17); - dest[19] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)minute, dest + 20); - dest[22] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)second, dest + 23); - dest[25] = TChar.CastFrom(' '); - dest[26] = TChar.CastFrom('G'); - dest[27] = TChar.CastFrom('M'); - dest[28] = TChar.CastFrom('T'); - } + char c = dayAbbrev[2]; // remove bounds checks on remaining dayAbbrev accesses + destination[0] = TChar.CastFrom(dayAbbrev[0]); + destination[1] = TChar.CastFrom(dayAbbrev[1]); + destination[2] = TChar.CastFrom(c); + destination[3] = TChar.CastFrom(','); + destination[4] = TChar.CastFrom(' '); + Number.WriteTwoDigits((uint)day, destination.Slice(5, 2)); + destination[7] = TChar.CastFrom(' '); + c = monthAbbrev[2]; // remove bounds checks on remaining monthAbbrev accesses + destination[8] = TChar.CastFrom(monthAbbrev[0]); + destination[9] = TChar.CastFrom(monthAbbrev[1]); + destination[10] = TChar.CastFrom(c); + destination[11] = TChar.CastFrom(' '); + Number.WriteFourDigits((uint)year, destination.Slice(12, 4)); + destination[16] = TChar.CastFrom(' '); + dateTime.GetTime(out int hour, out int minute, out int second); + Number.WriteTwoDigits((uint)hour, destination.Slice(17, 2)); + destination[19] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)minute, destination.Slice(20, 2)); + destination[22] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)second, destination.Slice(23, 2)); + destination[25] = TChar.CastFrom(' '); + destination[26] = TChar.CastFrom('G'); + destination[27] = TChar.CastFrom('M'); + destination[28] = TChar.CastFrom('T'); return true; } @@ -1689,7 +1652,7 @@ internal static unsafe bool TryFormatR(DateTime dateTime, TimeSpan offset // 01234567890123456789012345 // -------------------------- // 05/25/2017 10:30:15 -08:00 - internal static unsafe bool TryFormatInvariantG(DateTime value, TimeSpan offset, Span destination, out int bytesWritten) where TChar : unmanaged, IUtfChar + internal static bool TryFormatInvariantG(DateTime value, TimeSpan offset, Span destination, out int bytesWritten) where TChar : unmanaged, IUtfChar { int bytesRequired = FormatInvariantGMinLength; if (offset.Ticks != NullOffset) @@ -1707,39 +1670,36 @@ internal static unsafe bool TryFormatInvariantG(DateTime value, TimeSpan value.GetDate(out int year, out int month, out int day); - fixed (TChar* dest = &MemoryMarshal.GetReference(destination)) + Number.WriteTwoDigits((uint)month, destination.Slice(0, 2)); + destination[2] = TChar.CastFrom('/'); + Number.WriteTwoDigits((uint)day, destination.Slice(3, 2)); + destination[5] = TChar.CastFrom('/'); + Number.WriteFourDigits((uint)year, destination.Slice(6, 4)); + destination[10] = TChar.CastFrom(' '); + + value.GetTime(out int hour, out int minute, out int second); + Number.WriteTwoDigits((uint)hour, destination.Slice(11, 2)); + destination[13] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)minute, destination.Slice(14, 2)); + destination[16] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)second, destination.Slice(17, 2)); + + if (offset.Ticks != NullOffset) { - Number.WriteTwoDigits((uint)month, dest); - dest[2] = TChar.CastFrom('/'); - Number.WriteTwoDigits((uint)day, dest + 3); - dest[5] = TChar.CastFrom('/'); - Number.WriteFourDigits((uint)year, dest + 6); - dest[10] = TChar.CastFrom(' '); - - value.GetTime(out int hour, out int minute, out int second); - Number.WriteTwoDigits((uint)hour, dest + 11); - dest[13] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)minute, dest + 14); - dest[16] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)second, dest + 17); - - if (offset.Ticks != NullOffset) + int offsetMinutes = (int)(offset.Ticks / TimeSpan.TicksPerMinute); + TChar sign = TChar.CastFrom('+'); + if (offsetMinutes < 0) { - int offsetMinutes = (int)(offset.Ticks / TimeSpan.TicksPerMinute); - TChar sign = TChar.CastFrom('+'); - if (offsetMinutes < 0) - { - sign = TChar.CastFrom('-'); - offsetMinutes = -offsetMinutes; - } - (int offsetHours, offsetMinutes) = Math.DivRem(offsetMinutes, 60); - - dest[19] = TChar.CastFrom(' '); - dest[20] = sign; - Number.WriteTwoDigits((uint)offsetHours, dest + 21); - dest[23] = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)offsetMinutes, dest + 24); + sign = TChar.CastFrom('-'); + offsetMinutes = -offsetMinutes; } + (int offsetHours, offsetMinutes) = Math.DivRem(offsetMinutes, 60); + + destination[19] = TChar.CastFrom(' '); + destination[20] = sign; + Number.WriteTwoDigits((uint)offsetHours, destination.Slice(21, 2)); + destination[23] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)offsetMinutes, destination.Slice(24, 2)); } return true; diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs index 83725d79cefa44..4b91bd9abd19b7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs @@ -4089,7 +4089,7 @@ private static bool ParseJapaneseEraStart(ref __DTString str, DateTimeFormatInfo // Given a specified format character, parse and update the parsing result. // - private static unsafe bool ParseByFormat( + private static bool ParseByFormat( ref __DTString str, ref __DTString format, scoped ref ParsingInfo parseInfo, diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs index ce31b75f7f57d5..b041d759321ddb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text; namespace System.Globalization @@ -103,7 +102,7 @@ private static ReadOnlySpan GetUtf8Span(ref byte[]? utf8Cache, string valu { byte[] utf8 = utf8Cache ?? CreateUtf8Cache(ref utf8Cache, value); Debug.Assert(utf8.Length > 0); - return MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetArrayDataReference(utf8), utf8.Length - 1); + return utf8.AsSpan(0, utf8.Length - 1); } [MethodImpl(MethodImplOptions.NoInlining)] diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs index f109706a13958d..0bf087cea23dd4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs @@ -16,7 +16,7 @@ internal static class TimeSpanFormat internal static readonly FormatLiterals NegativeInvariantFormatLiterals = FormatLiterals.InitInvariant(isNegative: true); /// Main method called from TimeSpan.ToString. - internal static unsafe string Format(TimeSpan value, string? format, IFormatProvider? formatProvider) + internal static string Format(TimeSpan value, string? format, IFormatProvider? formatProvider) { if (string.IsNullOrEmpty(format)) { @@ -48,7 +48,7 @@ internal static unsafe string Format(TimeSpan value, string? format, IFormatProv } /// Main method called from TimeSpan.TryFormat. - internal static unsafe bool TryFormat(TimeSpan value, Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? formatProvider) where TChar : unmanaged, IUtfChar + internal static bool TryFormat(TimeSpan value, Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? formatProvider) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -81,14 +81,14 @@ internal static unsafe bool TryFormat(TimeSpan value, Span destina return result; } - internal static unsafe string FormatC(TimeSpan value) + internal static string FormatC(TimeSpan value) { Span destination = stackalloc char[26]; // large enough for any "c" TimeSpan TryFormatStandard(value, StandardFormat.C, null, destination, out int charsWritten); return new string(destination.Slice(0, charsWritten)); } - private static unsafe string FormatG(TimeSpan value, DateTimeFormatInfo dtfi, StandardFormat format) + private static string FormatG(TimeSpan value, DateTimeFormatInfo dtfi, StandardFormat format) { string decimalSeparator = dtfi.DecimalSeparator; int maxLength = checked(25 + decimalSeparator.Length); // large enough for any "g"/"G" TimeSpan @@ -106,7 +106,7 @@ internal enum StandardFormat g } - internal static unsafe bool TryFormatStandard(TimeSpan value, StandardFormat format, ReadOnlySpan decimalSeparator, Span destination, out int written) where TChar : unmanaged, IUtfChar + internal static bool TryFormatStandard(TimeSpan value, StandardFormat format, ReadOnlySpan decimalSeparator, Span destination, out int written) where TChar : unmanaged, IUtfChar { Debug.Assert(format == StandardFormat.C || format == StandardFormat.G || format == StandardFormat.g); @@ -229,66 +229,114 @@ internal static unsafe bool TryFormatStandard(TimeSpan value, StandardFor return false; } - fixed (TChar* dest = &MemoryMarshal.GetReference(destination)) + int pos = 0; + + // Write leading '-' if necessary + if (value.Ticks < 0) + { + destination[pos++] = TChar.CastFrom('-'); + } + + // Write day and separator, if necessary + if (dayDigits != 0) { - TChar* p = dest; + Number.WriteDigits(days, destination.Slice(pos, dayDigits)); + pos += dayDigits; + destination[pos++] = TChar.CastFrom(format == StandardFormat.C ? '.' : ':'); + } - // Write leading '-' if necessary - if (value.Ticks < 0) + // After writing the variable-length prefix into destination[0..pos), write the + // fixed "[h]h:mm:ss[.fraction]" suffix. We branch on hourDigits (1 or 2) and + // initialize suffixLen to the minimum length (8 or 7) before conditionally adding + // the fraction part, giving the JIT a concrete lower bound to hoist bounds checks + // out of the inner writes. + Debug.Assert(hourDigits == 1 || hourDigits == 2); + int suffixLen; + if (hourDigits == 2) + { + int decSepLen = 0; + suffixLen = 8; // hh:mm:ss + if (fractionDigits != 0) { - *p++ = TChar.CastFrom('-'); + decSepLen = format == StandardFormat.C ? 1 : decimalSeparator.Length; + suffixLen += decSepLen + fractionDigits; } - - // Write day and separator, if necessary - if (dayDigits != 0) + // Invariant: suffixLen >= 8 by construction; this check is unreachable but lets + // the JIT prove that all suffix writes at constant offsets 0..7 are in bounds. + if ((uint)suffixLen < 8u) { - Number.WriteDigits(days, p, dayDigits); - p += dayDigits; - *p++ = TChar.CastFrom(format == StandardFormat.C ? '.' : ':'); + ThrowHelper.ThrowArgumentOutOfRangeException(); } + Span suffix = destination.Slice(pos, suffixLen); + + Number.WriteTwoDigits(hours, suffix.Slice(0, 2)); + suffix[2] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)minutes, suffix.Slice(3, 2)); + suffix[5] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)seconds, suffix.Slice(6, 2)); + + if (fractionDigits != 0) + { + if (format == StandardFormat.C) + { + suffix[8] = TChar.CastFrom('.'); + } + else if (decSepLen == 1) + { + suffix[8] = decimalSeparator[0]; + } + else + { + decimalSeparator.CopyTo(suffix.Slice(8, decSepLen)); + } - // Write "[h]h:mm:ss - Debug.Assert(hourDigits == 1 || hourDigits == 2); - if (hourDigits == 2) + Number.WriteDigits(fraction, suffix.Slice(8 + decSepLen, fractionDigits)); + } + } + else + { + int decSepLen = 0; + suffixLen = 7; // h:mm:ss + if (fractionDigits != 0) { - Number.WriteTwoDigits(hours, p); - p += 2; + decSepLen = format == StandardFormat.C ? 1 : decimalSeparator.Length; + suffixLen += decSepLen + fractionDigits; } - else + // Invariant: suffixLen >= 7 by construction; this check is unreachable but lets + // the JIT prove that all suffix writes at constant offsets 0..6 are in bounds. + if ((uint)suffixLen < 7u) { - *p++ = TChar.CastFrom('0' + hours); + ThrowHelper.ThrowArgumentOutOfRangeException(); } - *p++ = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)minutes, p); - p += 2; - *p++ = TChar.CastFrom(':'); - Number.WriteTwoDigits((uint)seconds, p); - p += 2; - - // Write fraction and separator, if necessary + Span suffix = destination.Slice(pos, suffixLen); + + suffix[0] = TChar.CastFrom('0' + (int)hours); + suffix[1] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)minutes, suffix.Slice(2, 2)); + suffix[4] = TChar.CastFrom(':'); + Number.WriteTwoDigits((uint)seconds, suffix.Slice(5, 2)); + if (fractionDigits != 0) { if (format == StandardFormat.C) { - *p++ = TChar.CastFrom('.'); + suffix[7] = TChar.CastFrom('.'); } - else if (decimalSeparator.Length == 1) + else if (decSepLen == 1) { - *p++ = decimalSeparator[0]; + suffix[7] = decimalSeparator[0]; } else { - decimalSeparator.CopyTo(new Span(p, decimalSeparator.Length)); - p += decimalSeparator.Length; + decimalSeparator.CopyTo(suffix.Slice(7, decSepLen)); } - Number.WriteDigits(fraction, p, fractionDigits); - p += fractionDigits; + Number.WriteDigits(fraction, suffix.Slice(7 + decSepLen, fractionDigits)); } - - Debug.Assert(p - dest == requiredOutputLength); } + Debug.Assert(pos + suffixLen == requiredOutputLength); + written = requiredOutputLength; return true; } @@ -496,7 +544,7 @@ internal static FormatLiterals InitInvariant(bool isNegative) // the constants guaranteed to include DHMSF ordered greatest to least significant. // Once the data becomes more complex than this we will need to write a proper tokenizer for // parsing and formatting - internal unsafe void Init(ReadOnlySpan format, bool useInvariantFieldLengths) + internal void Init(ReadOnlySpan format, bool useInvariantFieldLengths) { dd = hh = mm = ss = ff = 0; _literals = new string[6]; diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.BigInteger.cs b/src/libraries/System.Private.CoreLib/src/System/Number.BigInteger.cs index 9b043a01c2842f..7416dc42f0e229 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.BigInteger.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.BigInteger.cs @@ -320,6 +320,11 @@ internal ref struct BigInteger private int _length; private BlocksBuffer _blocks; + // _blocks spans MaxBlockCount uints and only the first _length of them are ever read, so the + // out parameters below use Unsafe.SkipInit rather than `= default` to avoid zero-filling the + // whole buffer. Measured on the double.Parse slow path, `= default` costs 10-30% + // (267ns -> 340ns for 50 digits, 1456ns -> 1650ns for 408 digits). + public static void Add(scoped ref BigInteger lhs, scoped ref BigInteger rhs, out BigInteger result) { Unsafe.SkipInit(out result); @@ -512,7 +517,11 @@ public static void DivRem(scoped ref BigInteger lhs, scoped ref BigInteger rhs, else { int quoLength = lhsLength - rhsLength + 1; - SetValue(out rem, ref lhs); + + // rem is an out parameter and so is not scoped, which means it cannot be passed + // by ref alongside the scoped rhs below. Compute into a local and copy out at the + // end instead. This was only a warning while Number was an unsafe context. + SetValue(out BigInteger remValue, ref lhs); int remLength = lhsLength; // Executes the "grammar-school" algorithm for computing q = a / b. @@ -544,10 +553,10 @@ public static void DivRem(scoped ref BigInteger lhs, scoped ref BigInteger rhs, for (int i = lhsLength; i >= rhsLength; i--) { int n = i - rhsLength; - uint t = i < lhsLength ? rem._blocks[i] : 0; + uint t = i < lhsLength ? remValue._blocks[i] : 0; - ulong valHi = ((ulong)t << 32) | rem._blocks[i - 1]; - uint valLo = i > 1 ? rem._blocks[i - 2] : 0; + ulong valHi = ((ulong)t << 32) | remValue._blocks[i - 1]; + uint valLo = i > 1 ? remValue._blocks[i - 2] : 0; // We shifted the divisor, we shift the dividend too if (shiftLeft > 0) @@ -557,7 +566,7 @@ public static void DivRem(scoped ref BigInteger lhs, scoped ref BigInteger rhs, if (i > 2) { - valLo |= rem._blocks[i - 3] >> shiftRight; + valLo |= remValue._blocks[i - 3] >> shiftRight; } } @@ -584,14 +593,14 @@ public static void DivRem(scoped ref BigInteger lhs, scoped ref BigInteger rhs, // https://github.com/dotnet/roslyn/issues/64393 #pragma warning disable CS9080 // Now it's time to subtract our current quotient - uint carry = SubtractDivisor(ref rem, n, ref rhs, digit); + uint carry = SubtractDivisor(ref remValue, n, ref rhs, digit); if (carry != t) { Debug.Assert(carry == t + 1); // Our guess was still exactly one too high - carry = AddDivisor(ref rem, n, ref rhs); + carry = AddDivisor(ref remValue, n, ref rhs); digit--; Debug.Assert(carry == 1); @@ -624,7 +633,7 @@ public static void DivRem(scoped ref BigInteger lhs, scoped ref BigInteger rhs, for (int i = remLength - 1; i >= 0; i--) { - if (rem._blocks[i] == 0) + if (remValue._blocks[i] == 0) { remLength--; } @@ -635,7 +644,8 @@ public static void DivRem(scoped ref BigInteger lhs, scoped ref BigInteger rhs, } } - rem._length = remLength; + remValue._length = remLength; + SetValue(out rem, ref remValue); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs b/src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs index 033777187b62fc..56d01d531d6e05 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.DecimalIeee754.cs @@ -555,7 +555,7 @@ internal static TValue EncodeDecimalIeee754(TValue bidBits) TValue leadingDigit = decoded.Significand / scale; uint msd = uint.CreateTruncating(leadingDigit); - int exponentContinuationBits = (Unsafe.SizeOf() * 8) - 6 - TDecimal.NumberBitsSignificand; + int exponentContinuationBits = (sizeof(TValue) * 8) - 6 - TDecimal.NumberBitsSignificand; uint exponentHigh = biasedExponent >> exponentContinuationBits; uint exponentLow = biasedExponent & ((1u << exponentContinuationBits) - 1); @@ -598,7 +598,7 @@ internal static TValue DecodeDecimalIeee754(TValue dpdBits) return (dpdBits & (TDecimal.SignMask | TDecimal.SNaNMask)) | payload; } - int exponentContinuationBits = (Unsafe.SizeOf() * 8) - 6 - TDecimal.NumberBitsSignificand; + int exponentContinuationBits = (sizeof(TValue) * 8) - 6 - TDecimal.NumberBitsSignificand; uint combination = uint.CreateTruncating(dpdBits >> (TDecimal.NumberBitsSignificand + exponentContinuationBits)) & 0x1F; uint exponentLow = uint.CreateTruncating(dpdBits >> TDecimal.NumberBitsSignificand) & ((1u << exponentContinuationBits) - 1); diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs index 2541286461ee5c..289035f041befd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Buffers; +using System.Buffers.Binary; using System.Buffers.Text; using System.Collections.Generic; using System.Diagnostics; @@ -272,76 +273,36 @@ internal static partial class Number /// Lazily-populated cache of strings for uint values in the range [0, ). private static readonly string?[] s_smallNumberCache = new string[SmallNumberCacheLength]; - // Optimizations using "TwoDigits" inspired by: - // https://engineering.fb.com/2013/03/15/developer-tools/three-optimization-tips-for-c/ -#if MONO - // Workaround for a performance regression on Mono: https://github.com/dotnet/runtime/issues/111932 - private static readonly byte[] TwoDigitsCharsAsBytes = - MemoryMarshal.AsBytes("00010203040506070809" + - "10111213141516171819" + - "20212223242526272829" + - "30313233343536373839" + - "40414243444546474849" + - "50515253545556575859" + - "60616263646566676869" + - "70717273747576777879" + - "80818283848586878889" + - "90919293949596979899").ToArray(); - private static readonly byte[] TwoDigitsBytes = - ("00010203040506070809"u8 + - "10111213141516171819"u8 + - "20212223242526272829"u8 + - "30313233343536373839"u8 + - "40414243444546474849"u8 + - "50515253545556575859"u8 + - "60616263646566676869"u8 + - "70717273747576777879"u8 + - "80818283848586878889"u8 + - "90919293949596979899"u8).ToArray(); + // Keep the pair's alignment equal to char so every char span can be safely reinterpreted. + [StructLayout(LayoutKind.Sequential, Pack = sizeof(char))] + private readonly struct DigitPair + { + public readonly uint Value; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ref byte GetTwoDigitsBytesRef(bool useChars) => - ref MemoryMarshal.GetArrayDataReference(useChars ? TwoDigitsCharsAsBytes : TwoDigitsBytes); -#else - private static ReadOnlySpan TwoDigitsCharsAsBytes => - MemoryMarshal.AsBytes("00010203040506070809" + - "10111213141516171819" + - "20212223242526272829" + - "30313233343536373839" + - "40414243444546474849" + - "50515253545556575859" + - "60616263646566676869" + - "70717273747576777879" + - "80818283848586878889" + - "90919293949596979899"); - private static ReadOnlySpan TwoDigitsBytes => - "00010203040506070809"u8 + - "10111213141516171819"u8 + - "20212223242526272829"u8 + - "30313233343536373839"u8 + - "40414243444546474849"u8 + - "50515253545556575859"u8 + - "60616263646566676869"u8 + - "70717273747576777879"u8 + - "80818283848586878889"u8 + - "90919293949596979899"u8; + public DigitPair(uint value) => Value = value; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ref byte GetTwoDigitsBytesRef(bool useChars) => - ref MemoryMarshal.GetReference(useChars ? TwoDigitsCharsAsBytes : TwoDigitsBytes); -#endif + private static Span GetFreshStringSpan(string result) + { + // The string has its definitive length and does not become observable until every character is initialized. + return new Span(ref result.GetRawStringData(), result.Length); + } internal static string FormatDecimalIeee754(TValue value, string? format, NumberFormatInfo info) where TDecimal : unmanaged, IDecimalIeee754ParseAndFormatInfo where TValue : unmanaged, IBinaryInteger { var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); - string result = FormatDecimalIeee754(ref vlb, value, format, info) ?? vlb.AsSpan().ToString(); + NumberBuffer number = new NumberBuffer(NumberBufferKind.DecimalIeee754, stackalloc byte[TDecimal.BufferLength]); + string result = FormatDecimalIeee754(ref vlb, ref number, value, format, info) ?? vlb.AsSpan().ToString(); vlb.Dispose(); return result; } - private static unsafe string? FormatDecimalIeee754(ref ValueListBuilder vlb, TValue value, ReadOnlySpan format, NumberFormatInfo info) + // The number buffer is created by the caller so that it shares a scope with the value list builder; + // otherwise passing it on to the formatting helpers is a ref-safety error now that Number is not unsafe. + private static string? FormatDecimalIeee754(ref ValueListBuilder vlb, ref NumberBuffer number, TValue value, ReadOnlySpan format, NumberFormatInfo info) where TDecimal : unmanaged, IDecimalIeee754ParseAndFormatInfo where TValue : unmanaged, IBinaryInteger where TChar : unmanaged, IUtfChar @@ -375,9 +336,6 @@ internal static string FormatDecimalIeee754(TValue value, stri } char fmt = ParseFormatSpecifier(format, out int digits); - byte* pDigits = stackalloc byte[TDecimal.BufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.DecimalIeee754, pDigits, TDecimal.BufferLength); - DecimalIeee754ToNumber(value, ref number); if (fmt != 0) @@ -414,7 +372,8 @@ internal static bool TryFormatDecimalIeee754(TValue val Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); - string? s = FormatDecimalIeee754(ref vlb, value, format, info); + NumberBuffer number = new NumberBuffer(NumberBufferKind.DecimalIeee754, stackalloc byte[TDecimal.BufferLength]); + string? s = FormatDecimalIeee754(ref vlb, ref number, value, format, info); Debug.Assert(s is null || typeof(TChar) == typeof(char)); bool success = s != null ? @@ -435,7 +394,7 @@ internal static bool TryFormatDecimalIeee754(TValue val /// therefore required whenever the quantum exponent is positive, and is otherwise picked using the same /// compactness heuristic as the binary floating-point types. /// - private static unsafe void FormatGeneralAndRoundTripDecimalIeee754(ref ValueListBuilder vlb, ref NumberBuffer number, char expChar, int nMaxDigits, NumberFormatInfo info) + private static void FormatGeneralAndRoundTripDecimalIeee754(ref ValueListBuilder vlb, ref NumberBuffer number, char expChar, int nMaxDigits, NumberFormatInfo info) where TChar : unmanaged, IUtfChar { Debug.Assert(number.Kind == NumberBufferKind.DecimalIeee754); @@ -452,8 +411,8 @@ private static unsafe void FormatGeneralAndRoundTripDecimalIeee754(ref Va vlb.Append(info.NegativeSignTChar()); } - byte* dig = number.DigitsPtr; int digitCount = number.DigitsCount; + ReadOnlySpan dig = number.Digits.Slice(0, digitCount); // `Scale` is the coefficient digit count plus the quantum exponent, so `Scale` exceeding the number // of significant digits means the quantum exponent is positive. Rounding drops trailing coefficient @@ -515,17 +474,15 @@ private static unsafe void FormatGeneralAndRoundTripDecimalIeee754(ref Va } } - public static unsafe string FormatDecimal(decimal value, ReadOnlySpan format, NumberFormatInfo info) + public static string FormatDecimal(decimal value, ReadOnlySpan format, NumberFormatInfo info) { char fmt = ParseFormatSpecifier(format, out int digits); - byte* pDigits = stackalloc byte[DecimalNumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Decimal, pDigits, DecimalNumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Decimal, stackalloc byte[DecimalNumberBufferLength]); DecimalToNumber(ref value, ref number); - char* stackPtr = stackalloc char[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); if (fmt != 0) { @@ -541,19 +498,17 @@ public static unsafe string FormatDecimal(decimal value, ReadOnlySpan form return result; } - public static unsafe bool TryFormatDecimal(decimal value, ReadOnlySpan format, NumberFormatInfo info, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + public static bool TryFormatDecimal(decimal value, ReadOnlySpan format, NumberFormatInfo info, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); char fmt = ParseFormatSpecifier(format, out int digits); - byte* pDigits = stackalloc byte[DecimalNumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Decimal, pDigits, DecimalNumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Decimal, stackalloc byte[DecimalNumberBufferLength]); DecimalToNumber(ref value, ref number); - TChar* stackPtr = stackalloc TChar[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); if (fmt != 0) { @@ -603,31 +558,28 @@ internal static void DecimalIeee754ToNumber(TValue value, ref number.CheckConsistency(); } - internal static unsafe void DecimalToNumber(scoped ref decimal d, ref NumberBuffer number) + internal static void DecimalToNumber(scoped ref decimal d, ref NumberBuffer number) { - byte* buffer = number.DigitsPtr; - number.DigitsCount = DecimalPrecision; number.IsNegative = decimal.IsNegative(d); - byte* p = buffer + DecimalPrecision; - while ((d.Mid | d.High) != 0) - { - p = UInt32ToDecChars(p, decimal.DecDivMod1E9(ref d), 9); - } - p = UInt32ToDecChars(p, d.Low, 0); - - int i = (int)((buffer + DecimalPrecision) - p); + // Pre-compute the exact digit count from the 96-bit integer value so we can write + // directly into digits[0..i) without a subsequent shift. + UInt128 absValue = new UInt128((uint)d.High, ((ulong)(uint)d.Mid << 32) | (uint)d.Low); + int i = absValue != UInt128.Zero ? FormattingHelpers.CountDigits(absValue) : 0; + int scale = d.Scale; // capture before DecDivMod1E9 mutates d (it doesn't touch scale, but be explicit) number.DigitsCount = i; - number.Scale = i - d.Scale; + number.Scale = i - scale; - byte* dst = number.DigitsPtr; - while (--i >= 0) + Span digits = number.Digits; + int index = i; + while ((d.Mid | d.High) != 0) { - *dst++ = *p++; + index = UInt32ToDecChars(digits, index, decimal.DecDivMod1E9(ref d), 9); } - *dst = (byte)'\0'; + UInt32ToDecChars(digits, index, d.Low, 0); + digits[i] = (byte)'\0'; number.CheckConsistency(); } @@ -759,7 +711,7 @@ static int Slow(char fmt, ref int precision, NumberFormatInfo info, out bool isS } } - private static unsafe void FormatFloatingPointAsHex(ref ValueListBuilder vlb, TNumber value, char fmt, int precision, NumberFormatInfo info) + private static void FormatFloatingPointAsHex(ref ValueListBuilder vlb, TNumber value, char fmt, int precision, NumberFormatInfo info) where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo where TChar : unmanaged, IUtfChar { @@ -915,7 +867,6 @@ private static unsafe void FormatFloatingPointAsHex(ref ValueLis { // Default precision: emit significant hex digits, trimming trailing zeros. // Compute trailing zero nibbles from the nibble-aligned representation. - int trimmedDigits = 0; if (significandBits != 0) { // Align significand to nibble boundary (pad LSB so total bits = defaultHexDigits * 4), @@ -923,7 +874,7 @@ private static unsafe void FormatFloatingPointAsHex(ref ValueLis int paddingBits = defaultHexDigits * 4 - mantissaBits; ulong nibbleAligned = significandBits << paddingBits; int trailingZeroBits = BitOperations.TrailingZeroCount(nibbleAligned); - trimmedDigits = defaultHexDigits - (trailingZeroBits / 4); + int trimmedDigits = defaultHexDigits - (trailingZeroBits / 4); if (trimmedDigits > 0) { @@ -957,28 +908,30 @@ private static unsafe void FormatFloatingPointAsHex(ref ValueLis // Write exponent digits Debug.Assert(actualExponent >= 0); int digitCount = FormattingHelpers.CountDigits((uint)actualExponent); - TChar* pExponent = stackalloc TChar[digitCount]; - UInt32ToDecChars(pExponent + digitCount, (uint)actualExponent); - vlb.Append(new ReadOnlySpan(pExponent, digitCount)); + Span exponentBuffer = vlb.AppendSpan(digitCount); + int exponentPos = UInt32ToDecChars(exponentBuffer, digitCount, (uint)actualExponent); + Debug.Assert(exponentPos == 0); } - public static unsafe string FormatFloat(TNumber value, string? format, NumberFormatInfo info) + public static string FormatFloat(TNumber value, string? format, NumberFormatInfo info) where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo { var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); - string result = FormatFloat(ref vlb, value, format, info) ?? vlb.AsSpan().ToString(); + NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, stackalloc byte[TNumber.NumberBufferLength]); + string result = FormatFloat(ref vlb, ref number, value, format, info) ?? vlb.AsSpan().ToString(); vlb.Dispose(); return result; } - public static unsafe bool TryFormatFloat(TNumber value, ReadOnlySpan format, NumberFormatInfo info, Span destination, out int charsWritten) + public static bool TryFormatFloat(TNumber value, ReadOnlySpan format, NumberFormatInfo info, Span destination, out int charsWritten) where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); - string? s = FormatFloat(ref vlb, value, format, info); + NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, stackalloc byte[TNumber.NumberBufferLength]); + string? s = FormatFloat(ref vlb, ref number, value, format, info); Debug.Assert(s is null || typeof(TChar) == typeof(char)); bool success = s != null ? @@ -994,7 +947,7 @@ public static unsafe bool TryFormatFloat(TNumber value, ReadOnly /// Non-null if an existing string can be returned, in which case the builder will be unmodified. /// Null if no existing string was returned, in which case the formatted output is in the builder. /// - private static unsafe string? FormatFloat(ref ValueListBuilder vlb, TNumber value, ReadOnlySpan format, NumberFormatInfo info) + private static string? FormatFloat(ref ValueListBuilder vlb, ref NumberBuffer number, TNumber value, ReadOnlySpan format, NumberFormatInfo info) where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo where TChar : unmanaged, IUtfChar { @@ -1035,14 +988,11 @@ public static unsafe bool TryFormatFloat(TNumber value, ReadOnly return null; } - byte* pDigits = stackalloc byte[TNumber.NumberBufferLength]; - if (fmt == '\0') { precision = TNumber.MaxPrecisionCustomFormat; } - NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, pDigits, TNumber.NumberBufferLength); number.IsNegative = TNumber.IsNegative(value); // We need to track the original precision requested since some formats @@ -1102,10 +1052,10 @@ private static bool TryCopyTo(string source, Span destination, out charsWritten = 0; return false; } - else - { - return Encoding.UTF8.TryGetBytes(source, Unsafe.BitCast, Span>(destination), out charsWritten); - } + + Debug.Assert(typeof(TChar) == typeof(byte)); + + return Encoding.UTF8.TryGetBytes(source, Unsafe.BitCast, Span>(destination), out charsWritten); } internal static char GetHexBase(char fmt) @@ -1127,7 +1077,7 @@ public static string FormatInt32(int value, int hexMask, string? format, IFormat return FormatInt32Slow(value, hexMask, format, provider); - static unsafe string FormatInt32Slow(int value, int hexMask, string? format, IFormatProvider? provider) + static string FormatInt32Slow(int value, int hexMask, string? format, IFormatProvider? provider) { ReadOnlySpan formatSpan = format; char fmt = ParseFormatSpecifier(formatSpan, out int digits); @@ -1150,13 +1100,11 @@ static unsafe string FormatInt32Slow(int value, int hexMask, string? format, IFo { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[Int32NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, Int32NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int32NumberBufferLength]); Int32ToNumber(value, ref number); - char* stackPtr = stackalloc char[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); if (fmt != 0) { @@ -1187,7 +1135,7 @@ public static bool TryFormatInt32(int value, int hexMask, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) + static bool TryFormatInt32Slow(int value, int hexMask, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) { char fmt = ParseFormatSpecifier(format, out int digits); char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison @@ -1209,13 +1157,11 @@ static unsafe bool TryFormatInt32Slow(int value, int hexMask, ReadOnlySpan { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[Int32NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, Int32NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int32NumberBufferLength]); Int32ToNumber(value, ref number); - TChar* stackPtr = stackalloc TChar[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); if (fmt != 0) { @@ -1243,7 +1189,7 @@ public static string FormatUInt32(uint value, string? format, IFormatProvider? p return FormatUInt32Slow(value, format, provider); - static unsafe string FormatUInt32Slow(uint value, string? format, IFormatProvider? provider) + static string FormatUInt32Slow(uint value, string? format, IFormatProvider? provider) { ReadOnlySpan formatSpan = format; char fmt = ParseFormatSpecifier(formatSpan, out int digits); @@ -1264,13 +1210,11 @@ static unsafe string FormatUInt32Slow(uint value, string? format, IFormatProvide { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[UInt32NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, UInt32NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt32NumberBufferLength]); UInt32ToNumber(value, ref number); - char* stackPtr = stackalloc char[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); if (fmt != 0) { @@ -1301,7 +1245,7 @@ public static bool TryFormatUInt32(uint value, ReadOnlySpan format, return TryFormatUInt32Slow(value, format, provider, destination, out charsWritten); - static unsafe bool TryFormatUInt32Slow(uint value, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) + static bool TryFormatUInt32Slow(uint value, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) { char fmt = ParseFormatSpecifier(format, out int digits); char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison @@ -1321,13 +1265,11 @@ static unsafe bool TryFormatUInt32Slow(uint value, ReadOnlySpan format, IF { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[UInt32NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, UInt32NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt32NumberBufferLength]); UInt32ToNumber(value, ref number); - TChar* stackPtr = stackalloc TChar[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); if (fmt != 0) { @@ -1357,7 +1299,7 @@ public static string FormatInt64(long value, string? format, IFormatProvider? pr return FormatInt64Slow(value, format, provider); - static unsafe string FormatInt64Slow(long value, string? format, IFormatProvider? provider) + static string FormatInt64Slow(long value, string? format, IFormatProvider? provider) { ReadOnlySpan formatSpan = format; char fmt = ParseFormatSpecifier(formatSpan, out int digits); @@ -1380,13 +1322,11 @@ static unsafe string FormatInt64Slow(long value, string? format, IFormatProvider { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[Int64NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, Int64NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int64NumberBufferLength]); Int64ToNumber(value, ref number); - char* stackPtr = stackalloc char[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); if (fmt != 0) { @@ -1419,7 +1359,7 @@ public static bool TryFormatInt64(long value, ReadOnlySpan format, return TryFormatInt64Slow(value, format, provider, destination, out charsWritten); - static unsafe bool TryFormatInt64Slow(long value, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) + static bool TryFormatInt64Slow(long value, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) { char fmt = ParseFormatSpecifier(format, out int digits); char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison @@ -1441,13 +1381,11 @@ static unsafe bool TryFormatInt64Slow(long value, ReadOnlySpan format, IFo { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[Int64NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, Int64NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int64NumberBufferLength]); Int64ToNumber(value, ref number); - char* stackPtr = stackalloc char[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); if (fmt != 0) { @@ -1475,7 +1413,7 @@ public static string FormatUInt64(ulong value, string? format, IFormatProvider? return FormatUInt64Slow(value, format, provider); - static unsafe string FormatUInt64Slow(ulong value, string? format, IFormatProvider? provider) + static string FormatUInt64Slow(ulong value, string? format, IFormatProvider? provider) { ReadOnlySpan formatSpan = format; char fmt = ParseFormatSpecifier(formatSpan, out int digits); @@ -1496,13 +1434,11 @@ static unsafe string FormatUInt64Slow(ulong value, string? format, IFormatProvid { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[UInt64NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, UInt64NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt64NumberBufferLength]); UInt64ToNumber(value, ref number); - char* stackPtr = stackalloc char[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); if (fmt != 0) { @@ -1533,7 +1469,7 @@ public static bool TryFormatUInt64(ulong value, ReadOnlySpan format return TryFormatUInt64Slow(value, format, provider, destination, out charsWritten); - static unsafe bool TryFormatUInt64Slow(ulong value, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) + static bool TryFormatUInt64Slow(ulong value, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) { char fmt = ParseFormatSpecifier(format, out int digits); char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison @@ -1553,13 +1489,11 @@ static unsafe bool TryFormatUInt64Slow(ulong value, ReadOnlySpan format, I { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[UInt64NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, UInt64NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt64NumberBufferLength]); UInt64ToNumber(value, ref number); - TChar* stackPtr = stackalloc TChar[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); if (fmt != 0) { @@ -1589,7 +1523,7 @@ public static string FormatInt128(Int128 value, string? format, IFormatProvider? return FormatInt128Slow(value, format, provider); - static unsafe string FormatInt128Slow(Int128 value, string? format, IFormatProvider? provider) + static string FormatInt128Slow(Int128 value, string? format, IFormatProvider? provider) { ReadOnlySpan formatSpan = format; @@ -1614,13 +1548,11 @@ static unsafe string FormatInt128Slow(Int128 value, string? format, IFormatProvi { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[Int128NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, Int128NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int128NumberBufferLength]); Int128ToNumber(value, ref number); - char* stackPtr = stackalloc char[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); if (fmt != 0) { @@ -1652,7 +1584,7 @@ public static bool TryFormatInt128(Int128 value, ReadOnlySpan forma return TryFormatInt128Slow(value, format, provider, destination, out charsWritten); - static unsafe bool TryFormatInt128Slow(Int128 value, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) + static bool TryFormatInt128Slow(Int128 value, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) { char fmt = ParseFormatSpecifier(format, out int digits); char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison @@ -1675,13 +1607,11 @@ static unsafe bool TryFormatInt128Slow(Int128 value, ReadOnlySpan format, { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[Int128NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, Int128NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[Int128NumberBufferLength]); Int128ToNumber(value, ref number); - TChar* stackPtr = stackalloc TChar[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); if (fmt != 0) { @@ -1709,7 +1639,7 @@ public static string FormatUInt128(UInt128 value, string? format, IFormatProvide return FormatUInt128Slow(value, format, provider); - static unsafe string FormatUInt128Slow(UInt128 value, string? format, IFormatProvider? provider) + static string FormatUInt128Slow(UInt128 value, string? format, IFormatProvider? provider) { ReadOnlySpan formatSpan = format; @@ -1732,13 +1662,11 @@ static unsafe string FormatUInt128Slow(UInt128 value, string? format, IFormatPro { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[UInt128NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, UInt128NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt128NumberBufferLength]); UInt128ToNumber(value, ref number); - char* stackPtr = stackalloc char[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc char[CharStackBufferSize]); if (fmt != 0) { @@ -1768,7 +1696,7 @@ public static bool TryFormatUInt128(UInt128 value, ReadOnlySpan for return TryFormatUInt128Slow(value, format, provider, destination, out charsWritten); - static unsafe bool TryFormatUInt128Slow(UInt128 value, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) + static bool TryFormatUInt128Slow(UInt128 value, ReadOnlySpan format, IFormatProvider? provider, Span destination, out int charsWritten) { char fmt = ParseFormatSpecifier(format, out int digits); char fmtUpper = (char)(fmt & 0xFFDF); // ensure fmt is upper-cased for purposes of comparison @@ -1789,13 +1717,11 @@ static unsafe bool TryFormatUInt128Slow(UInt128 value, ReadOnlySpan format { NumberFormatInfo info = NumberFormatInfo.GetInstance(provider); - byte* pDigits = stackalloc byte[UInt128NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, pDigits, UInt128NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, stackalloc byte[UInt128NumberBufferLength]); UInt128ToNumber(value, ref number); - TChar* stackPtr = stackalloc TChar[CharStackBufferSize]; - var vlb = new ValueListBuilder(new Span(stackPtr, CharStackBufferSize)); + var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); if (fmt != 0) { @@ -1814,10 +1740,8 @@ static unsafe bool TryFormatUInt128Slow(UInt128 value, ReadOnlySpan format } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe void Int32ToNumber(int value, ref NumberBuffer number) + private static void Int32ToNumber(int value, ref NumberBuffer number) { - number.DigitsCount = Int32Precision; - if (value >= 0) { number.IsNegative = false; @@ -1828,20 +1752,14 @@ private static unsafe void Int32ToNumber(int value, ref NumberBuffer number) value = -value; } - byte* buffer = number.DigitsPtr; - byte* p = UInt32ToDecChars(buffer + Int32Precision, (uint)value, 0); - - int i = (int)(buffer + Int32Precision - p); - + // Pre-compute the exact digit count so we can write directly into digits[0..i) — no shift. + int i = value != 0 ? FormattingHelpers.CountDigits((uint)value) : 0; number.DigitsCount = i; number.Scale = i; - byte* dst = number.DigitsPtr; - while (--i >= 0) - { - *dst++ = *p++; - } - *dst = (byte)'\0'; + Span digits = number.Digits; + UInt32ToDecChars(digits, i, (uint)value, 0); + digits[i] = (byte)'\0'; number.CheckConsistency(); } @@ -1851,7 +1769,33 @@ public static string Int32ToDecStr(int value) => UInt32ToDecStr((uint)value) : NegativeInt32ToDecStr(value, -1, NumberFormatInfo.CurrentInfo.NegativeSign); - private static unsafe string NegativeInt32ToDecStr(int value, int digits, string sNegative) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void UInt32ToDecChars(uint value, Span buffer) + { + Debug.Assert(!buffer.IsEmpty); + + int leadingDigits = 2 - (buffer.Length & 1); + Span pairs = MemoryMarshal.Cast(buffer.Slice(leadingDigits)); + + for (int i = pairs.Length - 1; (uint)i < (uint)pairs.Length; i--) + { + (value, uint remainder) = Math.DivRem(value, 100); + pairs[i] = new DigitPair(GetTwoDigitsChars(remainder)); + } + + if (leadingDigits == 1) + { + Debug.Assert(value < 10); + buffer[0] = (char)(value + '0'); + } + else + { + Debug.Assert(value < 100); + WriteTwoDigits(value, buffer.Slice(0, 2)); + } + } + + private static string NegativeInt32ToDecStr(int value, int digits, string sNegative) { Debug.Assert(value < 0); @@ -1862,21 +1806,13 @@ private static unsafe string NegativeInt32ToDecStr(int value, int digits, string int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits((uint)(-value))) + sNegative.Length; string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = UInt32ToDecChars(buffer + bufferLength, (uint)(-value), digits); - Debug.Assert(p == buffer + sNegative.Length); - - for (int i = sNegative.Length - 1; i >= 0; i--) - { - *(--p) = sNegative[i]; - } - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt32ToDecChars((uint)(-value), buffer.Slice(sNegative.Length)); + CopyNegativeSign(sNegative, buffer); return result; } - internal static unsafe bool TryNegativeInt32ToDecStr(int value, int digits, ReadOnlySpan sNegative, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryNegativeInt32ToDecStr(int value, int digits, ReadOnlySpan sNegative, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(value < 0); @@ -1894,21 +1830,13 @@ internal static unsafe bool TryNegativeInt32ToDecStr(int value, int digit } charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = UInt32ToDecChars(buffer + bufferLength, (uint)(-value), digits); - Debug.Assert(p == buffer + sNegative.Length); - - for (int i = sNegative.Length - 1; i >= 0; i--) - { - *(--p) = sNegative[i]; - } - Debug.Assert(p == buffer); - } + int pos = UInt32ToDecChars(destination, bufferLength, (uint)(-value), digits); + Debug.Assert(pos == sNegative.Length); + CopyNegativeSign(sNegative, destination); return true; } - private static unsafe string Int32ToHexStr(int value, char hexBase, int digits) + private static string Int32ToHexStr(int value, char hexBase, int digits) { if (digits < 1) { @@ -1917,15 +1845,12 @@ private static unsafe string Int32ToHexStr(int value, char hexBase, int digits) int bufferLength = Math.Max(digits, FormattingHelpers.CountHexDigits((uint)value)); string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = Int32ToHexChars(buffer + bufferLength, (uint)value, hexBase, digits); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + Int32ToHexChars(buffer, (uint)value, hexBase); return result; } - internal static unsafe bool TryInt32ToHexStr(int value, char hexBase, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryInt32ToHexStr(int value, char hexBase, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -1942,29 +1867,24 @@ internal static unsafe bool TryInt32ToHexStr(int value, char hexBase, int } charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = Int32ToHexChars(buffer + bufferLength, (uint)value, hexBase, digits); - Debug.Assert(p == buffer); - } + Int32ToHexChars(destination.Slice(0, bufferLength), (uint)value, hexBase); return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe TChar* Int32ToHexChars(TChar* buffer, uint value, int hexBase, int digits) where TChar : unmanaged, IUtfChar + private static void Int32ToHexChars(Span buffer, uint value, int hexBase) where TChar : unmanaged, IUtfChar { - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - - while (--digits >= 0 || value != 0) + for (int i = buffer.Length - 1; (uint)i < (uint)buffer.Length; i--) { byte digit = (byte)(value & 0xF); - *(--buffer) = TChar.CastFrom(digit + (digit < 10 ? (byte)'0' : hexBase)); + buffer[i] = TChar.CastFrom(digit + (digit < 10 ? (byte)'0' : hexBase)); value >>= 4; } - return buffer; + + Debug.Assert(value == 0); } - private static unsafe string UInt32ToBinaryStr(uint value, int digits) + private static string UInt32ToBinaryStr(uint value, int digits) { if (digits < 1) { @@ -1973,15 +1893,12 @@ private static unsafe string UInt32ToBinaryStr(uint value, int digits) int bufferLength = Math.Max(digits, 32 - (int)uint.LeadingZeroCount(value)); string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = UInt32ToBinaryChars(buffer + bufferLength, value, digits); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt32ToBinaryChars(buffer, value); return result; } - private static unsafe bool TryUInt32ToBinaryStr(uint value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + private static bool TryUInt32ToBinaryStr(uint value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -1998,155 +1915,64 @@ private static unsafe bool TryUInt32ToBinaryStr(uint value, int digits, S } charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = UInt32ToBinaryChars(buffer + bufferLength, value, digits); - Debug.Assert(p == buffer); - } + UInt32ToBinaryChars(destination.Slice(0, bufferLength), value); return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe TChar* UInt32ToBinaryChars(TChar* buffer, uint value, int digits) where TChar : unmanaged, IUtfChar + private static void UInt32ToBinaryChars(Span buffer, uint value) where TChar : unmanaged, IUtfChar { - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - - while (--digits >= 0 || value != 0) + for (int i = buffer.Length - 1; (uint)i < (uint)buffer.Length; i--) { - *(--buffer) = TChar.CastFrom('0' + (byte)(value & 0x1)); + buffer[i] = TChar.CastFrom('0' + (byte)(value & 0x1)); value >>= 1; } - return buffer; + + Debug.Assert(value == 0); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe void UInt32ToNumber(uint value, ref NumberBuffer number) + internal static void UInt32ToNumber(uint value, ref NumberBuffer number) { - number.DigitsCount = UInt32Precision; number.IsNegative = false; - byte* buffer = number.DigitsPtr; - byte* p = UInt32ToDecChars(buffer + UInt32Precision, value, 0); - - int i = (int)(buffer + UInt32Precision - p); - + int i = value != 0 ? FormattingHelpers.CountDigits(value) : 0; number.DigitsCount = i; number.Scale = i; - byte* dst = number.DigitsPtr; - while (--i >= 0) - { - *dst++ = *p++; - } - *dst = (byte)'\0'; + Span digits = number.Digits; + UInt32ToDecChars(digits, i, value, 0); + digits[i] = (byte)'\0'; number.CheckConsistency(); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe void WriteTwoDigits(uint value, TChar* ptr) where TChar : unmanaged, IUtfChar - { - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - Debug.Assert(value <= 99); - - Unsafe.CopyBlockUnaligned( - ref *(byte*)ptr, - ref Unsafe.Add(ref GetTwoDigitsBytesRef(typeof(TChar) == typeof(char)), (uint)sizeof(TChar) * 2 * value), - (uint)sizeof(TChar) * 2); - } /// - /// Writes a value [ 0000 .. 9999 ] to the buffer starting at the specified offset. - /// This method performs best when the starting index is a constant literal. + /// Writes a value [ 0000 .. 9999 ] to the start of a pre-sliced 4-element span. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe void WriteFourDigits(uint value, TChar* ptr) where TChar : unmanaged, IUtfChar + internal static void WriteFourDigits(uint value, Span destination) where TChar : unmanaged, IUtfChar { - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - Debug.Assert(value <= 9999); - + Debug.Assert(destination.Length >= 4); (value, uint remainder) = Math.DivRem(value, 100); - - ref byte charsArray = ref GetTwoDigitsBytesRef(typeof(TChar) == typeof(char)); - - Unsafe.CopyBlockUnaligned( - ref *(byte*)ptr, - ref Unsafe.Add(ref charsArray, (uint)sizeof(TChar) * 2 * value), - (uint)sizeof(TChar) * 2); - - Unsafe.CopyBlockUnaligned( - ref *(byte*)(ptr + 2), - ref Unsafe.Add(ref charsArray, (uint)sizeof(TChar) * 2 * remainder), - (uint)sizeof(TChar) * 2); + WriteTwoDigits(value, destination.Slice(0, 2)); + WriteTwoDigits(remainder, destination.Slice(2, 2)); } + /// Writes exactly destination.Length digits for into . [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe void WriteDigits(uint value, TChar* ptr, int count) where TChar : unmanaged, IUtfChar + internal static void WriteDigits(uint value, Span destination) where TChar : unmanaged, IUtfChar { - TChar* cur; - for (cur = ptr + count - 1; cur > ptr; cur--) + int cur = destination.Length - 1; + while (cur > 0) { uint temp = '0' + value; value /= 10; - *cur = TChar.CastFrom(temp - (value * 10)); + destination[cur--] = TChar.CastFrom(temp - value * 10); } - Debug.Assert(value < 10); - Debug.Assert(cur == ptr); - *cur = TChar.CastFrom('0' + value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe TChar* UInt32ToDecChars(TChar* bufferEnd, uint value) where TChar : unmanaged, IUtfChar - { - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - - if (value >= 10) - { - // Handle all values >= 100 two-digits at a time so as to avoid expensive integer division operations. - while (value >= 100) - { - bufferEnd -= 2; - (value, uint remainder) = Math.DivRem(value, 100); - WriteTwoDigits(remainder, bufferEnd); - } - - // If there are two digits remaining, store them. - if (value >= 10) - { - bufferEnd -= 2; - WriteTwoDigits(value, bufferEnd); - return bufferEnd; - } - } - - // Otherwise, store the single digit remaining. - *(--bufferEnd) = TChar.CastFrom(value + '0'); - return bufferEnd; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe TChar* UInt32ToDecChars(TChar* bufferEnd, uint value, int digits) where TChar : unmanaged, IUtfChar - { - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); - - uint remainder; - while (value >= 100) - { - bufferEnd -= 2; - digits -= 2; - (value, remainder) = Math.DivRem(value, 100); - WriteTwoDigits(remainder, bufferEnd); - } - - while (value != 0 || digits > 0) - { - digits--; - (value, remainder) = Math.DivRem(value, 10); - *(--bufferEnd) = TChar.CastFrom(remainder + '0'); - } - - return bufferEnd; + destination[0] = TChar.CastFrom('0' + value); } internal static string UInt32ToDecStr(uint value) @@ -2170,37 +1996,28 @@ static string CreateAndCacheString(uint value) => s_smallNumberCache[value] = UInt32ToDecStr_NoSmallNumberCheck(value); } - private static unsafe string UInt32ToDecStr_NoSmallNumberCheck(uint value) + private static string UInt32ToDecStr_NoSmallNumberCheck(uint value) { int bufferLength = FormattingHelpers.CountDigits(value); - string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = buffer + bufferLength; - p = UInt32ToDecChars(p, value); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt32ToDecChars(value, buffer); return result; } - private static unsafe string UInt32ToDecStr(uint value, int digits) + private static string UInt32ToDecStr(uint value, int digits) { if (digits <= 1) return UInt32ToDecStr(value); int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value)); string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = buffer + bufferLength; - p = UInt32ToDecChars(p, value, digits); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt32ToDecChars(value, buffer); return result; } - internal static unsafe bool TryUInt32ToDecStr(uint value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryUInt32ToDecStr(uint value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2208,11 +2025,8 @@ internal static unsafe bool TryUInt32ToDecStr(uint value, Span des if (bufferLength <= destination.Length) { charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = UInt32ToDecChars(buffer + bufferLength, value); - Debug.Assert(p == buffer); - } + int pos = UInt32ToDecChars(destination, bufferLength, value); + Debug.Assert(pos == 0); return true; } @@ -2220,7 +2034,7 @@ internal static unsafe bool TryUInt32ToDecStr(uint value, Span des return false; } - internal static unsafe bool TryUInt32ToDecStr(uint value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryUInt32ToDecStr(uint value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2229,14 +2043,10 @@ internal static unsafe bool TryUInt32ToDecStr(uint value, int digits, Spa if (bufferLength <= destination.Length) { charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = buffer + bufferLength; - p = digits > countedDigits ? - UInt32ToDecChars(p, value, digits) : - UInt32ToDecChars(p, value); - Debug.Assert(p == buffer); - } + int pos = digits > countedDigits ? + UInt32ToDecChars(destination, bufferLength, value, digits) : + UInt32ToDecChars(destination, bufferLength, value); + Debug.Assert(pos == 0); return true; } @@ -2245,10 +2055,8 @@ internal static unsafe bool TryUInt32ToDecStr(uint value, int digits, Spa } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe void Int64ToNumber(long value, ref NumberBuffer number) + private static void Int64ToNumber(long value, ref NumberBuffer number) { - number.DigitsCount = Int64Precision; - if (value >= 0) { number.IsNegative = false; @@ -2259,20 +2067,13 @@ private static unsafe void Int64ToNumber(long value, ref NumberBuffer number) value = -value; } - byte* buffer = number.DigitsPtr; - byte* p = UInt64ToDecChars(buffer + Int64Precision, (ulong)value, 0); - - int i = (int)(buffer + Int64Precision - p); - + int i = value != 0 ? FormattingHelpers.CountDigits((ulong)value) : 0; number.DigitsCount = i; number.Scale = i; - byte* dst = number.DigitsPtr; - while (--i >= 0) - { - *dst++ = *p++; - } - *dst = (byte)'\0'; + Span digits = number.Digits; + UInt64ToDecChars(digits, i, (ulong)value, 0); + digits[i] = (byte)'\0'; number.CheckConsistency(); } @@ -2284,7 +2085,7 @@ public static string Int64ToDecStr(long value) NegativeInt64ToDecStr(value, -1, NumberFormatInfo.CurrentInfo.NegativeSign); } - private static unsafe string NegativeInt64ToDecStr(long value, int digits, string sNegative) + private static string NegativeInt64ToDecStr(long value, int digits, string sNegative) { Debug.Assert(value < 0); @@ -2295,21 +2096,13 @@ private static unsafe string NegativeInt64ToDecStr(long value, int digits, strin int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits((ulong)(-value))) + sNegative.Length; string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = UInt64ToDecChars(buffer + bufferLength, (ulong)(-value), digits); - Debug.Assert(p == buffer + sNegative.Length); - - for (int i = sNegative.Length - 1; i >= 0; i--) - { - *(--p) = sNegative[i]; - } - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt64ToDecChars((ulong)(-value), buffer.Slice(sNegative.Length)); + CopyNegativeSign(sNegative, buffer); return result; } - internal static unsafe bool TryNegativeInt64ToDecStr(long value, int digits, ReadOnlySpan sNegative, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryNegativeInt64ToDecStr(long value, int digits, ReadOnlySpan sNegative, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(value < 0); @@ -2327,21 +2120,13 @@ internal static unsafe bool TryNegativeInt64ToDecStr(long value, int digi } charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = UInt64ToDecChars(buffer + bufferLength, (ulong)(-value), digits); - Debug.Assert(p == buffer + sNegative.Length); - - for (int i = sNegative.Length - 1; i >= 0; i--) - { - *(--p) = sNegative[i]; - } - Debug.Assert(p == buffer); - } + int pos = UInt64ToDecChars(destination, bufferLength, (ulong)(-value), digits); + Debug.Assert(pos == sNegative.Length); + CopyNegativeSign(sNegative, destination); return true; } - private static unsafe string Int64ToHexStr(long value, char hexBase, int digits) + private static string Int64ToHexStr(long value, char hexBase, int digits) { if (digits < 1) { @@ -2350,15 +2135,12 @@ private static unsafe string Int64ToHexStr(long value, char hexBase, int digits) int bufferLength = Math.Max(digits, FormattingHelpers.CountHexDigits((ulong)value)); string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = Int64ToHexChars(buffer + bufferLength, (ulong)value, hexBase, digits); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + Int64ToHexChars(buffer, (ulong)value, hexBase); return result; } - internal static unsafe bool TryInt64ToHexStr(long value, char hexBase, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryInt64ToHexStr(long value, char hexBase, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2375,45 +2157,40 @@ internal static unsafe bool TryInt64ToHexStr(long value, char hexBase, in } charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = Int64ToHexChars(buffer + bufferLength, (ulong)value, hexBase, digits); - Debug.Assert(p == buffer); - } + Int64ToHexChars(destination.Slice(0, bufferLength), (ulong)value, hexBase); return true; } #if TARGET_64BIT [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe TChar* Int64ToHexChars(TChar* buffer, ulong value, int hexBase, int digits) where TChar : unmanaged, IUtfChar + private static void Int64ToHexChars(Span buffer, ulong value, int hexBase) where TChar : unmanaged, IUtfChar { - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); #if TARGET_32BIT - uint lower = (uint)value; - uint upper = (uint)(value >> 32); - - if (upper != 0) + if (buffer.Length > 8) { - buffer = Int32ToHexChars(buffer, lower, hexBase, 8); - return Int32ToHexChars(buffer, upper, hexBase, digits - 8); + int upperLength = buffer.Length - 8; + Int32ToHexChars(buffer.Slice(upperLength), (uint)value, hexBase); + Int32ToHexChars(buffer.Slice(0, upperLength), (uint)(value >> 32), hexBase); } else { - return Int32ToHexChars(buffer, lower, hexBase, Math.Max(digits, 1)); + Debug.Assert((uint)(value >> 32) == 0); + Int32ToHexChars(buffer, (uint)value, hexBase); } #else - while (--digits >= 0 || value != 0) + for (int i = buffer.Length - 1; (uint)i < (uint)buffer.Length; i--) { byte digit = (byte)(value & 0xF); - *(--buffer) = TChar.CastFrom(digit + (digit < 10 ? (byte)'0' : hexBase)); + buffer[i] = TChar.CastFrom(digit + (digit < 10 ? (byte)'0' : hexBase)); value >>= 4; } - return buffer; + + Debug.Assert(value == 0); #endif } - private static unsafe string UInt64ToBinaryStr(ulong value, int digits) + private static string UInt64ToBinaryStr(ulong value, int digits) { if (digits < 1) { @@ -2422,15 +2199,12 @@ private static unsafe string UInt64ToBinaryStr(ulong value, int digits) int bufferLength = Math.Max(digits, 64 - (int)ulong.LeadingZeroCount(value)); string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = UInt64ToBinaryChars(buffer + bufferLength, value, digits); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt64ToBinaryChars(buffer, value); return result; } - private static unsafe bool TryUInt64ToBinaryStr(ulong value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + private static bool TryUInt64ToBinaryStr(ulong value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2447,63 +2221,50 @@ private static unsafe bool TryUInt64ToBinaryStr(ulong value, int digits, } charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = UInt64ToBinaryChars(buffer + bufferLength, value, digits); - Debug.Assert(p == buffer); - } + UInt64ToBinaryChars(destination.Slice(0, bufferLength), value); return true; } #if TARGET_64BIT [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - private static unsafe TChar* UInt64ToBinaryChars(TChar* buffer, ulong value, int digits) where TChar : unmanaged, IUtfChar + private static void UInt64ToBinaryChars(Span buffer, ulong value) where TChar : unmanaged, IUtfChar { - Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); #if TARGET_32BIT - uint lower = (uint)value; - uint upper = (uint)(value >> 32); - - if (upper != 0) + if (buffer.Length > 32) { - buffer = UInt32ToBinaryChars(buffer, lower, 32); - return UInt32ToBinaryChars(buffer, upper, digits - 32); + int upperLength = buffer.Length - 32; + UInt32ToBinaryChars(buffer.Slice(upperLength), (uint)value); + UInt32ToBinaryChars(buffer.Slice(0, upperLength), (uint)(value >> 32)); } else { - return UInt32ToBinaryChars(buffer, lower, Math.Max(digits, 1)); + Debug.Assert((uint)(value >> 32) == 0); + UInt32ToBinaryChars(buffer, (uint)value); } #else - while (--digits >= 0 || value != 0) + for (int i = buffer.Length - 1; (uint)i < (uint)buffer.Length; i--) { - *(--buffer) = TChar.CastFrom('0' + (byte)(value & 0x1)); + buffer[i] = TChar.CastFrom('0' + (byte)(value & 0x1)); value >>= 1; } - return buffer; + + Debug.Assert(value == 0); #endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe void UInt64ToNumber(ulong value, ref NumberBuffer number) + internal static void UInt64ToNumber(ulong value, ref NumberBuffer number) { - number.DigitsCount = UInt64Precision; number.IsNegative = false; - byte* buffer = number.DigitsPtr; - byte* p = UInt64ToDecChars(buffer + UInt64Precision, value, 0); - - int i = (int)(buffer + UInt64Precision - p); - + int i = value != 0 ? FormattingHelpers.CountDigits(value) : 0; number.DigitsCount = i; number.Scale = i; - byte* dst = number.DigitsPtr; - while (--i >= 0) - { - *dst++ = *p++; - } - *dst = (byte)'\0'; + Span digits = number.Digits; + UInt64ToDecChars(digits, i, value, 0); + digits[i] = (byte)'\0'; number.CheckConsistency(); } @@ -2519,78 +2280,110 @@ private static uint Int64DivMod1E9(ref ulong value) #if TARGET_64BIT [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - internal static unsafe TChar* UInt64ToDecChars(TChar* bufferEnd, ulong value) where TChar : unmanaged, IUtfChar + private static void UInt64ToDecChars(ulong value, Span buffer) + { + Debug.Assert(!buffer.IsEmpty); + +#if TARGET_32BIT + while ((uint)(value >> 32) != 0) + { + Debug.Assert(buffer.Length > 9); + int index = buffer.Length - 9; + UInt32ToDecChars(Int64DivMod1E9(ref value), buffer.Slice(index)); + buffer = buffer.Slice(0, index); + } + UInt32ToDecChars((uint)value, buffer); +#else + int leadingDigits = 2 - (buffer.Length & 1); + Span pairs = MemoryMarshal.Cast(buffer.Slice(leadingDigits)); + + for (int i = pairs.Length - 1; (uint)i < (uint)pairs.Length; i--) + { + (value, ulong remainder) = Math.DivRem(value, 100); + pairs[i] = new DigitPair(GetTwoDigitsChars((uint)remainder)); + } + + if (leadingDigits == 1) + { + Debug.Assert(value < 10); + buffer[0] = (char)(value + '0'); + } + else + { + Debug.Assert(value < 100); + WriteTwoDigits((uint)value, buffer.Slice(0, 2)); + } +#endif + } + +#if TARGET_64BIT + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#endif + internal static int UInt64ToDecChars(Span buffer, int index, ulong value) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); #if TARGET_32BIT while ((uint)(value >> 32) != 0) { - bufferEnd = UInt32ToDecChars(bufferEnd, Int64DivMod1E9(ref value), 9); + index = UInt32ToDecChars(buffer, index, Int64DivMod1E9(ref value), 9); } - return UInt32ToDecChars(bufferEnd, (uint)value); + return UInt32ToDecChars(buffer, index, (uint)value); #else if (value >= 10) { - // Handle all values >= 100 two-digits at a time so as to avoid expensive integer division operations. while (value >= 100) { - bufferEnd -= 2; + index -= 2; (value, ulong remainder) = Math.DivRem(value, 100); - WriteTwoDigits((uint)remainder, bufferEnd); + WriteTwoDigits((uint)remainder, buffer, index); } - - // If there are two digits remaining, store them. if (value >= 10) { - bufferEnd -= 2; - WriteTwoDigits((uint)value, bufferEnd); - return bufferEnd; + index -= 2; + WriteTwoDigits((uint)value, buffer, index); + return index; } } - - // Otherwise, store the single digit remaining. - *(--bufferEnd) = TChar.CastFrom(value + '0'); - return bufferEnd; + buffer[--index] = TChar.CastFrom(value + '0'); + return index; #endif } #if TARGET_64BIT [MethodImpl(MethodImplOptions.AggressiveInlining)] #endif - internal static unsafe TChar* UInt64ToDecChars(TChar* bufferEnd, ulong value, int digits) where TChar : unmanaged, IUtfChar + internal static int UInt64ToDecChars(Span buffer, int index, ulong value, int digits) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); #if TARGET_32BIT while ((uint)(value >> 32) != 0) { - bufferEnd = UInt32ToDecChars(bufferEnd, Int64DivMod1E9(ref value), 9); + index = UInt32ToDecChars(buffer, index, Int64DivMod1E9(ref value), 9); digits -= 9; } - return UInt32ToDecChars(bufferEnd, (uint)value, digits); + return UInt32ToDecChars(buffer, index, (uint)value, digits); #else ulong remainder; while (value >= 100) { - bufferEnd -= 2; + index -= 2; digits -= 2; (value, remainder) = Math.DivRem(value, 100); - WriteTwoDigits((uint)remainder, bufferEnd); + WriteTwoDigits((uint)remainder, buffer, index); } - while (value != 0 || digits > 0) { digits--; (value, remainder) = Math.DivRem(value, 10); - *(--bufferEnd) = TChar.CastFrom(remainder + '0'); + buffer[--index] = TChar.CastFrom(remainder + '0'); } - - return bufferEnd; + return index; #endif } - internal static unsafe string UInt64ToDecStr(ulong value) + internal static string UInt64ToDecStr(ulong value) { // For small numbers, consult a lazily-populated cache. if (value < SmallNumberCacheLength) @@ -2599,18 +2392,13 @@ internal static unsafe string UInt64ToDecStr(ulong value) } int bufferLength = FormattingHelpers.CountDigits(value); - string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = buffer + bufferLength; - p = UInt64ToDecChars(p, value); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt64ToDecChars(value, buffer); return result; } - internal static unsafe string UInt64ToDecStr(ulong value, int digits) + internal static string UInt64ToDecStr(ulong value, int digits) { if (digits <= 1) { @@ -2619,16 +2407,12 @@ internal static unsafe string UInt64ToDecStr(ulong value, int digits) int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value)); string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = buffer + bufferLength; - p = UInt64ToDecChars(p, value, digits); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt64ToDecChars(value, buffer); return result; } - internal static unsafe bool TryUInt64ToDecStr(ulong value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryUInt64ToDecStr(ulong value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2636,12 +2420,8 @@ internal static unsafe bool TryUInt64ToDecStr(ulong value, Span de if (bufferLength <= destination.Length) { charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = buffer + bufferLength; - p = UInt64ToDecChars(p, value); - Debug.Assert(p == buffer); - } + int pos = UInt64ToDecChars(destination, bufferLength, value); + Debug.Assert(pos == 0); return true; } @@ -2649,21 +2429,17 @@ internal static unsafe bool TryUInt64ToDecStr(ulong value, Span de return false; } - internal static unsafe bool TryUInt64ToDecStr(ulong value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static bool TryUInt64ToDecStr(ulong value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { int countedDigits = FormattingHelpers.CountDigits(value); int bufferLength = Math.Max(digits, countedDigits); if (bufferLength <= destination.Length) { charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = buffer + bufferLength; - p = digits > countedDigits ? - UInt64ToDecChars(p, value, digits) : - UInt64ToDecChars(p, value); - Debug.Assert(p == buffer); - } + int pos = digits > countedDigits ? + UInt64ToDecChars(destination, bufferLength, value, digits) : + UInt64ToDecChars(destination, bufferLength, value); + Debug.Assert(pos == 0); return true; } @@ -2671,7 +2447,7 @@ internal static unsafe bool TryUInt64ToDecStr(ulong value, int digits, Sp return false; } - private static unsafe void Int128ToNumber(Int128 value, ref NumberBuffer number) + private static void Int128ToNumber(Int128 value, ref NumberBuffer number) { number.DigitsCount = Int128Precision; @@ -2685,20 +2461,19 @@ private static unsafe void Int128ToNumber(Int128 value, ref NumberBuffer number) value = -value; } - byte* buffer = number.DigitsPtr; - byte* p = UInt128ToDecChars(buffer + Int128Precision, (UInt128)value, 0); + Span digits = number.Digits; + int start = UInt128ToDecChars(digits, Int128Precision, (UInt128)value, 0); - int i = (int)(buffer + Int128Precision - p); + int i = Int128Precision - start; number.DigitsCount = i; number.Scale = i; - byte* dst = number.DigitsPtr; - while (--i >= 0) + if (start != 0) { - *dst++ = *p++; + digits.Slice(start, i).CopyTo(digits); } - *dst = (byte)'\0'; + digits[i] = (byte)'\0'; number.CheckConsistency(); } @@ -2710,7 +2485,7 @@ public static string Int128ToDecStr(Int128 value) : NegativeInt128ToDecStr(value, -1, NumberFormatInfo.CurrentInfo.NegativeSign); } - private static unsafe string NegativeInt128ToDecStr(Int128 value, int digits, string sNegative) + private static string NegativeInt128ToDecStr(Int128 value, int digits, string sNegative) { Debug.Assert(Int128.IsNegative(value)); @@ -2723,21 +2498,13 @@ private static unsafe string NegativeInt128ToDecStr(Int128 value, int digits, st int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(absValue)) + sNegative.Length; string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = UInt128ToDecChars(buffer + bufferLength, absValue, digits); - Debug.Assert(p == buffer + sNegative.Length); - - for (int i = sNegative.Length - 1; i >= 0; i--) - { - *(--p) = sNegative[i]; - } - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt128ToDecChars(absValue, buffer.Slice(sNegative.Length)); + CopyNegativeSign(sNegative, buffer); return result; } - private static unsafe bool TryNegativeInt128ToDecStr(Int128 value, int digits, ReadOnlySpan sNegative, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + private static bool TryNegativeInt128ToDecStr(Int128 value, int digits, ReadOnlySpan sNegative, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(Int128.IsNegative(value)); @@ -2757,21 +2524,13 @@ private static unsafe bool TryNegativeInt128ToDecStr(Int128 value, int di } charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = UInt128ToDecChars(buffer + bufferLength, absValue, digits); - Debug.Assert(p == buffer + sNegative.Length); - - for (int i = sNegative.Length - 1; i >= 0; i--) - { - *(--p) = sNegative[i]; - } - Debug.Assert(p == buffer); - } + int pos = UInt128ToDecChars(destination, bufferLength, absValue, digits); + Debug.Assert(pos == sNegative.Length); + CopyNegativeSign(sNegative, destination); return true; } - private static unsafe string Int128ToHexStr(Int128 value, char hexBase, int digits) + private static string Int128ToHexStr(Int128 value, char hexBase, int digits) { if (digits < 1) { @@ -2782,15 +2541,12 @@ private static unsafe string Int128ToHexStr(Int128 value, char hexBase, int digi int bufferLength = Math.Max(digits, FormattingHelpers.CountHexDigits(uValue)); string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = Int128ToHexChars(buffer + bufferLength, uValue, hexBase, digits); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + Int128ToHexChars(buffer, uValue, hexBase); return result; } - private static unsafe bool TryInt128ToHexStr(Int128 value, char hexBase, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + private static bool TryInt128ToHexStr(Int128 value, char hexBase, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2809,32 +2565,27 @@ private static unsafe bool TryInt128ToHexStr(Int128 value, char hexBase, } charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = Int128ToHexChars(buffer + bufferLength, uValue, hexBase, digits); - Debug.Assert(p == buffer); - } + Int128ToHexChars(destination.Slice(0, bufferLength), uValue, hexBase); return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe TChar* Int128ToHexChars(TChar* buffer, UInt128 value, int hexBase, int digits) where TChar : unmanaged, IUtfChar + private static void Int128ToHexChars(Span buffer, UInt128 value, int hexBase) where TChar : unmanaged, IUtfChar { - ulong lower = value.Lower; - ulong upper = value.Upper; - - if (upper != 0) + if (buffer.Length > 16) { - buffer = Int64ToHexChars(buffer, lower, hexBase, 16); - return Int64ToHexChars(buffer, upper, hexBase, digits - 16); + int upperLength = buffer.Length - 16; + Int64ToHexChars(buffer.Slice(upperLength), value.Lower, hexBase); + Int64ToHexChars(buffer.Slice(0, upperLength), value.Upper, hexBase); } else { - return Int64ToHexChars(buffer, lower, hexBase, Math.Max(digits, 1)); + Debug.Assert(value.Upper == 0); + Int64ToHexChars(buffer, value.Lower, hexBase); } } - private static unsafe string UInt128ToBinaryStr(Int128 value, int digits) + private static string UInt128ToBinaryStr(Int128 value, int digits) { if (digits < 1) { @@ -2845,15 +2596,12 @@ private static unsafe string UInt128ToBinaryStr(Int128 value, int digits) int bufferLength = Math.Max(digits, 128 - (int)UInt128.LeadingZeroCount((UInt128)value)); string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = UInt128ToBinaryChars(buffer + bufferLength, uValue, digits); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt128ToBinaryChars(buffer, uValue); return result; } - private static unsafe bool TryUInt128ToBinaryStr(Int128 value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + private static bool TryUInt128ToBinaryStr(Int128 value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2872,50 +2620,44 @@ private static unsafe bool TryUInt128ToBinaryStr(Int128 value, int digits } charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = UInt128ToBinaryChars(buffer + bufferLength, uValue, digits); - Debug.Assert(p == buffer); - } + UInt128ToBinaryChars(destination.Slice(0, bufferLength), uValue); return true; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe TChar* UInt128ToBinaryChars(TChar* buffer, UInt128 value, int digits) where TChar : unmanaged, IUtfChar + private static void UInt128ToBinaryChars(Span buffer, UInt128 value) where TChar : unmanaged, IUtfChar { - ulong lower = value.Lower; - ulong upper = value.Upper; - - if (upper != 0) + if (buffer.Length > 64) { - buffer = UInt64ToBinaryChars(buffer, lower, 64); - return UInt64ToBinaryChars(buffer, upper, digits - 64); + int upperLength = buffer.Length - 64; + UInt64ToBinaryChars(buffer.Slice(upperLength), value.Lower); + UInt64ToBinaryChars(buffer.Slice(0, upperLength), value.Upper); } else { - return UInt64ToBinaryChars(buffer, lower, Math.Max(digits, 1)); + Debug.Assert(value.Upper == 0); + UInt64ToBinaryChars(buffer, value.Lower); } } - internal static unsafe void UInt128ToNumber(UInt128 value, ref NumberBuffer number) + internal static void UInt128ToNumber(UInt128 value, ref NumberBuffer number) { number.DigitsCount = UInt128Precision; number.IsNegative = false; - byte* buffer = number.DigitsPtr; - byte* p = UInt128ToDecChars(buffer + UInt128Precision, value, 0); + Span digits = number.Digits; + int start = UInt128ToDecChars(digits, UInt128Precision, value, 0); - int i = (int)(buffer + UInt128Precision - p); + int i = UInt128Precision - start; number.DigitsCount = i; number.Scale = i; - byte* dst = number.DigitsPtr; - while (--i >= 0) + if (start != 0) { - *dst++ = *p++; + digits.Slice(start, i).CopyTo(digits); } - *dst = (byte)'\0'; + digits[i] = (byte)'\0'; number.CheckConsistency(); } @@ -2928,32 +2670,53 @@ private static ulong Int128DivMod1E19(ref UInt128 value) return remainder.Lower; } +#if TARGET_64BIT [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe TChar* UInt128ToDecChars(TChar* bufferEnd, UInt128 value) where TChar : unmanaged, IUtfChar +#endif + private static void UInt128ToDecChars(UInt128 value, Span buffer) + { + Debug.Assert(!buffer.IsEmpty); + +#if TARGET_32BIT + while (value.Upper != 0) +#else + while (buffer.Length > 19) +#endif + { + Debug.Assert(buffer.Length > 19); + int index = buffer.Length - 19; + UInt64ToDecChars(Int128DivMod1E19(ref value), buffer.Slice(index)); + buffer = buffer.Slice(0, index); + } + Debug.Assert(value.Upper == 0); + UInt64ToDecChars(value.Lower, buffer); + } + + internal static int UInt128ToDecChars(Span buffer, int index, UInt128 value) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); while (value.Upper != 0) { - bufferEnd = UInt64ToDecChars(bufferEnd, Int128DivMod1E19(ref value), 19); + index = UInt64ToDecChars(buffer, index, Int128DivMod1E19(ref value), 19); } - return UInt64ToDecChars(bufferEnd, value.Lower); + return UInt64ToDecChars(buffer, index, value.Lower); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static unsafe TChar* UInt128ToDecChars(TChar* bufferEnd, UInt128 value, int digits) where TChar : unmanaged, IUtfChar + internal static int UInt128ToDecChars(Span buffer, int index, UInt128 value, int digits) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); while (value.Upper != 0) { - bufferEnd = UInt64ToDecChars(bufferEnd, Int128DivMod1E19(ref value), 19); + index = UInt64ToDecChars(buffer, index, Int128DivMod1E19(ref value), 19); digits -= 19; } - return UInt64ToDecChars(bufferEnd, value.Lower, digits); + return UInt64ToDecChars(buffer, index, value.Lower, digits); } - internal static unsafe string UInt128ToDecStr(UInt128 value) + internal static string UInt128ToDecStr(UInt128 value) { if (value.Upper == 0) { @@ -2961,18 +2724,13 @@ internal static unsafe string UInt128ToDecStr(UInt128 value) } int bufferLength = FormattingHelpers.CountDigits(value); - string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = buffer + bufferLength; - p = UInt128ToDecChars(p, value); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt128ToDecChars(value, buffer); return result; } - internal static unsafe string UInt128ToDecStr(UInt128 value, int digits) + internal static string UInt128ToDecStr(UInt128 value, int digits) { if (digits <= 1) { @@ -2981,30 +2739,22 @@ internal static unsafe string UInt128ToDecStr(UInt128 value, int digits) int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value)); string result = string.FastAllocateString(bufferLength); - fixed (char* buffer = result) - { - char* p = buffer + bufferLength; - p = UInt128ToDecChars(p, value, digits); - Debug.Assert(p == buffer); - } + Span buffer = GetFreshStringSpan(result); + UInt128ToDecChars(value, buffer); return result; } - private static unsafe bool TryUInt128ToDecStr(UInt128 value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + private static bool TryUInt128ToDecStr(UInt128 value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { int countedDigits = FormattingHelpers.CountDigits(value); int bufferLength = Math.Max(digits, countedDigits); if (bufferLength <= destination.Length) { charsWritten = bufferLength; - fixed (TChar* buffer = &MemoryMarshal.GetReference(destination)) - { - TChar* p = buffer + bufferLength; - p = digits > countedDigits ? - UInt128ToDecChars(p, value, digits) : - UInt128ToDecChars(p, value); - Debug.Assert(p == buffer); - } + int pos = digits > countedDigits ? + UInt128ToDecChars(destination, bufferLength, value, digits) : + UInt128ToDecChars(destination, bufferLength, value); + Debug.Assert(pos == 0); return true; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.NumberToFloatingPointBits.cs b/src/libraries/System.Private.CoreLib/src/System/Number.NumberToFloatingPointBits.cs index c4dca930cb5d0d..4a2716545a17e7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.NumberToFloatingPointBits.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.NumberToFloatingPointBits.cs @@ -9,7 +9,7 @@ namespace System { - internal unsafe partial class Number + internal partial class Number { internal static ReadOnlySpan Pow10DoubleTable => [ @@ -701,19 +701,17 @@ internal static void AccumulateDecimalDigitsIntoBigInteger(scoped ref NumberBuff { BigInteger.SetZero(out result); - byte* src = number.DigitsPtr + firstIndex; - uint remaining = lastIndex - firstIndex; + ReadOnlySpan digits = number.Digits.Slice((int)firstIndex, (int)(lastIndex - firstIndex)); - while (remaining != 0) + while (!digits.IsEmpty) { - uint count = Math.Min(remaining, 9); - uint value = DigitsToUInt32(src, (int)(count)); + uint count = (uint)Math.Min(digits.Length, 9); + uint value = DigitsToUInt32(digits.Slice(0, (int)count)); result.MultiplyPow10(count); result.Add(value); - src += count; - remaining -= count; + digits = digits.Slice((int)count); } } @@ -891,48 +889,44 @@ private static ulong ConvertBigIntegerToFloatingPointBits(ref BigInteger } // get 32-bit integer from at most 9 digits - internal static uint DigitsToUInt32(byte* p, int count) + internal static uint DigitsToUInt32(ReadOnlySpan p) { - Debug.Assert((1 <= count) && (count <= 9)); + Debug.Assert((1 <= p.Length) && (p.Length <= 9)); - byte* end = (p + count); uint res = 0; // parse batches of 8 digits with SWAR - while (p <= end - 8) + while (p.Length >= 8) { res = (res * 100000000) + ParseEightDigitsUnrolled(p); - p += 8; + p = p.Slice(8); } - while (p != end) + foreach (byte b in p) { - res = (10 * res) + p[0] - '0'; - ++p; + res = (10 * res) + b - '0'; } return res; } // get 64-bit integer from at most 19 digits - internal static ulong DigitsToUInt64(byte* p, int count) + internal static ulong DigitsToUInt64(ReadOnlySpan p) { - Debug.Assert((1 <= count) && (count <= 19)); + Debug.Assert((1 <= p.Length) && (p.Length <= 19)); - byte* end = (p + count); ulong res = 0; // parse batches of 8 digits with SWAR - while (end - p >= 8) + while (p.Length >= 8) { res = (res * 100000000) + ParseEightDigitsUnrolled(p); - p += 8; + p = p.Slice(8); } - while (p != end) + foreach (byte b in p) { - res = (10 * res) + p[0] - '0'; - ++p; + res = (10 * res) + b - '0'; } return res; @@ -943,23 +937,16 @@ internal static ulong DigitsToUInt64(byte* p, int count) /// https://lemire.me/blog/2022/01/21/swar-explained-parsing-eight-digits/ /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static uint ParseEightDigitsUnrolled(byte* chars) + internal static uint ParseEightDigitsUnrolled(ReadOnlySpan chars) { - // let's take the following value (byte*) 12345678 and read it unaligned : - // we get a ulong value of 0x3837363534333231 + // take the first eight digits, e.g. "12345678", and read them as a + // little-endian ulong to get 0x3837363534333231: // 1. Subtract character '0' 0x30 for each byte to get 0x0807060504030201 // 2. Consider this sequence as bytes sequence : b8b7b6b5b4b3b2b1 // we need to transform it to b1b2b3b4b5b6b7b8 by computing : // 10000 * (100 * (10*b1+b2) + 10*b3+b4) + 100*(10*b5+b6) + 10*b7+b8 // this is achieved by masking and shifting values - ulong val = Unsafe.ReadUnaligned(chars); - - // With BigEndian system an endianness swap has to be performed - // before the following operations as if it has been read with LittleEndian system - if (!BitConverter.IsLittleEndian) - { - val = BinaryPrimitives.ReverseEndianness(val); - } + ulong val = BinaryPrimitives.ReadUInt64LittleEndian(chars); const ulong mask = 0x000000FF000000FF; const ulong mul1 = 0x000F424000000064; // 100 + (1000000ULL << 32) @@ -975,7 +962,7 @@ private static ulong NumberToFloatingPointBits(ref NumberBuffer number) { Debug.Assert(TFloat.DenormalMantissaBits <= FloatingPointMaxDenormalMantissaBits); - Debug.Assert(number.DigitsPtr[0] != '0'); + Debug.Assert(number.Digits[0] != '0'); Debug.Assert(number.Scale <= FloatingPointMaxExponent); Debug.Assert(number.Scale >= FloatingPointMinExponent); @@ -999,9 +986,7 @@ private static ulong NumberToFloatingPointBits(ref NumberBuffer number) // Above 19 digits, we rely on slow path if (totalDigits <= 19) { - byte* src = number.DigitsPtr; - - ulong mantissa = DigitsToUInt64(src, (int)(totalDigits)); + ulong mantissa = DigitsToUInt64(number.Digits.Slice(0, (int)(totalDigits))); int exponent = (int)(number.Scale - integerDigitsPresent - fractionalDigitsPresent); if (TryFloatingPointBitsFromMantissa(mantissa, exponent, out ulong bits)) diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs index 5ecc71f0a76a49..3bea595eb4285c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs @@ -178,10 +178,6 @@ internal interface IDecimalIeee754ParseAndFormatInfo internal static partial class Number { - private const int Int32Precision = 10; - private const int UInt32Precision = Int32Precision; - private const int Int64Precision = 19; - private const int UInt64Precision = 20; private const int Int128Precision = 39; private const int UInt128Precision = 39; @@ -190,7 +186,7 @@ internal static partial class Number private const int FloatingPointMaxDenormalMantissaBits = 52; - private static unsafe bool TryNumberBufferToBinaryInteger(ref NumberBuffer number, ref TInteger value) + private static bool TryNumberBufferToBinaryInteger(ref NumberBuffer number, ref TInteger value) where TInteger : unmanaged, IBinaryIntegerParseAndFormatInfo { number.CheckConsistency(); @@ -202,9 +198,9 @@ private static unsafe bool TryNumberBufferToBinaryInteger(ref NumberBu return false; } - byte* p = number.DigitsPtr; + ReadOnlySpan digits = number.Digits; + int pos = 0; - Debug.Assert(p != null); TInteger n = TInteger.Zero; while (--i >= 0) @@ -216,9 +212,12 @@ private static unsafe bool TryNumberBufferToBinaryInteger(ref NumberBu n = TInteger.MultiplyBy10(n); - if (*p != '\0') + byte digit = digits[pos]; + + if (digit != '\0') { - TInteger newN = n + TInteger.CreateTruncating(*p++ - '0'); + pos++; + TInteger newN = n + TInteger.CreateTruncating(digit - '0'); if (!TInteger.IsSigned && (newN < n)) { @@ -626,7 +625,7 @@ internal interface IHexOrBinaryParser public static bool IsValidChar(uint ch) => (ch - '0') <= 1; public static uint FromChar(uint ch) => ch - '0'; public static uint MaxDigitValue => 1; - public static unsafe int MaxDigitCount => sizeof(TInteger) * 8; + public static int MaxDigitCount => sizeof(TInteger) * 8; public static TInteger ShiftLeftForNextDigit(TInteger value) => value << 1; } @@ -837,14 +836,18 @@ internal static TDecimal ParseDecimalIeee754(ReadOnlySp return result; } - internal static unsafe bool TryNumberToDecimal(ref NumberBuffer number, ref decimal value) + internal static bool TryNumberToDecimal(ref NumberBuffer number, ref decimal value) { number.CheckConsistency(); - byte* p = number.DigitsPtr; + // Walk the digits as a span sliced to the digit count. Reading past the end yields + // the '\0' terminator, expressed here via the length so the JIT can prove each access + // in bounds and elide the checks. + ReadOnlySpan digits = number.Digits.Slice(0, number.DigitsCount); + int pos = 0; int e = number.Scale; bool sign = number.IsNegative; - uint c = *p; + uint c = PeekDigit(digits, pos); if (c == 0) { // To avoid risking an app-compat issue with pre 4.5 (where some app was illegally using Reflection to examine the internal scale bits), we'll only force @@ -862,7 +865,7 @@ internal static unsafe bool TryNumberToDecimal(ref NumberBuffer number, ref deci e--; low64 *= 10; low64 += c - '0'; - c = *++p; + c = PeekDigit(digits, ++pos); if (low64 >= ulong.MaxValue / 10) break; if (c == 0) @@ -894,7 +897,7 @@ internal static unsafe bool TryNumberToDecimal(ref NumberBuffer number, ref deci low64 += c; if (low64 < c) high++; - c = *++p; + c = PeekDigit(digits, ++pos); } e--; } @@ -903,7 +906,7 @@ internal static unsafe bool TryNumberToDecimal(ref NumberBuffer number, ref deci { if ((c == '5') && ((low64 & 1) == 0)) { - c = *++p; + c = PeekDigit(digits, ++pos); bool hasZeroTail = !number.HasNonZeroTail; @@ -918,7 +921,7 @@ internal static unsafe bool TryNumberToDecimal(ref NumberBuffer number, ref deci while ((c != 0) && hasZeroTail) { hasZeroTail &= c == '0'; - c = *++p; + c = PeekDigit(digits, ++pos); } // We should either be at the end of the stream or have a non-zero tail @@ -955,6 +958,11 @@ internal static unsafe bool TryNumberToDecimal(ref NumberBuffer number, ref deci value = new decimal((int)low64, (int)(low64 >> 32), (int)high, sign, (byte)-e); } return true; + + // Returns the digit at pos, or '\0' once the digits are exhausted. Guarding the read + // with the unsigned length compare lets the JIT elide the bounds check. + static uint PeekDigit(ReadOnlySpan digits, int pos) + => (uint)pos < (uint)digits.Length ? digits[pos] : (byte)'\0'; } internal static TFloat ParseFloat(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info) @@ -968,7 +976,7 @@ internal static TFloat ParseFloat(ReadOnlySpan value, Numb return result; } - internal static unsafe ParsingStatus TryParseDecimal(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info, out decimal result, out int elementsConsumed) + internal static ParsingStatus TryParseDecimal(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info, out decimal result, out int elementsConsumed) where TChar : unmanaged, IUtfChar { NumberBuffer number = new NumberBuffer(NumberBufferKind.Decimal, stackalloc byte[DecimalNumberBufferLength]); @@ -1103,17 +1111,15 @@ internal static bool SpanStartsWith(ReadOnlySpan span, ReadOnlySpa { if (typeof(TChar) == typeof(char)) { - ReadOnlySpan typedSpan = Unsafe.BitCast, ReadOnlySpan>(span); - ReadOnlySpan typedValue = Unsafe.BitCast, ReadOnlySpan>(value); - return typedSpan.StartsWith(typedValue, comparisonType); + return Unsafe.BitCast, ReadOnlySpan>(span) + .StartsWith(Unsafe.BitCast, ReadOnlySpan>(value), comparisonType); } else { Debug.Assert(typeof(TChar) == typeof(byte)); - ReadOnlySpan typedSpan = Unsafe.BitCast, ReadOnlySpan>(span); - ReadOnlySpan typedValue = Unsafe.BitCast, ReadOnlySpan>(value); - return typedSpan.StartsWithUtf8(typedValue, comparisonType); + return Unsafe.BitCast, ReadOnlySpan>(span) + .StartsWithUtf8(Unsafe.BitCast, ReadOnlySpan>(value), comparisonType); } } @@ -1122,17 +1128,15 @@ internal static bool SpanEqualsOrdinalIgnoreCase(ReadOnlySpan span { if (typeof(TChar) == typeof(char)) { - ReadOnlySpan typedSpan = Unsafe.BitCast, ReadOnlySpan>(span); - ReadOnlySpan typedValue = Unsafe.BitCast, ReadOnlySpan>(value); - return typedSpan.EqualsOrdinalIgnoreCase(typedValue); + return Unsafe.BitCast, ReadOnlySpan>(span) + .EqualsOrdinalIgnoreCase(Unsafe.BitCast, ReadOnlySpan>(value)); } else { Debug.Assert(typeof(TChar) == typeof(byte)); - ReadOnlySpan typedSpan = Unsafe.BitCast, ReadOnlySpan>(span); - ReadOnlySpan typedValue = Unsafe.BitCast, ReadOnlySpan>(value); - return typedSpan.EqualsOrdinalIgnoreCaseUtf8(typedValue); + return Unsafe.BitCast, ReadOnlySpan>(span) + .EqualsOrdinalIgnoreCaseUtf8(Unsafe.BitCast, ReadOnlySpan>(value)); } } @@ -1573,7 +1577,7 @@ internal static bool TryParseHexFloatingPoint(ReadOnlySpan return true; } - internal static unsafe bool TryParseFloat(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info, out TFloat result, out int elementsConsumed) + internal static bool TryParseFloat(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info, out TFloat result, out int elementsConsumed) where TChar : unmanaged, IUtfChar where TFloat : unmanaged, IBinaryFloatParseAndFormatInfo { diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Rounding.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Rounding.cs index c6efa31b6c5f29..3363e3e87c9780 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Rounding.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Rounding.cs @@ -203,7 +203,7 @@ private static bool TryRoundToDecimalDigitsViaInteger(float value, int digits, M // Computes `floor(scaled / 2^shift)` and how the discarded fraction `(scaled mod 2^shift) / 2^shift` // compares to the `1/2` midpoint, where `scaled` is the exact `mantissa * 10^digits`. Returns false // when the integer part would not be exactly representable so the caller can fall back. - private static unsafe bool TryGetFloorAndMidpoint(TUInt scaled, int shift, int integerBoundaryLog2, out ulong floor, out int midpointComparison, out bool hasRemainder) + private static bool TryGetFloorAndMidpoint(TUInt scaled, int shift, int integerBoundaryLog2, out ulong floor, out int midpointComparison, out bool hasRemainder) where TUInt : unmanaged, IBinaryInteger, IUnsignedNumber { int bitWidth = sizeof(TUInt) * 8; @@ -293,7 +293,7 @@ private static int CompareResidualToThreshold(TNumber s, TNumber e, TNu // The caller is responsible for handling values which cannot have a fractional portion at // the requested number of digits (namely non-finite values and values whose magnitude is at // or above the point where all representable values are integers). - public static unsafe TNumber RoundToDecimalDigits(TNumber value, int digits, MidpointRounding mode) + public static TNumber RoundToDecimalDigits(TNumber value, int digits, MidpointRounding mode) where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo { Debug.Assert(TNumber.IsFinite(value)); @@ -389,8 +389,7 @@ public static unsafe TNumber RoundToDecimalDigits(TNumber value, int di // materializing the decimal digits and letting the shared conversion perform the correctly // rounded decimal-to-binary step. - byte* pDigits = stackalloc byte[TNumber.NumberBufferLength]; - NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, pDigits, TNumber.NumberBufferLength); + NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, stackalloc byte[TNumber.NumberBufferLength]); number.IsNegative = isNegative; Span buffer = number.Digits; diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs index b91149c6148b97..ffaea33d2c0f8b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal128.cs @@ -1744,11 +1744,11 @@ static string IDecimalIeee754ParseAndFormatInfo.ToDecStr(UI return Number.UInt128ToDecStr(significand); } - static unsafe UInt128 IDecimalIeee754ParseAndFormatInfo.NumberToSignificand(ref Number.NumberBuffer number, int digits) + static UInt128 IDecimalIeee754ParseAndFormatInfo.NumberToSignificand(ref Number.NumberBuffer number, int digits) { if (digits <= 19) { - return Number.DigitsToUInt64(number.DigitsPtr, digits); + return Number.DigitsToUInt64(number.Digits.Slice(0, digits)); } else { diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs index b46866b0525ac3..985b87a2295cc9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal32.cs @@ -1735,9 +1735,9 @@ static string IDecimalIeee754ParseAndFormatInfo.ToDecStr(uint s return Number.UInt32ToDecStr(significand); } - static unsafe uint IDecimalIeee754ParseAndFormatInfo.NumberToSignificand(ref Number.NumberBuffer number, int digits) + static uint IDecimalIeee754ParseAndFormatInfo.NumberToSignificand(ref Number.NumberBuffer number, int digits) { - return Number.DigitsToUInt32(number.DigitsPtr, digits); + return Number.DigitsToUInt32(number.Digits.Slice(0, digits)); } static Decimal32 IDecimalIeee754ParseAndFormatInfo.Construct(uint value) => new Decimal32(value); diff --git a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs index 56fde15e2c7d9c..84854021b5e6d1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Numerics/Decimal64.cs @@ -1728,9 +1728,9 @@ static string IDecimalIeee754ParseAndFormatInfo.ToDecStr(ulong return Number.UInt64ToDecStr(significand); } - static unsafe ulong IDecimalIeee754ParseAndFormatInfo.NumberToSignificand(ref Number.NumberBuffer number, int digits) + static ulong IDecimalIeee754ParseAndFormatInfo.NumberToSignificand(ref Number.NumberBuffer number, int digits) { - return Number.DigitsToUInt64(number.DigitsPtr, digits); + return Number.DigitsToUInt64(number.Digits.Slice(0, digits)); } static Decimal64 IDecimalIeee754ParseAndFormatInfo.Construct(ulong value) => new Decimal64(value); diff --git a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs index 2c7b1c11539519..7eac564d111ec6 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs @@ -96,7 +96,7 @@ private static ParsingStatus TryParseBigIntegerCore(ReadOnlySpan v return TryParseBigIntegerNumber(value, style, info, out result, out elementsConsumed); } - internal static unsafe ParsingStatus TryParseBigIntegerNumber(ReadOnlySpan value, NumberStyles style, NumberFormatInfo info, out BigInteger result, out int elementsConsumed) + internal static ParsingStatus TryParseBigIntegerNumber(ReadOnlySpan value, NumberStyles style, NumberFormatInfo info, out BigInteger result, out int elementsConsumed) where TChar : unmanaged, IUtfChar { scoped Span buffer; @@ -119,20 +119,16 @@ internal static unsafe ParsingStatus TryParseBigIntegerNumber(ReadOnlySpa } ParsingStatus ret; + NumberBuffer number = new(NumberBufferKind.Integer, buffer); - fixed (byte* ptr = buffer) // NumberBuffer expects pinned span + if (!TryStringToNumber(value, style, ref number, info, out elementsConsumed)) { - NumberBuffer number = new(NumberBufferKind.Integer, buffer); - - if (!TryStringToNumber(value, style, ref number, info, out elementsConsumed)) - { - result = default; - ret = ParsingStatus.Failed; - } - else - { - ret = NumberToBigInteger(ref number, out result); - } + result = default; + ret = ParsingStatus.Failed; + } + else + { + ret = NumberToBigInteger(ref number, out result); } if (arrayFromPool != null) @@ -612,7 +608,7 @@ static nuint MultiplyAdd(Span bits, nuint multiplier, nuint addValue) } } - private static unsafe string? FormatBigIntegerToHex(bool targetSpan, BigInteger value, char format, int digits, NumberFormatInfo info, Span destination, out int charsWritten, out bool spanSuccess) + private static string? FormatBigIntegerToHex(bool targetSpan, BigInteger value, char format, int digits, NumberFormatInfo info, Span destination, out int charsWritten, out bool spanSuccess) where TChar : unmanaged, IUtfChar { Debug.Assert(format is 'x' or 'X'); @@ -698,7 +694,7 @@ static nuint MultiplyAdd(Span bits, nuint multiplier, nuint addValue) } } - private static unsafe string? FormatBigIntegerToBinary(bool targetSpan, BigInteger value, int digits, Span destination, out int charsWritten, out bool spanSuccess) + private static string? FormatBigIntegerToBinary(bool targetSpan, BigInteger value, int digits, Span destination, out int charsWritten, out bool spanSuccess) where TChar : unmanaged, IUtfChar { // Get the bytes that make up the BigInteger. @@ -813,7 +809,7 @@ internal static bool TryFormatBigInteger(BigInteger value, ReadOnlySpan(bool targetSpan, BigInteger value, string? formatString, ReadOnlySpan formatSpan, NumberFormatInfo info, Span destination, out int charsWritten, out bool spanSuccess) + private static string? FormatBigInteger(bool targetSpan, BigInteger value, string? formatString, ReadOnlySpan formatSpan, NumberFormatInfo info, Span destination, out int charsWritten, out bool spanSuccess) where TChar : unmanaged, IUtfChar { Debug.Assert(formatString == null || formatString.Length == formatSpan.Length); @@ -838,16 +834,7 @@ internal static bool TryFormatBigInteger(BigInteger value, ReadOnlySpan, Span>(destination), out charsWritten, formatSpan, info); - } - else - { - Debug.Assert(typeof(TChar) == typeof(Utf16Char)); - spanSuccess = value._sign.TryFormat(Unsafe.BitCast, Span>(destination), out charsWritten, formatSpan, info); - } - + spanSuccess = TryFormatInt32(value._sign, destination, formatSpan, info, out charsWritten); return null; } else @@ -902,11 +889,8 @@ internal static bool TryFormatBigInteger(BigInteger value, ReadOnlySpan(BigInteger value, ReadOnlySpan, ReadOnlySpan>(sNegative), - }; + digits = digits, + base1E9Value = base1E9Value, + sNegative = negativeSign, + }; - strResult = string.Create(strLength, state, static (span, state) => - { - state.sNegative.CopyTo(span); - fixed (char* ptr = &MemoryMarshal.GetReference(span)) - { - BigIntegerToDecChars((Utf16Char*)ptr + span.Length, state.base1E9Value, state.digits); - } - }); - } + strResult = string.Create(strLength, state, static (span, state) => + { + CopyNegativeSign(state.sNegative.AsSpan(), span); + BigIntegerToDecChars(Unsafe.BitCast, Span>(span.Slice(state.sNegative.Length)), state.base1E9Value, state.digits); + }); } } else @@ -947,52 +926,49 @@ internal static bool TryFormatBigInteger(BigInteger value, ReadOnlySpan numberBuffer = valueDigits + 1 <= CharStackBufferSize ? stackalloc byte[valueDigits + 1] : (numberBufferToReturn = ArrayPool.Shared.Rent(valueDigits + 1)); - fixed (byte* ptr = numberBuffer) // NumberBuffer expects pinned Digits - { - scoped NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, ptr, valueDigits + 1); - BigIntegerToDecChars((Utf8Char*)ptr + valueDigits, base1E9Value, valueDigits); - number.Digits[^1] = 0; - number.DigitsCount = valueDigits; - number.Scale = valueDigits; - number.IsNegative = value.Sign < 0; + scoped NumberBuffer number = new NumberBuffer(NumberBufferKind.Integer, numberBuffer.Slice(0, valueDigits + 1)); + BigIntegerToDecChars(Unsafe.BitCast, Span>(number.Digits.Slice(0, valueDigits)), base1E9Value, valueDigits); + number.Digits[^1] = 0; + number.DigitsCount = valueDigits; + number.Scale = valueDigits; + number.IsNegative = value.Sign < 0; - scoped var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); // arbitrary stack cut-off + scoped var vlb = new ValueListBuilder(stackalloc TChar[CharStackBufferSize]); // arbitrary stack cut-off - if (fmt != 0) - { - NumberToString(ref vlb, ref number, fmt, digits, info); - } - else - { - NumberToStringFormat(ref vlb, ref number, formatSpan, info); - } + if (fmt != 0) + { + NumberToString(ref vlb, ref number, fmt, digits, info); + } + else + { + NumberToStringFormat(ref vlb, ref number, formatSpan, info); + } + + if (targetSpan) + { + spanSuccess = vlb.TryCopyTo(destination, out charsWritten); + strResult = null; + } + else + { + charsWritten = 0; + spanSuccess = false; - if (targetSpan) + if (typeof(TChar) == typeof(Utf8Char)) { - spanSuccess = vlb.TryCopyTo(destination, out charsWritten); - strResult = null; + strResult = Utf8CharsToString(vlb.AsSpan()); } else { - charsWritten = 0; - spanSuccess = false; - - if (typeof(TChar) == typeof(Utf8Char)) - { - strResult = Encoding.UTF8.GetString(Unsafe.BitCast, ReadOnlySpan>(vlb.AsSpan())); - } - else - { - Debug.Assert(typeof(TChar) == typeof(Utf16Char)); - strResult = Unsafe.BitCast, ReadOnlySpan>(vlb.AsSpan()).ToString(); - } + Debug.Assert(typeof(TChar) == typeof(Utf16Char)); + strResult = Utf16CharsToString(vlb.AsSpan()); } + } - vlb.Dispose(); - if (numberBufferToReturn != null) - { - ArrayPool.Shared.Return(numberBufferToReturn); - } + vlb.Dispose(); + if (numberBufferToReturn != null) + { + ArrayPool.Shared.Return(numberBufferToReturn); } } @@ -1001,26 +977,52 @@ internal static bool TryFormatBigInteger(BigInteger value, ReadOnlySpan base1E9Value; - public ReadOnlySpan sNegative; + public string sNegative; } - private static unsafe TChar* BigIntegerToDecChars(TChar* bufferEnd, ReadOnlySpan base1E9Value, int digits) + private static void BigIntegerToDecChars(Span destination, ReadOnlySpan base1E9Value, int digits) where TChar : unmanaged, IUtfChar { Debug.Assert(base1E9Value[^1] != 0, "Leading zeros should be trimmed by caller."); + int pos = destination.Length; // The base 10^9 value is in reverse order for (int i = 0; i < base1E9Value.Length - 1; i++) { - bufferEnd = UInt32ToDecChars(bufferEnd, (uint)base1E9Value[i], PowersOf1e9.MaxPartialDigits); + pos = UInt32ToDecChars(destination, pos, (uint)base1E9Value[i], PowersOf1e9.MaxPartialDigits); digits -= PowersOf1e9.MaxPartialDigits; } - return UInt32ToDecChars(bufferEnd, (uint)base1E9Value[^1], digits); + pos = UInt32ToDecChars(destination, pos, (uint)base1E9Value[^1], digits); + Debug.Assert(pos == 0); + } + + private static bool TryFormatInt32(int value, Span destination, ReadOnlySpan format, IFormatProvider? provider, out int charsWritten) + where TChar : unmanaged, IUtfChar + { + if (typeof(TChar) == typeof(Utf8Char)) + { + return value.TryFormat(Unsafe.BitCast, Span>(destination), out charsWritten, format, provider); + } + + Debug.Assert(typeof(TChar) == typeof(Utf16Char)); + return value.TryFormat(Unsafe.BitCast, Span>(destination), out charsWritten, format, provider); + } + + private static string Utf8CharsToString(ReadOnlySpan value) + where TChar : unmanaged, IUtfChar + { + return Encoding.UTF8.GetString(Unsafe.BitCast, ReadOnlySpan>(value)); + } + + private static string Utf16CharsToString(ReadOnlySpan value) + where TChar : unmanaged, IUtfChar + { + return new string(Unsafe.BitCast, ReadOnlySpan>(value)); } public @@ -1521,8 +1523,6 @@ public void MultiplyPowerOfTen(ReadOnlySpan left, int trailingZeroCount, } } - Debug.Assert(Unsafe.AreSame(ref bits[0], ref powersOfTen2[0])); - powersOfTen = powersOfTen.Slice(0, curLength); Span bits2 = bits.Slice(omittedLength, curLength += left.Length); @@ -1627,16 +1627,17 @@ static virtual bool TryParseWholeBlocks(ReadOnlySpan input, Span d [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool TryParseWholeBlocks(ReadOnlySpan input, Span destination) { + Span bytes = MemoryMarshal.AsBytes(destination); if ((typeof(TChar) == typeof(Utf8Char)) - ? (Convert.FromHexString(Unsafe.BitCast, ReadOnlySpan>(input), MemoryMarshal.AsBytes(destination), out _, out _) != OperationStatus.Done) - : (Convert.FromHexString(Unsafe.BitCast, ReadOnlySpan>(input), MemoryMarshal.AsBytes(destination), out _, out _) != OperationStatus.Done)) + ? (Convert.FromHexString(Unsafe.BitCast, ReadOnlySpan>(input), bytes, out _, out _) != OperationStatus.Done) + : (Convert.FromHexString(Unsafe.BitCast, ReadOnlySpan>(input), bytes, out _, out _) != OperationStatus.Done)) { return false; } if (BitConverter.IsLittleEndian) { - MemoryMarshal.AsBytes(destination).Reverse(); + bytes.Reverse(); } else { diff --git a/src/libraries/System.Runtime.Numerics/src/System/Number.Polyfill.cs b/src/libraries/System.Runtime.Numerics/src/System/Number.Polyfill.cs index cce131765786dc..922f3bd81d169c 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Number.Polyfill.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Number.Polyfill.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.Wasm; @@ -111,9 +110,14 @@ internal static bool IsWhiteSpace(this ReadOnlySpan span, out int internal static OperationStatus DecodeFromUtfChar(ReadOnlySpan span, out Rune result, out int elemsConsumed) where TChar : unmanaged, IUtfChar { - return (typeof(TChar) == typeof(Utf8Char)) - ? Rune.DecodeFromUtf8(Unsafe.BitCast, ReadOnlySpan>(span), out result, out elemsConsumed) - : Rune.DecodeFromUtf16(Unsafe.BitCast, ReadOnlySpan>(span), out result, out elemsConsumed); + if (typeof(TChar) == typeof(Utf8Char)) + { + return Rune.DecodeFromUtf8(Unsafe.BitCast, ReadOnlySpan>(span), out result, out elemsConsumed); + } + + Debug.Assert(typeof(TChar) == typeof(Utf16Char)); + + return Rune.DecodeFromUtf16(Unsafe.BitCast, ReadOnlySpan>(span), out result, out elemsConsumed); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -124,11 +128,9 @@ internal static ReadOnlySpan FromString(string value) { return Unsafe.BitCast, ReadOnlySpan>(Encoding.UTF8.GetBytes(value)); } - else - { - Debug.Assert(typeof(TChar) == typeof(Utf16Char)); - return Unsafe.BitCast, ReadOnlySpan>(value); - } + + Debug.Assert(typeof(TChar) == typeof(Utf16Char)); + return Unsafe.BitCast, ReadOnlySpan>(value.AsSpan()); } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 59bb6ff5cb6fd74951b8a085ac9c876df3cd3ec1 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Wed, 5 Aug 2026 17:13:02 -0700 Subject: [PATCH 2/3] Address formatting review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/System/Globalization/DateTimeFormat.cs | 10 +++++++++- .../src/System/Globalization/TimeSpanFormat.cs | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs index ca4d00c6eb772b..1b9019ac35169d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs @@ -1138,7 +1138,15 @@ internal static bool TryFormat(DateTime dateTime, Span destination var vlb = new ValueListBuilder(destination); FormatCustomized(dateTime, format, dtfi, offset, ref vlb); - bool success = vlb.TryCopyTo(destination, out charsWritten); + bool success = Unsafe.AreSame(ref MemoryMarshal.GetReference(destination), ref MemoryMarshal.GetReference(vlb.AsSpan())); + if (success) + { + charsWritten = vlb.Length; + } + else + { + success = vlb.TryCopyTo(destination, out charsWritten); + } vlb.Dispose(); return success; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs index 0bf087cea23dd4..8bbf7ddb654c63 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/TimeSpanFormat.cs @@ -265,7 +265,7 @@ internal static bool TryFormatStandard(TimeSpan value, StandardFormat for // the JIT prove that all suffix writes at constant offsets 0..7 are in bounds. if ((uint)suffixLen < 8u) { - ThrowHelper.ThrowArgumentOutOfRangeException(); + ThrowHelper.ThrowUnreachableException(); } Span suffix = destination.Slice(pos, suffixLen); @@ -306,7 +306,7 @@ internal static bool TryFormatStandard(TimeSpan value, StandardFormat for // the JIT prove that all suffix writes at constant offsets 0..6 are in bounds. if ((uint)suffixLen < 7u) { - ThrowHelper.ThrowArgumentOutOfRangeException(); + ThrowHelper.ThrowUnreachableException(); } Span suffix = destination.Slice(pos, suffixLen); From 45b23d05af812a1eb386f85ad12c376bbd1a1517 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Wed, 5 Aug 2026 17:28:59 -0700 Subject: [PATCH 3/3] Keep DateTime formatting copy path safe Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/System/Globalization/DateTimeFormat.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs index 1b9019ac35169d..ca4d00c6eb772b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs @@ -1138,15 +1138,7 @@ internal static bool TryFormat(DateTime dateTime, Span destination var vlb = new ValueListBuilder(destination); FormatCustomized(dateTime, format, dtfi, offset, ref vlb); - bool success = Unsafe.AreSame(ref MemoryMarshal.GetReference(destination), ref MemoryMarshal.GetReference(vlb.AsSpan())); - if (success) - { - charsWritten = vlb.Length; - } - else - { - success = vlb.TryCopyTo(destination, out charsWritten); - } + bool success = vlb.TryCopyTo(destination, out charsWritten); vlb.Dispose(); return success; }