Skip to content

Commit

Permalink
Fix perf regressions in Utf8Formatter for integers (#85277)
Browse files Browse the repository at this point in the history
When I added UTF8 support to the core numeric types, I also just routed Utf8Formatter to use the public TryFormat API on each type.  That, however, regressed some microbenchmarks due to a) going from `StandardFormat` to a `ReadOnlySpan<char>` format and then parsing it back out and b) removing some of the inlining that was there previously.  This change puts back into Utf8Formatter.TryFormat the handling of the format and then delegating to the relevant helpers that already exist rather than always going through the public entrypoint (it doesn't do so for 'n', but that's also much rarer to use on a hot path and is also in general more expensive).
  • Loading branch information
stephentoub authored Apr 25, 2023
1 parent 01e2455 commit 759fabe
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ namespace System.Buffers
/// </summary>
public bool HasPrecision => _precision != NoPrecision;

/// <summary>Gets the precision if one was specified; otherwise, 0.</summary>
internal byte PrecisionOrZero => _precision != NoPrecision ? _precision : (byte)0;

/// <summary>
/// true if the StandardFormat == default(StandardFormat)
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;

namespace System.Buffers.Text
{
/// <summary>
Expand Down Expand Up @@ -30,7 +32,7 @@ public static partial class Utf8Formatter
/// <cref>System.FormatException</cref> if the format is not valid for this data type.
/// </exceptions>
public static bool TryFormat(byte value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
TryFormat((uint)value, destination, out bytesWritten, format);

/// <summary>
/// Formats an SByte as a UTF8 string.
Expand All @@ -55,7 +57,7 @@ public static bool TryFormat(byte value, Span<byte> destination, out int bytesWr
/// </exceptions>
[CLSCompliant(false)]
public static bool TryFormat(sbyte value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
TryFormat(value, 0xFF, destination, out bytesWritten, format);

/// <summary>
/// Formats a Unt16 as a UTF8 string.
Expand All @@ -80,7 +82,7 @@ public static bool TryFormat(sbyte value, Span<byte> destination, out int bytesW
/// </exceptions>
[CLSCompliant(false)]
public static bool TryFormat(ushort value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
TryFormat((uint)value, destination, out bytesWritten, format);

/// <summary>
/// Formats an Int16 as a UTF8 string.
Expand All @@ -104,7 +106,7 @@ public static bool TryFormat(ushort value, Span<byte> destination, out int bytes
/// <cref>System.FormatException</cref> if the format is not valid for this data type.
/// </exceptions>
public static bool TryFormat(short value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
TryFormat(value, 0xFFFF, destination, out bytesWritten, format);

/// <summary>
/// Formats a UInt32 as a UTF8 string.
Expand All @@ -127,9 +129,38 @@ public static bool TryFormat(short value, Span<byte> destination, out int bytesW
/// <exceptions>
/// <cref>System.FormatException</cref> if the format is not valid for this data type.
/// </exceptions>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[CLSCompliant(false)]
public static bool TryFormat(uint value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
public static bool TryFormat(uint value, Span<byte> destination, out int bytesWritten, StandardFormat format = default)
{
if (format.IsDefault)
{
return Number.TryUInt32ToDecStr(value, destination, out bytesWritten);
}

switch (format.Symbol | 0x20)
{
case 'd':
return Number.TryUInt32ToDecStr(value, format.PrecisionOrZero, destination, out bytesWritten);

case 'x':
return Number.TryInt32ToHexStr((int)value, Number.GetHexBase(format.Symbol), format.PrecisionOrZero, destination, out bytesWritten);

case 'n':
return FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);

case 'g' or 'r':
if (format.HasPrecision)
{
ThrowGWithPrecisionNotSupported();
}
goto case 'd';

default:
ThrowHelper.ThrowFormatException_BadFormatSpecifier();
goto case 'd';
}
}

/// <summary>
/// Formats an Int32 as a UTF8 string.
Expand All @@ -153,7 +184,43 @@ public static bool TryFormat(uint value, Span<byte> destination, out int bytesWr
/// <cref>System.FormatException</cref> if the format is not valid for this data type.
/// </exceptions>
public static bool TryFormat(int value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
TryFormat(value, ~0, destination, out bytesWritten, format);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool TryFormat(int value, int hexMask, Span<byte> destination, out int bytesWritten, StandardFormat format = default)
{
if (format.IsDefault)
{
return value >= 0 ?
Number.TryUInt32ToDecStr((uint)value, destination, out bytesWritten) :
Number.TryNegativeInt32ToDecStr(value, format.PrecisionOrZero, "-"u8, destination, out bytesWritten);
}

switch (format.Symbol | 0x20)
{
case 'd':
return value >= 0 ?
Number.TryUInt32ToDecStr((uint)value, format.PrecisionOrZero, destination, out bytesWritten) :
Number.TryNegativeInt32ToDecStr(value, format.PrecisionOrZero, "-"u8, destination, out bytesWritten);

case 'x':
return Number.TryInt32ToHexStr(value & hexMask, Number.GetHexBase(format.Symbol), format.PrecisionOrZero, destination, out bytesWritten);

case 'n':
return FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);

case 'g' or 'r':
if (format.HasPrecision)
{
ThrowGWithPrecisionNotSupported();
}
goto case 'd';

default:
ThrowHelper.ThrowFormatException_BadFormatSpecifier();
goto case 'd';
}
}

/// <summary>
/// Formats a UInt64 as a UTF8 string.
Expand All @@ -176,9 +243,38 @@ public static bool TryFormat(int value, Span<byte> destination, out int bytesWri
/// <exceptions>
/// <cref>System.FormatException</cref> if the format is not valid for this data type.
/// </exceptions>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[CLSCompliant(false)]
public static bool TryFormat(ulong value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
public static bool TryFormat(ulong value, Span<byte> destination, out int bytesWritten, StandardFormat format = default)
{
if (format.IsDefault)
{
return Number.TryUInt64ToDecStr(value, destination, out bytesWritten);
}

switch (format.Symbol | 0x20)
{
case 'd':
return Number.TryUInt64ToDecStr(value, format.PrecisionOrZero, destination, out bytesWritten);

case 'x':
return Number.TryInt64ToHexStr((long)value, Number.GetHexBase(format.Symbol), format.PrecisionOrZero, destination, out bytesWritten);

case 'n':
return FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);

case 'g' or 'r':
if (format.HasPrecision)
{
ThrowGWithPrecisionNotSupported();
}
goto case 'd';

default:
ThrowHelper.ThrowFormatException_BadFormatSpecifier();
goto case 'd';
}
}

/// <summary>
/// Formats an Int64 as a UTF8 string.
Expand All @@ -201,7 +297,44 @@ public static bool TryFormat(ulong value, Span<byte> destination, out int bytesW
/// <exceptions>
/// <cref>System.FormatException</cref> if the format is not valid for this data type.
/// </exceptions>
public static bool TryFormat(long value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryFormat(long value, Span<byte> destination, out int bytesWritten, StandardFormat format = default)
{
if (format.IsDefault)
{
return value >= 0 ?
Number.TryUInt64ToDecStr((ulong)value, destination, out bytesWritten) :
Number.TryNegativeInt64ToDecStr(value, format.PrecisionOrZero, "-"u8, destination, out bytesWritten);
}

switch (format.Symbol | 0x20)
{
case 'd':
return value >= 0 ?
Number.TryUInt64ToDecStr((ulong)value, format.PrecisionOrZero, destination, out bytesWritten) :
Number.TryNegativeInt64ToDecStr(value, format.PrecisionOrZero, "-"u8, destination, out bytesWritten);

case 'x':
return Number.TryInt64ToHexStr(value, Number.GetHexBase(format.Symbol), format.PrecisionOrZero, destination, out bytesWritten);

case 'n':
return FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);

case 'g' or 'r':
if (format.HasPrecision)
{
ThrowGWithPrecisionNotSupported();
}
goto case 'd';

default:
ThrowHelper.ThrowFormatException_BadFormatSpecifier();
goto case 'd';
}
}

private static void ThrowGWithPrecisionNotSupported() =>
// With a precision, 'G' can produce exponential format, even for integers.
throw new NotSupportedException(SR.Argument_GWithPrecisionNotSupported);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -907,7 +907,7 @@ private static bool TryCopyTo<TChar>(string source, Span<TChar> destination, out
}
}

private static char GetHexBase(char fmt)
internal static char GetHexBase(char fmt)
{
// The fmt-(X-A+10) hack has the effect of dictating whether we produce uppercase or lowercase
// hex numbers for a-f. 'X' as the fmt code produces uppercase. 'x' as the format code produces lowercase.
Expand Down Expand Up @@ -1675,7 +1675,7 @@ private static unsafe string NegativeInt32ToDecStr(int value, int digits, string
return result;
}

private static unsafe bool TryNegativeInt32ToDecStr<TChar>(int value, int digits, ReadOnlySpan<TChar> sNegative, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
internal static unsafe bool TryNegativeInt32ToDecStr<TChar>(int value, int digits, ReadOnlySpan<TChar> sNegative, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
Debug.Assert(value < 0);
Expand Down Expand Up @@ -1724,7 +1724,7 @@ private static unsafe string Int32ToHexStr(int value, char hexBase, int digits)
return result;
}

private static unsafe bool TryInt32ToHexStr<TChar>(int value, char hexBase, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
internal static unsafe bool TryInt32ToHexStr<TChar>(int value, char hexBase, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

Expand Down Expand Up @@ -1999,7 +1999,7 @@ private static unsafe string UInt32ToDecStr(uint value, int digits)
return result;
}

private static unsafe bool TryUInt32ToDecStr<TChar>(uint value, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
internal static unsafe bool TryUInt32ToDecStr<TChar>(uint value, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

Expand All @@ -2019,7 +2019,7 @@ private static unsafe bool TryUInt32ToDecStr<TChar>(uint value, Span<TChar> dest
return false;
}

private static unsafe bool TryUInt32ToDecStr<TChar>(uint value, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
internal static unsafe bool TryUInt32ToDecStr<TChar>(uint value, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

Expand Down Expand Up @@ -2108,7 +2108,7 @@ private static unsafe string NegativeInt64ToDecStr(long value, int digits, strin
return result;
}

private static unsafe bool TryNegativeInt64ToDecStr<TChar>(long value, int digits, ReadOnlySpan<TChar> sNegative, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
internal static unsafe bool TryNegativeInt64ToDecStr<TChar>(long value, int digits, ReadOnlySpan<TChar> sNegative, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
Debug.Assert(value < 0);
Expand Down Expand Up @@ -2157,7 +2157,7 @@ private static unsafe string Int64ToHexStr(long value, char hexBase, int digits)
return result;
}

private static unsafe bool TryInt64ToHexStr<TChar>(long value, char hexBase, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
internal static unsafe bool TryInt64ToHexStr<TChar>(long value, char hexBase, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

Expand Down Expand Up @@ -2427,7 +2427,7 @@ internal static unsafe string UInt64ToDecStr(ulong value, int digits)
return result;
}

private static unsafe bool TryUInt64ToDecStr<TChar>(ulong value, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
internal static unsafe bool TryUInt64ToDecStr<TChar>(ulong value, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
{
Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));

Expand All @@ -2448,7 +2448,7 @@ private static unsafe bool TryUInt64ToDecStr<TChar>(ulong value, Span<TChar> des
return false;
}

private static unsafe bool TryUInt64ToDecStr<TChar>(ulong value, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
internal static unsafe bool TryUInt64ToDecStr<TChar>(ulong value, int digits, Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
{
int countedDigits = FormattingHelpers.CountDigits(value);
int bufferLength = Math.Max(digits, countedDigits);
Expand Down

0 comments on commit 759fabe

Please sign in to comment.