Skip to content

Commit

Permalink
Implement IUtf8SpanFormattable on all the numeric types in corelib (#…
Browse files Browse the repository at this point in the history
…84587)

* Implement IUtf8SpanFormattable on all the numeric types in corelib

Augments SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Int128, UInt128, Half, Single, Double, NFloat, and Decimal.

Also fixes corelib TODOs around using IUtf8SpanFormattable.

And removes some duplicate code from Utf8Formatter.

There is still more consolidation to be done between FormattingHelpers and Number.Formatting.

* Address regressions from previous formatting changes

- Use an internal interface implemented by char and byte to have dedicated CastFrom methods that are always inlineable due to very small size.
- Use pointers in some core formatting routines to avoid needing bulky IL for manipulating refs with spans, making various members more inlineable.
- Avoid Encoding.UTF8.GetBytes in various code paths by caching more UTF8 sequences on DateTimeFormatInfo and NumberFormatInfo
- Change FormatCustomizedTimeZone to special-case 2 vs 3+ tokens in order to avoid extra AppendSpan calls
- Fix growth logic in ValueListBuilder to not forcibly grow more than is needed
- Inline ValueListBuilder.AppendSpan and remove some bounds checks (at least on 64-bit)
- Change FormatDigits to special-case lengths of 1/2/4 and to use existing formatting routines rather than a custom one
- Remove the FormatDigits wrapper overload and just have all calls go to the main workhorse method.
- Remove the use of "..."u8 in R/O formatting that leads to needing to use additional span-based helpers.  The minimal gain on coreclr isn't worth the extra complication
- Changed some switches to include half the cases based on lowercasing the ASCII input char
- Moved Date/TimeOnly charsWritten into Try method to be closer to the source of truth rather than having the value far aware (this isn't for perf and could possibly even be a microregression, so I included it here to ensure it's not measurable).

* Remove mono ifdef in WriteTwo/FourDigits
  • Loading branch information
stephentoub authored Apr 14, 2023
1 parent 3d97f41 commit ed09ae5
Show file tree
Hide file tree
Showing 77 changed files with 1,967 additions and 3,024 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,6 @@
<ItemGroup>
<Compile Include="ParsersAndFormatters\Formatter\FormatterTestData.cs" />
<Compile Include="ParsersAndFormatters\Formatter\FormatterTests.cs" />
<Compile Include="ParsersAndFormatters\Formatter\FormatterTests.Negative.cs" />
<Compile Include="ParsersAndFormatters\Formatter\TestData.Formatter.cs" />
<Compile Include="ParsersAndFormatters\Formatter\ValidateFormatter.cs" />
<Compile Include="ParsersAndFormatters\Parser\ParserTestData.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,21 +131,9 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Date.G.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Date.L.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Decimal.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Decimal.E.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Decimal.F.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Decimal.G.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Float.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Guid.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Integer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Integer.Signed.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Integer.Signed.D.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Integer.Signed.Default.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Integer.Signed.N.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Integer.Unsigned.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Integer.Unsigned.D.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Integer.Unsigned.Default.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Integer.Unsigned.N.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.Integer.Unsigned.X.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Formatter\Utf8Formatter.TimeSpan.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Parser\ParserHelpers.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Buffers\Text\Utf8Parser\Utf8Parser.Boolean.cs" />
Expand Down Expand Up @@ -516,6 +504,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\IObserver.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IProgress.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\ISpanFormattable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IUtfChar.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IUtf8SpanFormattable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Lazy.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\LazyOfTTMetadata.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ namespace System.Buffers
/// <summary>
/// true if the StandardFormat == default(StandardFormat)
/// </summary>
public bool IsDefault => _format == 0 && _precision == 0;
public bool IsDefault => (_format | _precision) == 0;

