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)
{
- int digits = 1;
- uint part;
- if (value >= 10000000)
+ // Map the log2(value) to a power of 10.
+ ReadOnlySpan<byte> log2ToPow10 = new byte[]
{
- if (value >= 100000000000000)
- {
- part = (uint)(value / 100000000000000);
- digits += 14;
- }
- else
- {
- part = (uint)(value / 10000000);
- digits += 7;
- }
- }
- else
- {
- part = (uint)value;
- }
+ 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5,
+ 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10,
+ 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 15, 15,
+ 15, 16, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 19, 20
+ };
+ Debug.Assert(log2ToPow10.Length == 64);
+ uint index = Unsafe.Add(ref MemoryMarshal.GetReference(log2ToPow10), BitOperations.Log2(value));
- if (part < 10)
- {
- // no-op
- }
- else if (part < 100)
+ // TODO https://github.com/dotnet/runtime/issues/60948: Use ReadOnlySpan<ulong> instead of ReadOnlySpan<byte>.
+ // Read the associated power of 10.
+ ReadOnlySpan<byte> powersOf10 = new byte[]
{
- digits++;
- }
- else if (part < 1000)
- {
- digits += 2;
- }
- else if (part < 10000)
- {
- digits += 3;
- }
- else if (part < 100000)
- {
- digits += 4;
- }
- else if (part < 1000000)
- {
- digits += 5;
- }
- else
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // unused entry to avoid needing to subtract
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0
+ 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 10
+ 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 100
+ 0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 1000
+ 0x10, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 10000
+ 0xA0, 0x86, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // 100000
+ 0x40, 0x42, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, // 1000000
+ 0x80, 0x96, 0x98, 0x00, 0x00, 0x00, 0x00, 0x00, // 10000000
+ 0x00, 0xE1, 0xF5, 0x05, 0x00, 0x00, 0x00, 0x00, // 100000000
+ 0x00, 0xCA, 0x9A, 0x3B, 0x00, 0x00, 0x00, 0x00, // 1000000000
+ 0x00, 0xE4, 0x0B, 0x54, 0x02, 0x00, 0x00, 0x00, // 10000000000
+ 0x00, 0xE8, 0x76, 0x48, 0x17, 0x00, 0x00, 0x00, // 100000000000
+ 0x00, 0x10, 0xA5, 0xD4, 0xE8, 0x00, 0x00, 0x00, // 1000000000000
+ 0x00, 0xA0, 0x72, 0x4E, 0x18, 0x09, 0x00, 0x00, // 10000000000000
+ 0x00, 0x40, 0x7A, 0x10, 0xF3, 0x5A, 0x00, 0x00, // 100000000000000
+ 0x00, 0x80, 0xC6, 0xA4, 0x7E, 0x8D, 0x03, 0x00, // 1000000000000000
+ 0x00, 0x00, 0xC1, 0x6F, 0xF2, 0x86, 0x23, 0x00, // 10000000000000000
+ 0x00, 0x00, 0x8A, 0x5D, 0x78, 0x45, 0x63, 0x01, // 100000000000000000
+ 0x00, 0x00, 0x64, 0xA7, 0xB3, 0xB6, 0xE0, 0x0D, // 1000000000000000000
+ 0x00, 0x00, 0xE8, 0x89, 0x04, 0x23, 0xC7, 0x8A, // 10000000000000000000
+ };
+ Debug.Assert((index + 1) * sizeof(ulong) <= powersOf10.Length);
+ ulong powerOf10 = Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref MemoryMarshal.GetReference(powersOf10), index * sizeof(ulong)));
+ if (!BitConverter.IsLittleEndian)
{
- Debug.Assert(part < 10000000);
- digits += 6;
+ powerOf10 = BinaryPrimitives.ReverseEndianness(powerOf10);
}
- return digits;
+ // Return the number of digits based on the power of 10, shifted by 1
+ // if it falls below the threshold.
+ bool lessThan = value < powerOf10;
+ return (int)(index - Unsafe.As<bool, byte>(ref lessThan)); // while arbitrary bools may be non-0/1, comparison operators are expected to return 0/1
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
"(#)", "-#", "- #", "#-", "# -",
};
+ // Optimizations using "TwoDigits" inspired by:
+ // https://engineering.fb.com/2013/03/15/developer-tools/three-optimization-tips-for-c/
+ private const string TwoDigitsChars =
+ "00010203040506070809" +
+ "10111213141516171819" +
+ "20212223242526272829" +
+ "30313233343536373839" +
+ "40414243444546474849" +
+ "50515253545556575859" +
+ "60616263646566676869" +
+ "70717273747576777879" +
+ "80818283848586878889" +
+ "90919293949596979899";
+ private static ReadOnlySpan<byte> TwoDigitsBytes =>
+ "00010203040506070809"u8 +
+ "10111213141516171819"u8 +
+ "20212223242526272829"u8 +
+ "30313233343536373839"u8 +
+ "40414243444546474849"u8 +
+ "50515253545556575859"u8 +
+ "60616263646566676869"u8 +
+ "70717273747576777879"u8 +
+ "80818283848586878889"u8 +
+ "90919293949596979899"u8;
+
public static unsafe string FormatDecimal(decimal value, ReadOnlySpan<char> format, NumberFormatInfo info)
{
char fmt = ParseFormatSpecifier(format, out int digits);
if (format.Length == 0)
{
return value >= 0 ?
- TryUInt32ToDecStr((uint)value, digits: -1, destination, out charsWritten) :
+ TryUInt32ToDecStr((uint)value, destination, out charsWritten) :
TryNegativeInt32ToDecStr(value, digits: -1, NumberFormatInfo.GetInstance(provider).NegativeSign, destination, out charsWritten);
}
// Fast path for default format
if (format.Length == 0)
{
- return TryUInt32ToDecStr(value, digits: -1, destination, out charsWritten);
+ return TryUInt32ToDecStr(value, destination, out charsWritten);
}
return TryFormatUInt32Slow(value, format, provider, destination, out charsWritten);
if (string.IsNullOrEmpty(format))
{
return value >= 0 ?
- UInt64ToDecStr((ulong)value, digits: -1) :
+ UInt64ToDecStr((ulong)value) :
NegativeInt64ToDecStr(value, digits: -1, NumberFormatInfo.GetInstance(provider).NegativeSign);
}
if (format.Length == 0)
{
return value >= 0 ?
- TryUInt64ToDecStr((ulong)value, digits: -1, destination, out charsWritten) :
+ TryUInt64ToDecStr((ulong)value, destination, out charsWritten) :
TryNegativeInt64ToDecStr(value, digits: -1, NumberFormatInfo.GetInstance(provider).NegativeSign, destination, out charsWritten);
}
// Fast path for default format
if (string.IsNullOrEmpty(format))
{
- return UInt64ToDecStr(value, digits: -1);
+ return UInt64ToDecStr(value);
}
return FormatUInt64Slow(value, format, provider);
// Fast path for default format
if (format.Length == 0)
{
- return TryUInt64ToDecStr(value, digits: -1, destination, out charsWritten);
+ return TryUInt64ToDecStr(value, destination, out charsWritten);
}
return TryFormatUInt64Slow(value, format, provider, destination, out charsWritten);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static unsafe void WriteTwoDigits(char* ptr, uint value)
+ {
+ Debug.Assert(value <= 99);
+ Unsafe.WriteUnaligned(ptr,
+ Unsafe.ReadUnaligned<uint>(
+ ref Unsafe.As<char, byte>(
+ ref Unsafe.Add(ref TwoDigitsChars.GetRawStringData(), (int)value * 2))));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static unsafe void WriteTwoDigits(byte* ptr, uint value)
+ {
+ Debug.Assert(value <= 99);
+ Unsafe.WriteUnaligned(ptr,
+ Unsafe.ReadUnaligned<ushort>(
+ ref Unsafe.Add(ref MemoryMarshal.GetReference(TwoDigitsBytes), (int)value * 2)));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe byte* UInt32ToDecChars(byte* bufferEnd, uint value)
{
- do
+ if (value >= 10)
{
- uint remainder;
- (value, remainder) = Math.DivRem(value, 10);
- *(--bufferEnd) = (byte)(remainder + '0');
+ // 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(bufferEnd, remainder);
+ }
+
+ // If there are two digits remaining, store them.
+ if (value >= 10)
+ {
+ bufferEnd -= 2;
+ WriteTwoDigits(bufferEnd, value);
+ return bufferEnd;
+ }
}
- while (value != 0);
+ // Otherwise, store the single digit remaining.
+ *(--bufferEnd) = (byte)(value + '0');
return bufferEnd;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe byte* UInt32ToDecChars(byte* bufferEnd, uint value, int digits)
{
- while (--digits >= 0 || value != 0)
+ uint remainder;
+ while (value >= 100)
{
- uint remainder;
+ bufferEnd -= 2;
+ digits -= 2;
+ (value, remainder) = Math.DivRem(value, 100);
+ WriteTwoDigits(bufferEnd, remainder);
+ }
+
+ while (value != 0 || digits > 0)
+ {
+ digits--;
(value, remainder) = Math.DivRem(value, 10);
*(--bufferEnd) = (byte)(remainder + '0');
}
+
return bufferEnd;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe char* UInt32ToDecChars(char* bufferEnd, uint value)
{
- do
+ if (value >= 10)
{
- uint remainder;
- (value, remainder) = Math.DivRem(value, 10);
- *(--bufferEnd) = (char)(remainder + '0');
+ // 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(bufferEnd, remainder);
+ }
+
+ // If there are two digits remaining, store them.
+ if (value >= 10)
+ {
+ bufferEnd -= 2;
+ WriteTwoDigits(bufferEnd, value);
+ return bufferEnd;
+ }
}
- while (value != 0);
+ // Otherwise, store the single digit remaining.
+ *(--bufferEnd) = (char)(value + '0');
return bufferEnd;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe char* UInt32ToDecChars(char* bufferEnd, uint value, int digits)
{
- while (--digits >= 0 || value != 0)
+ // Handle all values >= 100 two-digits at a time so as to avoid expensive integer division operations.
+ uint remainder;
+ while (value >= 100)
{
- uint remainder;
+ bufferEnd -= 2;
+ digits -= 2;
+ (value, remainder) = Math.DivRem(value, 100);
+ WriteTwoDigits(bufferEnd, remainder);
+ }
+
+ // Continue writing single digits until we've exhausted both the value and the requested number of digits.
+ while (value != 0 || digits > 0)
+ {
+ digits--;
(value, remainder) = Math.DivRem(value, 10);
*(--bufferEnd) = (char)(remainder + '0');
}
+
return bufferEnd;
}
return result;
}
- private static unsafe bool TryUInt32ToDecStr(uint value, int digits, Span<char> destination, out int charsWritten)
+ private static unsafe bool TryUInt32ToDecStr(uint value, Span<char> destination, out int charsWritten)
{
- int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value));
- if (bufferLength > destination.Length)
+ int bufferLength = FormattingHelpers.CountDigits(value);
+ if (bufferLength <= destination.Length)
{
- charsWritten = 0;
- return false;
+ charsWritten = bufferLength;
+ fixed (char* buffer = &MemoryMarshal.GetReference(destination))
+ {
+ char* p = UInt32ToDecChars(buffer + bufferLength, value);
+ Debug.Assert(p == buffer);
+ }
+ return true;
}
- charsWritten = bufferLength;
- fixed (char* buffer = &MemoryMarshal.GetReference(destination))
+ charsWritten = 0;
+ return false;
+ }
+
+ private static unsafe bool TryUInt32ToDecStr(uint value, int digits, Span<char> destination, out int charsWritten)
+ {
+ int countedDigits = FormattingHelpers.CountDigits(value);
+ int bufferLength = Math.Max(digits, countedDigits);
+ if (bufferLength <= destination.Length)
{
- char* p = buffer + bufferLength;
- if (digits <= 1)
+ charsWritten = bufferLength;
+ fixed (char* buffer = &MemoryMarshal.GetReference(destination))
{
- p = UInt32ToDecChars(p, value);
+ char* p = buffer + bufferLength;
+ p = digits > countedDigits ?
+ UInt32ToDecChars(p, value, digits) :
+ UInt32ToDecChars(p, value);
+ Debug.Assert(p == buffer);
}
- else
- {
- p = UInt32ToDecChars(p, value, digits);
- }
- Debug.Assert(p == buffer);
+ return true;
}
- return true;
+
+ charsWritten = 0;
+ return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
}
return UInt32ToDecChars(bufferEnd, (uint)value);
#else
- do
+ if (value >= 10)
{
- ulong remainder;
- (value, remainder) = Math.DivRem(value, 10);
- *(--bufferEnd) = (byte)(remainder + '0');
+ // Handle all values >= 100 two-digits at a time so as to avoid expensive integer division operations.
+ while (value >= 100)
+ {
+ bufferEnd -= 2;
+ (value, ulong remainder) = Math.DivRem(value, 100);
+ WriteTwoDigits(bufferEnd, (uint)remainder);
+ }
+
+ // If there are two digits remaining, store them.
+ if (value >= 10)
+ {
+ bufferEnd -= 2;
+ WriteTwoDigits(bufferEnd, (uint)value);
+ return bufferEnd;
+ }
}
- while (value != 0);
+ // Otherwise, store the single digit remaining.
+ *(--bufferEnd) = (byte)(value + '0');
return bufferEnd;
#endif
}
}
return UInt32ToDecChars(bufferEnd, (uint)value, digits);
#else
- while (--digits >= 0 || value != 0)
+ ulong remainder;
+ while (value >= 100)
+ {
+ bufferEnd -= 2;
+ digits -= 2;
+ (value, remainder) = Math.DivRem(value, 100);
+ WriteTwoDigits(bufferEnd, (uint)remainder);
+ }
+
+ while (value != 0 || digits > 0)
{
- ulong remainder;
+ digits--;
(value, remainder) = Math.DivRem(value, 10);
*(--bufferEnd) = (byte)(remainder + '0');
}
+
return bufferEnd;
#endif
}
}
return UInt32ToDecChars(bufferEnd, (uint)value);
#else
- do
+ if (value >= 10)
{
- ulong remainder;
- (value, remainder) = Math.DivRem(value, 10);
- *(--bufferEnd) = (char)(remainder + '0');
+ // Handle all values >= 100 two-digits at a time so as to avoid expensive integer division operations.
+ while (value >= 100)
+ {
+ bufferEnd -= 2;
+ (value, ulong remainder) = Math.DivRem(value, 100);
+ WriteTwoDigits(bufferEnd, (uint)remainder);
+ }
+
+ // If there are two digits remaining, store them.
+ if (value >= 10)
+ {
+ bufferEnd -= 2;
+ WriteTwoDigits(bufferEnd, (uint)value);
+ return bufferEnd;
+ }
}
- while (value != 0);
+ // Otherwise, store the single digit remaining.
+ *(--bufferEnd) = (char)(value + '0');
return bufferEnd;
#endif
}
}
return UInt32ToDecChars(bufferEnd, (uint)value, digits);
#else
- while (--digits >= 0 || value != 0)
+ ulong remainder;
+ while (value >= 100)
{
- ulong remainder;
+ bufferEnd -= 2;
+ digits -= 2;
+ (value, remainder) = Math.DivRem(value, 100);
+ WriteTwoDigits(bufferEnd, (uint)remainder);
+ }
+
+ while (value != 0 || digits > 0)
+ {
+ digits--;
(value, remainder) = Math.DivRem(value, 10);
*(--bufferEnd) = (char)(remainder + '0');
}
+
return bufferEnd;
#endif
}
return result;
}
- private static unsafe bool TryUInt64ToDecStr(ulong value, int digits, Span<char> destination, out int charsWritten)
+ private static unsafe bool TryUInt64ToDecStr(ulong value, Span<char> destination, out int charsWritten)
{
- int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value));
- if (bufferLength > destination.Length)
- {
- charsWritten = 0;
- return false;
- }
-
- charsWritten = bufferLength;
- fixed (char* buffer = &MemoryMarshal.GetReference(destination))
+ int bufferLength = FormattingHelpers.CountDigits(value);
+ if (bufferLength <= destination.Length)
{
- char* p = buffer + bufferLength;
- if (digits <= 1)
+ charsWritten = bufferLength;
+ fixed (char* buffer = &MemoryMarshal.GetReference(destination))
{
+ char* p = buffer + bufferLength;
p = UInt64ToDecChars(p, value);
+ Debug.Assert(p == buffer);
}
- else
+ return true;
+ }
+
+ charsWritten = 0;
+ return false;
+ }
+
+ private static unsafe bool TryUInt64ToDecStr(ulong value, int digits, Span<char> destination, out int charsWritten)
+ {
+ int countedDigits = FormattingHelpers.CountDigits(value);
+ int bufferLength = Math.Max(digits, countedDigits);
+ if (bufferLength <= destination.Length)
+ {
+ charsWritten = bufferLength;
+ fixed (char* buffer = &MemoryMarshal.GetReference(destination))
{
- p = UInt64ToDecChars(p, value, digits);
+ char* p = buffer + bufferLength;
+ p = digits > countedDigits ?
+ UInt64ToDecChars(p, value, digits) :
+ UInt64ToDecChars(p, value);
+ Debug.Assert(p == buffer);
}
- Debug.Assert(p == buffer);
+ return true;
}
- return true;
+
+ charsWritten = 0;
+ return false;
}
private static unsafe void Int128ToNumber(Int128 value, ref NumberBuffer number)
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static unsafe byte* UInt128ToDecChars(byte* bufferEnd, UInt128 value)
- {
- while (value.Upper != 0)
- {
- bufferEnd = UInt64ToDecChars(bufferEnd, Int128DivMod1E19(ref value), 19);
- }
- return UInt64ToDecChars(bufferEnd, value.Lower);
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static unsafe byte* UInt128ToDecChars(byte* bufferEnd, UInt128 value, int digits)
{
while (value.Upper != 0)
private static unsafe bool TryUInt128ToDecStr(UInt128 value, int digits, Span<char> destination, out int charsWritten)
{
- int bufferLength = Math.Max(digits, FormattingHelpers.CountDigits(value));
- if (bufferLength > destination.Length)
+ int countedDigits = FormattingHelpers.CountDigits(value);
+ int bufferLength = Math.Max(digits, countedDigits);
+ if (bufferLength <= destination.Length)
{
- charsWritten = 0;
- return false;
- }
-
- charsWritten = bufferLength;
- fixed (char* buffer = &MemoryMarshal.GetReference(destination))
- {
- char* p = buffer + bufferLength;
- if (digits <= 1)
+ charsWritten = bufferLength;
+ fixed (char* buffer = &MemoryMarshal.GetReference(destination))
{
- p = UInt128ToDecChars(p, value);
+ char* p = buffer + bufferLength;
+ p = digits > countedDigits ?
+ UInt128ToDecChars(p, value, digits) :
+ UInt128ToDecChars(p, value);
+ Debug.Assert(p == buffer);
}
- else
- {
- p = UInt128ToDecChars(p, value, digits);
- }
- Debug.Assert(p == buffer);
+ return true;
}
- return true;
+
+ charsWritten = 0;
+ return false;
}
internal static unsafe char ParseFormatSpecifier(ReadOnlySpan<char> format, out int digits)