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)