/// <summary>
/// Create a StandardFormat.
Expand Down Expand Up @@ -144,30 +144,23 @@ private static bool ParseHelper(ReadOnlySpan<char> format, out StandardFormat st
/// <summary>
/// Returns the format in classic .NET format.
/// </summary>
public override string ToString()
{
Span<char> buffer = stackalloc char[FormatStringLength];
int charsWritten = Format(buffer);
return new string(buffer.Slice(0, charsWritten));
}
public override string ToString() => new string(Format(stackalloc char[FormatStringLength]));

/// <summary>The exact buffer length required by <see cref="Format"/>.</summary>
internal const int FormatStringLength = 3;

/// <summary>
/// Formats the format in classic .NET format.
/// </summary>
internal int Format(Span<char> destination)
internal Span<char> Format(Span<char> destination)
{
Debug.Assert(destination.Length == FormatStringLength);

int count = 0;
char symbol = Symbol;

if (symbol != default && destination.Length == FormatStringLength)
{
destination[0] = symbol;
count = 1;

uint precision = Precision;
if (precision != NoPrecision)
Expand All @@ -185,15 +178,18 @@ internal int Format(Span<char> destination)
uint div;
(div, precision) = Math.DivRem(precision, 10);
destination[1] = (char)('0' + div % 10);
count = 2;
destination[2] = (char)('0' + precision);
return destination;
}

destination[count] = (char)('0' + precision);
count++;
destination[1] = (char)('0' + precision);
return destination.Slice(0, 2);
}

return destination.Slice(0, 1);
}

return count;
return default;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ internal static partial class Utf8Constants
public const byte Space = (byte)' ';
public const byte Hyphen = (byte)'-';

public const byte Separator = (byte)',';

// Invariant formatting uses groups of 3 for each number group separated by commas.
// ex. 1,234,567,890
public const int GroupSize = 3;
Expand All @@ -26,8 +24,5 @@ internal static partial class Utf8Constants

public const int DateTimeNumFractionDigits = 7; // TimeSpan and DateTime formats allow exactly up to many digits for specifying the fraction after the seconds.
public const int MaxDateTimeFraction = 9999999; // ... and hence, the largest fraction expressible is this.

