diff --git a/src/libraries/Common/src/System/Number.Formatting.Common.cs b/src/libraries/Common/src/System/Number.Formatting.Common.cs
index 4caa59472eaea4..672ff2402682b9 100644
--- a/src/libraries/Common/src/System/Number.Formatting.Common.cs
+++ b/src/libraries/Common/src/System/Number.Formatting.Common.cs
@@ -12,6 +12,8 @@ namespace System
{
internal static partial class Number
{
+ private const int CharStackBufferSize = 32;
+
private const int DefaultPrecisionExponentialFormat = 6;
private const int MaxUInt32DecDigits = 10;
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 22a4ddea6ed606..b9f4be64bc9a74 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -133,6 +133,7 @@
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/FormattingHelpers.CountDigits.Int128.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/FormattingHelpers.CountDigits.Int128.cs
new file mode 100644
index 00000000000000..5264f100112eb7
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/FormattingHelpers.CountDigits.Int128.cs
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace System.Buffers.Text
+{
+ internal static partial class FormattingHelpers
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int CountDigits(UInt128 value)
+ {
+ ulong upper = value.Upper;
+
+ // 1e19 is 8AC7_2304_89E8_0000
+ // 1e20 is 5_6BC7_5E2D_6310_0000
+ // 1e21 is 36_35C9_ADC5_DEA0_0000
+
+ if (upper == 0)
+ {
+ // We have less than 64-bits, so just return the lower count
+ return CountDigits(value.Lower);
+ }
+
+ // We have more than 1e19, so we have at least 20 digits
+ int digits = 20;
+
+ if (upper > 5)
+ {
+ // ((2^128) - 1) / 1e20 < 34_02_823_669_209_384_635 which
+ // is 18.5318 digits, meaning the result definitely fits
+ // into 64-bits and we only need to add the lower digit count
+
+ value /= new UInt128(0x5, 0x6BC7_5E2D_6310_0000); // value /= 1e20
+ Debug.Assert(value.Upper == 0);
+
+ digits += CountDigits(value.Lower);
+ }
+ else if ((upper == 5) && (value.Lower >= 0x6BC75E2D63100000))
+ {
+ // We're greater than 1e20, but definitely less than 1e21
+ // so we have exactly 21 digits
+
+ digits++;
+ Debug.Assert(digits == 21);
+ }
+
+ return digits;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int CountHexDigits(UInt128 value)
+ {
+ // The number of hex digits is log16(value) + 1, or log2(value) / 4 + 1
+ return ((int)UInt128.Log2(value) >> 2) + 1;
+ }
+ }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/FormattingHelpers.CountDigits.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/FormattingHelpers.CountDigits.cs
index 3da8684132f077..18eac2cf08eb7e 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/FormattingHelpers.CountDigits.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/FormattingHelpers.CountDigits.cs
@@ -1,7 +1,6 @@
// 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.Binary;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -11,47 +10,6 @@ namespace System.Buffers.Text
{
internal static partial class FormattingHelpers
{
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int CountDigits(UInt128 value)
- {
- ulong upper = value.Upper;
-
- // 1e19 is 8AC7_2304_89E8_0000
- // 1e20 is 5_6BC7_5E2D_6310_0000
- // 1e21 is 36_35C9_ADC5_DEA0_0000
-
- if (upper == 0)
- {
- // We have less than 64-bits, so just return the lower count
- return CountDigits(value.Lower);
- }
-
- // We have more than 1e19, so we have at least 20 digits
- int digits = 20;
-
- if (upper > 5)
- {
- // ((2^128) - 1) / 1e20 < 34_02_823_669_209_384_635 which
- // is 18.5318 digits, meaning the result definitely fits
- // into 64-bits and we only need to add the lower digit count
-
- value /= new UInt128(0x5, 0x6BC7_5E2D_6310_0000); // value /= 1e20
- Debug.Assert(value.Upper == 0);
-
- digits += CountDigits(value.Lower);
- }
- else if ((upper == 5) && (value.Lower >= 0x6BC75E2D63100000))
- {
- // We're greater than 1e20, but definitely less than 1e21
- // so we have exactly 21 digits
-
- digits++;
- Debug.Assert(digits == 21);
- }
-
- return digits;
- }
-
// Based on do_count_digits from https://github.com/fmtlib/fmt/blob/662adf4f33346ba9aba8b072194e319869ede54a/include/fmt/format.h#L1124
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int CountDigits(ulong value)
@@ -149,13 +107,6 @@ public static int CountDigits(uint value)
return (int)((value + tableValue) >> 32);
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static int CountHexDigits(UInt128 value)
- {
- // The number of hex digits is log16(value) + 1, or log2(value) / 4 + 1
- return ((int)UInt128.Log2(value) >> 2) + 1;
- }
-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int CountHexDigits(ulong value)
{
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 ad75d88cbda7c1..2a89bf96385294 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs
@@ -269,8 +269,6 @@ internal static partial class Number
private const int SinglePrecisionCustomFormat = 7;
private const int DoublePrecisionCustomFormat = 15;
- private const int CharStackBufferSize = 32;
-
/// The non-inclusive upper bound of .
///
/// This is a semi-arbitrary bound. For mono, which is often used for more size-constrained workloads,
diff --git a/src/libraries/System.Runtime.Numerics/src/System.Runtime.Numerics.csproj b/src/libraries/System.Runtime.Numerics/src/System.Runtime.Numerics.csproj
index d2003757c60f3d..6526656921c343 100644
--- a/src/libraries/System.Runtime.Numerics/src/System.Runtime.Numerics.csproj
+++ b/src/libraries/System.Runtime.Numerics/src/System.Runtime.Numerics.csproj
@@ -25,6 +25,8 @@
+
fo
return spanSuccess;
}
+ private const uint kuBase = 1_000_000_000; // 10^9
+ private const int kcchBase = 9;
+
private static unsafe string? FormatBigInteger(
bool targetSpan, BigInteger value,
string? formatString, ReadOnlySpan formatSpan,
@@ -882,6 +886,7 @@ internal static bool TryFormatBigInteger(BigInteger value, ReadOnlySpan fo
}
else
{
+ Debug.Assert(formatString != null);
charsWritten = 0;
spanSuccess = false;
return value._sign.ToString(formatString, info);
@@ -889,17 +894,17 @@ internal static bool TryFormatBigInteger(BigInteger value, ReadOnlySpan fo
}
// First convert to base 10^9.
- const uint kuBase = 1000000000; // 10^9
- const int kcchBase = 9;
-
int cuSrc = value._bits.Length;
- int cuMax;
- try
- {
- cuMax = checked(cuSrc * 10 / 9 + 2);
- }
- catch (OverflowException e) { throw new FormatException(SR.Format_TooLarge, e); }
- uint[] rguDst = new uint[cuMax];
+ // A quick conservative max length of base 10^9 representation
+ // A uint contributes to no more than 10/9 of 10^9 block, +1 for ceiling of division
+ int cuMax = cuSrc * (kcchBase + 1) / kcchBase + 1;
+ Debug.Assert((long)BigInteger.MaxLength * (kcchBase + 1) / kcchBase + 1 < (long)int.MaxValue); // won't overflow
+
+ uint[]? bufferToReturn = null;
+ Span base1E9Buffer = cuMax < BigIntegerCalculator.StackAllocThreshold ?
+ stackalloc uint[cuMax] :
+ (bufferToReturn = ArrayPool.Shared.Rent(cuMax));
+
int cuDst = 0;
for (int iuSrc = cuSrc; --iuSrc >= 0;)
@@ -907,93 +912,86 @@ internal static bool TryFormatBigInteger(BigInteger value, ReadOnlySpan fo
uint uCarry = value._bits[iuSrc];
for (int iuDst = 0; iuDst < cuDst; iuDst++)
{
- Debug.Assert(rguDst[iuDst] < kuBase);
- ulong uuRes = NumericsHelpers.MakeUInt64(rguDst[iuDst], uCarry);
- rguDst[iuDst] = (uint)(uuRes % kuBase);
- uCarry = (uint)(uuRes / kuBase);
+ Debug.Assert(base1E9Buffer[iuDst] < kuBase);
+
+ // Use X86Base.DivRem when stable
+ ulong uuRes = NumericsHelpers.MakeUInt64(base1E9Buffer[iuDst], uCarry);
+ (ulong quo, ulong rem) = Math.DivRem(uuRes, kuBase);
+ uCarry = (uint)quo;
+ base1E9Buffer[iuDst] = (uint)rem;
}
if (uCarry != 0)
{
- rguDst[cuDst++] = uCarry % kuBase;
- uCarry /= kuBase;
+ (uCarry, base1E9Buffer[cuDst++]) = Math.DivRem(uCarry, kuBase);
if (uCarry != 0)
- rguDst[cuDst++] = uCarry;
+ base1E9Buffer[cuDst++] = uCarry;
}
}
- int cchMax;
- try
- {
- // Each uint contributes at most 9 digits to the decimal representation.
- cchMax = checked(cuDst * kcchBase);
- }
- catch (OverflowException e) { throw new FormatException(SR.Format_TooLarge, e); }
+ ReadOnlySpan base1E9Value = base1E9Buffer[..cuDst];
+
+ int valueDigits = (base1E9Value.Length - 1) * kcchBase + FormattingHelpers.CountDigits(base1E9Value[^1]);
+
+ string? strResult;
- bool decimalFmt = (fmt == 'g' || fmt == 'G' || fmt == 'd' || fmt == 'D' || fmt == 'r' || fmt == 'R');
- if (decimalFmt)
+ if (fmt == 'g' || fmt == 'G' || fmt == 'd' || fmt == 'D' || fmt == 'r' || fmt == 'R')
{
- if (digits > 0 && digits > cchMax)
- cchMax = digits;
- if (value._sign < 0)
+ int strDigits = Math.Max(digits, valueDigits);
+ string? sNegative = value.Sign < 0 ? info.NegativeSign : null;
+ int strLength = strDigits + (sNegative?.Length ?? 0);
+
+ if (targetSpan)
{
- try
+ if (destination.Length < strLength)
+ {
+ spanSuccess = false;
+ charsWritten = 0;
+ }
+ else
{
- // Leave an extra slot for a minus sign.
- cchMax = checked(cchMax + info.NegativeSign.Length);
+ sNegative?.CopyTo(destination);
+ fixed (char* ptr = &MemoryMarshal.GetReference(destination))
+ {
+ BigIntegerToDecChars((Utf16Char*)ptr + strLength, base1E9Value, digits);
+ }
+ charsWritten = strLength;
+ spanSuccess = true;
}
- catch (OverflowException e) { throw new FormatException(SR.Format_TooLarge, e); }
+ strResult = null;
}
- }
-
- int rgchBufSize;
-
- try
- {
- // We'll pass the rgch buffer to native code, which is going to treat it like a string of digits, so it needs
- // to be null terminated. Let's ensure that we can allocate a buffer of that size.
- rgchBufSize = checked(cchMax + 1);
- }
- catch (OverflowException e) { throw new FormatException(SR.Format_TooLarge, e); }
-
- char[] rgch = new char[rgchBufSize];
-
- int ichDst = cchMax;
-
- for (int iuDst = 0; iuDst < cuDst - 1; iuDst++)
- {
- uint uDig = rguDst[iuDst];
- Debug.Assert(uDig < kuBase);
- for (int cch = kcchBase; --cch >= 0;)
+ else
{
- rgch[--ichDst] = (char)('0' + uDig % 10);
- uDig /= 10;
+ spanSuccess = false;
+ charsWritten = 0;
+ fixed (uint* ptr = base1E9Value)
+ {
+ strResult = string.Create(strLength, (digits, ptr: (IntPtr)ptr, base1E9Value.Length, sNegative), static (span, state) =>
+ {
+ state.sNegative?.CopyTo(span);
+ fixed (char* ptr = &MemoryMarshal.GetReference(span))
+ {
+ BigIntegerToDecChars((Utf16Char*)ptr + span.Length, new ReadOnlySpan((void*)state.ptr, state.Length), state.digits);
+ }
+ });
+ }
}
}
- for (uint uDig = rguDst[cuDst - 1]; uDig != 0;)
- {
- rgch[--ichDst] = (char)('0' + uDig % 10);
- uDig /= 10;
- }
-
- if (!decimalFmt)
+ else
{
- // sign = true for negative and false for 0 and positive values
- bool sign = (value._sign < 0);
- int scale = cchMax - ichDst;
-
- byte[]? buffer = ArrayPool.Shared.Rent(rgchBufSize + 1);
- fixed (byte* ptr = buffer) // NumberBuffer expects pinned Digits
+ byte[]? numberBufferToReturn = null;
+ Span 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, buffer);
+ 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;
- for (int i = 0; i < rgch.Length - ichDst; i++)
- number.Digits[i] = (byte)rgch[ichDst + i];
- number.Digits[rgch.Length - ichDst] = 0;
- number.DigitsCount = rgch.Length - ichDst - 1; // The cut-off point to switch (G)eneral from (F)ixed-point to (E)xponential form
- number.Scale = scale;
- number.IsNegative = sign;
-
- scoped var vlb = new ValueListBuilder(stackalloc Utf16Char[128]); // arbitrary stack cut-off
+ scoped var vlb = new ValueListBuilder(stackalloc Utf16Char[CharStackBufferSize]); // arbitrary stack cut-off
if (fmt != 0)
{
@@ -1007,59 +1005,44 @@ internal static bool TryFormatBigInteger(BigInteger value, ReadOnlySpan fo
if (targetSpan)
{
spanSuccess = vlb.TryCopyTo(MemoryMarshal.Cast(destination), out charsWritten);
- vlb.Dispose();
- return null;
+ strResult = null;
}
else
{
charsWritten = 0;
spanSuccess = false;
- string result = MemoryMarshal.Cast(vlb.AsSpan()).ToString();
- vlb.Dispose();
- return result;
+ strResult = MemoryMarshal.Cast(vlb.AsSpan()).ToString();
+ }
+
+ vlb.Dispose();
+ if (numberBufferToReturn != null)
+ {
+ ArrayPool.Shared.Return(numberBufferToReturn);
}
}
}
- // Format Round-trip decimal
- // This format is supported for integral types only. The number is converted to a string of
- // decimal digits (0-9), prefixed by a minus sign if the number is negative. The precision
- // specifier indicates the minimum number of digits desired in the resulting string. If required,
- // the number is padded with zeros to its left to produce the number of digits given by the
- // precision specifier.
- int numDigitsPrinted = cchMax - ichDst;
- while (digits > 0 && digits > numDigitsPrinted)
- {
- // pad leading zeros
- rgch[--ichDst] = '0';
- digits--;
- }
- if (value._sign < 0)
+ if (bufferToReturn != null)
{
- string negativeSign = info.NegativeSign;
- for (int i = negativeSign.Length - 1; i > -1; i--)
- rgch[--ichDst] = negativeSign[i];
+ ArrayPool.Shared.Return(bufferToReturn);
}
- int resultLength = cchMax - ichDst;
- if (!targetSpan)
- {
- charsWritten = 0;
- spanSuccess = false;
- return new string(rgch, ichDst, cchMax - ichDst);
- }
- else if (new ReadOnlySpan(rgch, ichDst, cchMax - ichDst).TryCopyTo(destination))
- {
- charsWritten = resultLength;
- spanSuccess = true;
- return null;
- }
- else
+ return strResult;
+ }
+
+ private static unsafe TChar* BigIntegerToDecChars(TChar* bufferEnd, ReadOnlySpan base1E9Value, int digits)
+ where TChar : unmanaged, IUtfChar
+ {
+ Debug.Assert(base1E9Value[^1] != 0, "Leading zeros should be trimmed by caller.");
+
+ // The base 10^9 value is in reverse order
+ for (int i = 0; i < base1E9Value.Length - 1; i++)
{
- charsWritten = 0;
- spanSuccess = false;
- return null;
+ bufferEnd = UInt32ToDecChars(bufferEnd, base1E9Value[i], kcchBase);
+ digits -= kcchBase;
}
+
+ return UInt32ToDecChars(bufferEnd, base1E9Value[^1], digits);
}
}
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 d5de46b5842eb5..9c1c2a89daf5ef 100644
--- a/src/libraries/System.Runtime.Numerics/src/System/Number.Polyfill.cs
+++ b/src/libraries/System.Runtime.Numerics/src/System/Number.Polyfill.cs
@@ -43,6 +43,21 @@ internal readonly struct Utf16Char(char ch) : IUtfChar
public bool Equals(Utf16Char other) => value == other.value;
}
+#pragma warning disable CA1067 // Polyfill only type
+ internal readonly struct Utf8Char(byte ch) : IUtfChar
+#pragma warning restore CA1067
+ {
+ private readonly byte value = ch;
+
+ public static Utf8Char CastFrom(byte value) => new(value);
+ public static Utf8Char CastFrom(char value) => new((byte)value);
+ public static Utf8Char CastFrom(int value) => new((byte)value);
+ public static Utf8Char CastFrom(uint value) => new((byte)value);
+ public static Utf8Char CastFrom(ulong value) => new((byte)value);
+ public static uint CastToUInt32(Utf8Char value) => value.value;
+ public bool Equals(Utf8Char other) => value == other.value;
+ }
+
internal static partial class Number
{
internal static bool AllowHyphenDuringParsing(this NumberFormatInfo info)