From 9b780c96c0c09eae97e2845cd11ed8630a76484e Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 24 Apr 2023 18:02:54 -0400 Subject: [PATCH] Fix perf regressions in Utf8Formatter for integers 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` 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). --- .../src/System/Buffers/StandardFormat.cs | 3 + .../Utf8Formatter/Utf8Formatter.Integer.cs | 155 ++++++++++++++++-- .../src/System/Number.Formatting.cs | 18 +- 3 files changed, 156 insertions(+), 20 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/StandardFormat.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/StandardFormat.cs index 91dca61b2415a3..9a738100d8b7b9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/StandardFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/StandardFormat.cs @@ -40,6 +40,9 @@ namespace System.Buffers /// public bool HasPrecision => _precision != NoPrecision; + /// Gets the precision if one was specified; otherwise, 0. + internal byte PrecisionOrZero => _precision != NoPrecision ? _precision : (byte)0; + /// /// true if the StandardFormat == default(StandardFormat) /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Integer.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Integer.cs index 303383a097e33b..0a55ad664ab270 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Integer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Integer.cs @@ -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 { /// @@ -30,7 +32,7 @@ public static partial class Utf8Formatter /// System.FormatException if the format is not valid for this data type. /// public static bool TryFormat(byte value, Span destination, out int bytesWritten, StandardFormat format = default) => - FormattingHelpers.TryFormat(value, destination, out bytesWritten, format); + TryFormat((uint)value, destination, out bytesWritten, format); /// /// Formats an SByte as a UTF8 string. @@ -55,7 +57,7 @@ public static bool TryFormat(byte value, Span destination, out int bytesWr /// [CLSCompliant(false)] public static bool TryFormat(sbyte value, Span destination, out int bytesWritten, StandardFormat format = default) => - FormattingHelpers.TryFormat(value, destination, out bytesWritten, format); + TryFormat(value, 0xFF, destination, out bytesWritten, format); /// /// Formats a Unt16 as a UTF8 string. @@ -80,7 +82,7 @@ public static bool TryFormat(sbyte value, Span destination, out int bytesW /// [CLSCompliant(false)] public static bool TryFormat(ushort value, Span destination, out int bytesWritten, StandardFormat format = default) => - FormattingHelpers.TryFormat(value, destination, out bytesWritten, format); + TryFormat((uint)value, destination, out bytesWritten, format); /// /// Formats an Int16 as a UTF8 string. @@ -104,7 +106,7 @@ public static bool TryFormat(ushort value, Span destination, out int bytes /// System.FormatException if the format is not valid for this data type. /// public static bool TryFormat(short value, Span destination, out int bytesWritten, StandardFormat format = default) => - FormattingHelpers.TryFormat(value, destination, out bytesWritten, format); + TryFormat(value, 0xFFFF, destination, out bytesWritten, format); /// /// Formats a UInt32 as a UTF8 string. @@ -127,9 +129,38 @@ public static bool TryFormat(short value, Span destination, out int bytesW /// /// System.FormatException if the format is not valid for this data type. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] [CLSCompliant(false)] - public static bool TryFormat(uint value, Span destination, out int bytesWritten, StandardFormat format = default) => - FormattingHelpers.TryFormat(value, destination, out bytesWritten, format); + public static bool TryFormat(uint value, Span 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'; + } + } /// /// Formats an Int32 as a UTF8 string. @@ -153,7 +184,43 @@ public static bool TryFormat(uint value, Span destination, out int bytesWr /// System.FormatException if the format is not valid for this data type. /// public static bool TryFormat(int value, Span 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 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'; + } + } /// /// Formats a UInt64 as a UTF8 string. @@ -176,9 +243,38 @@ public static bool TryFormat(int value, Span destination, out int bytesWri /// /// System.FormatException if the format is not valid for this data type. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] [CLSCompliant(false)] - public static bool TryFormat(ulong value, Span destination, out int bytesWritten, StandardFormat format = default) => - FormattingHelpers.TryFormat(value, destination, out bytesWritten, format); + public static bool TryFormat(ulong value, Span 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'; + } + } /// /// Formats an Int64 as a UTF8 string. @@ -201,7 +297,44 @@ public static bool TryFormat(ulong value, Span destination, out int bytesW /// /// System.FormatException if the format is not valid for this data type. /// - public static bool TryFormat(long value, Span destination, out int bytesWritten, StandardFormat format = default) => - FormattingHelpers.TryFormat(value, destination, out bytesWritten, format); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryFormat(long value, Span 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); } } 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 0a4d1ceaf65845..ed689b5d9ec774 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs @@ -907,7 +907,7 @@ private static bool TryCopyTo(string source, Span 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. @@ -1675,7 +1675,7 @@ private static unsafe string NegativeInt32ToDecStr(int value, int digits, string return result; } - private static unsafe bool TryNegativeInt32ToDecStr(int value, int digits, ReadOnlySpan sNegative, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static unsafe bool TryNegativeInt32ToDecStr(int value, int digits, ReadOnlySpan sNegative, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(value < 0); @@ -1724,7 +1724,7 @@ private static unsafe string Int32ToHexStr(int value, char hexBase, int digits) return result; } - private static unsafe bool TryInt32ToHexStr(int value, char hexBase, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static unsafe bool TryInt32ToHexStr(int value, char hexBase, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -1999,7 +1999,7 @@ private static unsafe string UInt32ToDecStr(uint value, int digits) return result; } - private static unsafe bool TryUInt32ToDecStr(uint value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static unsafe bool TryUInt32ToDecStr(uint value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2019,7 +2019,7 @@ private static unsafe bool TryUInt32ToDecStr(uint value, Span dest return false; } - private static unsafe bool TryUInt32ToDecStr(uint value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static unsafe bool TryUInt32ToDecStr(uint value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2108,7 +2108,7 @@ private static unsafe string NegativeInt64ToDecStr(long value, int digits, strin return result; } - private static unsafe bool TryNegativeInt64ToDecStr(long value, int digits, ReadOnlySpan sNegative, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static unsafe bool TryNegativeInt64ToDecStr(long value, int digits, ReadOnlySpan sNegative, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); Debug.Assert(value < 0); @@ -2157,7 +2157,7 @@ private static unsafe string Int64ToHexStr(long value, char hexBase, int digits) return result; } - private static unsafe bool TryInt64ToHexStr(long value, char hexBase, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static unsafe bool TryInt64ToHexStr(long value, char hexBase, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2427,7 +2427,7 @@ internal static unsafe string UInt64ToDecStr(ulong value, int digits) return result; } - private static unsafe bool TryUInt64ToDecStr(ulong value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static unsafe bool TryUInt64ToDecStr(ulong value, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte)); @@ -2448,7 +2448,7 @@ private static unsafe bool TryUInt64ToDecStr(ulong value, Span des return false; } - private static unsafe bool TryUInt64ToDecStr(ulong value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar + internal static unsafe bool TryUInt64ToDecStr(ulong value, int digits, Span destination, out int charsWritten) where TChar : unmanaged, IUtfChar { int countedDigits = FormattingHelpers.CountDigits(value); int bufferLength = Math.Max(digits, countedDigits);