public const ulong BillionMaxUIntValue = (ulong)uint.MaxValue * Billion; // maximum value that can be split into two uint32 {1-10 digits}{9 digits}
public const uint Billion = 1000000000; // 10^9, used to split int64/uint64 into three uint32 {1-2 digits}{9 digits}{9 digits}
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
// 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.Numerics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System.Buffers.Text
{
// All the helper methods in this class assume that the by-ref is valid and that there is
// enough space to fit the items that will be written into the underlying memory. The calling
// code must have already done all the necessary validation.
internal static partial class FormattingHelpers
{
public static bool TryFormat<T>(T value, Span<byte> utf8Destination, out int bytesWritten, StandardFormat format) where T : IUtf8SpanFormattable
{
scoped Span<char> formatText = default;
if (!format.IsDefault)
{
formatText = format.Format(stackalloc char[StandardFormat.FormatStringLength]);
}

return value.TryFormat(utf8Destination, out bytesWritten, formatText, CultureInfo.InvariantCulture);
}

/// <summary>
/// Returns the symbol contained within the standard format. If the standard format
/// has not been initialized, returns the provided fallback symbol.
Expand All @@ -32,138 +38,5 @@ public static char GetSymbolOrDefault(in StandardFormat format, char defaultSymb
}
return symbol;
}

/// <summary>
/// Fills a buffer with the ASCII character '0' (0x30).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void FillWithAsciiZeros(Span<byte> buffer)
{
// This is a faster implementation of Span<T>.Fill() for very short buffers.
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = (byte)'0';
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteDigits<TChar>(ulong value, Span<TChar> buffer) where TChar : unmanaged, IBinaryInteger<TChar>
{
// We can mutate the 'value' parameter since it's a copy-by-value local.
// It'll be used to represent the value left over after each division by 10.

for (int i = buffer.Length - 1; i >= 1; i--)
{
ulong temp = '0' + value;
value /= 10;
buffer[i] = TChar.CreateTruncating(temp - (value * 10));
}

Debug.Assert(value < 10);
buffer[0] = TChar.CreateTruncating('0' + value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteDigitsWithGroupSeparator(ulong value, Span<byte> buffer)
{
// We can mutate the 'value' parameter since it's a copy-by-value local.
// It'll be used to represent the value left over after each division by 10.

int digitsWritten = 0;
for (int i = buffer.Length - 1; i >= 1; i--)
{
ulong temp = '0' + value;
value /= 10;
buffer[i] = (byte)(temp - (value * 10));
if (digitsWritten == Utf8Constants.GroupSize - 1)
{
buffer[--i] = Utf8Constants.Comma;
digitsWritten = 0;
}
else
{
digitsWritten++;
}
}

Debug.Assert(value < 10);
buffer[0] = (byte)('0' + value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void WriteDigits<TChar>(uint value, Span<TChar> buffer) where TChar : unmanaged, IBinaryInteger<TChar>
{
Debug.Assert(buffer.Length > 0);

for (int i = buffer.Length - 1; i >= 1; i--)
{
uint temp = '0' + value;
value /= 10;
buffer[i] = TChar.CreateTruncating(temp - (value * 10));
}

Debug.Assert(value < 10);
buffer[0] = TChar.CreateTruncating('0' + value);
}

/// <summary>
/// Writes a value [ 00 .. 99 ] to the buffer starting at the specified offset.
/// This method performs best when the starting index is a constant literal.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe void WriteTwoDigits<TChar>(uint value, Span<TChar> buffer, int startingIndex = 0) where TChar : unmanaged, IBinaryInteger<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
Debug.Assert(value <= 99);
Debug.Assert(startingIndex <= buffer.Length - 2);

fixed (TChar* bufferPtr = &MemoryMarshal.GetReference(buffer))
{
Number.WriteTwoDigits(bufferPtr + startingIndex, value);
}
}

/// <summary>
/// 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.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe void WriteFourDigits<TChar>(uint value, Span<TChar> buffer, int startingIndex = 0) where TChar : unmanaged, IBinaryInteger<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
Debug.Assert(value <= 9999);
Debug.Assert(startingIndex <= buffer.Length - 4);

(value, uint remainder) = Math.DivRem(value, 100);
fixed (TChar* bufferPtr = &MemoryMarshal.GetReference(buffer))
{
Number.WriteTwoDigits(bufferPtr + startingIndex, value);
Number.WriteTwoDigits(bufferPtr + startingIndex + 2, remainder);
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void CopyFour<TChar>(ReadOnlySpan<TChar> source, Span<TChar> destination) where TChar : unmanaged, IBinaryInteger<TChar>
{
if (typeof(TChar) == typeof(byte))
{
Unsafe.WriteUnaligned(ref Unsafe.As<TChar, byte>(ref MemoryMarshal.GetReference(destination)),
Unsafe.ReadUnaligned<uint>(ref Unsafe.As<TChar, byte>(ref MemoryMarshal.GetReference(source))));
}
else
{
Debug.Assert(typeof(TChar) == typeof(char));
Unsafe.WriteUnaligned(ref Unsafe.As<TChar, byte>(ref MemoryMarshal.GetReference(destination)),
Unsafe.ReadUnaligned<ulong>(ref Unsafe.As<TChar, byte>(ref MemoryMarshal.GetReference(source))));
}
}

/// <summary>Enable use of ThrowHelper from TryFormat() routines without introducing dozens of non-code-coveraged "bytesWritten = 0; return false" boilerplate.</summary>
public static bool TryFormatThrowFormatException(out int bytesWritten)
{
bytesWritten = 0;
ThrowHelper.ThrowFormatException_BadFormatSpecifier();
return false;
}
}
}
Loading

0 comments on commit ed09ae5

Please sign in to comment.