Skip to content

Commit 759fabe

Browse files
authored
Fix perf regressions in Utf8Formatter for integers (#85277)
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).
1 parent 01e2455 commit 759fabe

File tree

3 files changed

+156
-20
lines changed

3 files changed

+156
-20
lines changed

src/libraries/System.Private.CoreLib/src/System/Buffers/StandardFormat.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ namespace System.Buffers
4040
/// </summary>
4141
public bool HasPrecision => _precision != NoPrecision;
4242

43+
/// <summary>Gets the precision if one was specified; otherwise, 0.</summary>
44+
internal byte PrecisionOrZero => _precision != NoPrecision ? _precision : (byte)0;
45+
4346
/// <summary>
4447
/// true if the StandardFormat == default(StandardFormat)
4548
/// </summary>

src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Integer.cs

Lines changed: 144 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Runtime.CompilerServices;
5+
46
namespace System.Buffers.Text
57
{
68
/// <summary>
@@ -30,7 +32,7 @@ public static partial class Utf8Formatter
3032
/// <cref>System.FormatException</cref> if the format is not valid for this data type.
3133
/// </exceptions>
3234
public static bool TryFormat(byte value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
33-
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
35+
TryFormat((uint)value, destination, out bytesWritten, format);
3436

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

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

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

109111
/// <summary>
110112
/// Formats a UInt32 as a UTF8 string.
@@ -127,9 +129,38 @@ public static bool TryFormat(short value, Span<byte> destination, out int bytesW
127129
/// <exceptions>
128130
/// <cref>System.FormatException</cref> if the format is not valid for this data type.
129131
/// </exceptions>
132+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
130133
[CLSCompliant(false)]
131-
public static bool TryFormat(uint value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
132-
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
134+
public static bool TryFormat(uint value, Span<byte> destination, out int bytesWritten, StandardFormat format = default)
135+
{
136+
if (format.IsDefault)
137+
{
138+
return Number.TryUInt32ToDecStr(value, destination, out bytesWritten);
139+
}
140+
141+
switch (format.Symbol | 0x20)
142+
{
143+
case 'd':
144+
return Number.TryUInt32ToDecStr(value, format.PrecisionOrZero, destination, out bytesWritten);
145+
146+
case 'x':
147+
return Number.TryInt32ToHexStr((int)value, Number.GetHexBase(format.Symbol), format.PrecisionOrZero, destination, out bytesWritten);
148+
149+
case 'n':
150+
return FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
151+
152+
case 'g' or 'r':
153+
if (format.HasPrecision)
154+
{
155+
ThrowGWithPrecisionNotSupported();
156+
}
157+
goto case 'd';
158+
159+
default:
160+
ThrowHelper.ThrowFormatException_BadFormatSpecifier();
161+
goto case 'd';
162+
}
163+
}
133164

134165
/// <summary>
135166
/// Formats an Int32 as a UTF8 string.
@@ -153,7 +184,43 @@ public static bool TryFormat(uint value, Span<byte> destination, out int bytesWr
153184
/// <cref>System.FormatException</cref> if the format is not valid for this data type.
154185
/// </exceptions>
155186
public static bool TryFormat(int value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
156-
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
187+
TryFormat(value, ~0, destination, out bytesWritten, format);
188+
189+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
190+
private static bool TryFormat(int value, int hexMask, Span<byte> destination, out int bytesWritten, StandardFormat format = default)
191+
{
192+
if (format.IsDefault)
193+
{
194+
return value >= 0 ?
195+
Number.TryUInt32ToDecStr((uint)value, destination, out bytesWritten) :
196+
Number.TryNegativeInt32ToDecStr(value, format.PrecisionOrZero, "-"u8, destination, out bytesWritten);
197+
}
198+
199+
switch (format.Symbol | 0x20)
200+
{
201+
case 'd':
202+
return value >= 0 ?
203+
Number.TryUInt32ToDecStr((uint)value, format.PrecisionOrZero, destination, out bytesWritten) :
204+
Number.TryNegativeInt32ToDecStr(value, format.PrecisionOrZero, "-"u8, destination, out bytesWritten);
205+
206+
case 'x':
207+
return Number.TryInt32ToHexStr(value & hexMask, Number.GetHexBase(format.Symbol), format.PrecisionOrZero, destination, out bytesWritten);
208+
209+
case 'n':
210+
return FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
211+
212+
case 'g' or 'r':
213+
if (format.HasPrecision)
214+
{
215+
ThrowGWithPrecisionNotSupported();
216+
}
217+
goto case 'd';
218+
219+
default:
220+
ThrowHelper.ThrowFormatException_BadFormatSpecifier();
221+
goto case 'd';
222+
}
223+
}
157224

158225
/// <summary>
159226
/// Formats a UInt64 as a UTF8 string.
@@ -176,9 +243,38 @@ public static bool TryFormat(int value, Span<byte> destination, out int bytesWri
176243
/// <exceptions>
177244
/// <cref>System.FormatException</cref> if the format is not valid for this data type.
178245
/// </exceptions>
246+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
179247
[CLSCompliant(false)]
180-
public static bool TryFormat(ulong value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
181-
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
248+
public static bool TryFormat(ulong value, Span<byte> destination, out int bytesWritten, StandardFormat format = default)
249+
{
250+
if (format.IsDefault)
251+
{
252+
return Number.TryUInt64ToDecStr(value, destination, out bytesWritten);
253+
}
254+
255+
switch (format.Symbol | 0x20)
256+
{
257+
case 'd':
258+
return Number.TryUInt64ToDecStr(value, format.PrecisionOrZero, destination, out bytesWritten);
259+
260+
case 'x':
261+
return Number.TryInt64ToHexStr((long)value, Number.GetHexBase(format.Symbol), format.PrecisionOrZero, destination, out bytesWritten);
262+
263+
case 'n':
264+
return FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
265+
266+
case 'g' or 'r':
267+
if (format.HasPrecision)
268+
{
269+
ThrowGWithPrecisionNotSupported();
270+
}
271+
goto case 'd';
272+
273+
default:
274+
ThrowHelper.ThrowFormatException_BadFormatSpecifier();
275+
goto case 'd';
276+
}
277+
}
182278

183279
/// <summary>
184280
/// Formats an Int64 as a UTF8 string.
@@ -201,7 +297,44 @@ public static bool TryFormat(ulong value, Span<byte> destination, out int bytesW
201297
/// <exceptions>
202298
/// <cref>System.FormatException</cref> if the format is not valid for this data type.
203299
/// </exceptions>
204-
public static bool TryFormat(long value, Span<byte> destination, out int bytesWritten, StandardFormat format = default) =>
205-
FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
300+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
301+
public static bool TryFormat(long value, Span<byte> destination, out int bytesWritten, StandardFormat format = default)
302+
{
303+
if (format.IsDefault)
304+
{
305+
return value >= 0 ?
306+
Number.TryUInt64ToDecStr((ulong)value, destination, out bytesWritten) :
307+
Number.TryNegativeInt64ToDecStr(value, format.PrecisionOrZero, "-"u8, destination, out bytesWritten);
308+
}
309+
310+
switch (format.Symbol | 0x20)
311+
{
312+
case 'd':
313+
return value >= 0 ?
314+
Number.TryUInt64ToDecStr((ulong)value, format.PrecisionOrZero, destination, out bytesWritten) :
315+
Number.TryNegativeInt64ToDecStr(value, format.PrecisionOrZero, "-"u8, destination, out bytesWritten);
316+
317+
case 'x':
318+
return Number.TryInt64ToHexStr(value, Number.GetHexBase(format.Symbol), format.PrecisionOrZero, destination, out bytesWritten);
319+
320+
case 'n':
321+
return FormattingHelpers.TryFormat(value, destination, out bytesWritten, format);
322+
323+
case 'g' or 'r':
324+
if (format.HasPrecision)
325+
{
326+
ThrowGWithPrecisionNotSupported();
327+
}
328+
goto case 'd';
329+
330+
default:
331+
ThrowHelper.ThrowFormatException_BadFormatSpecifier();
332+
goto case 'd';
333+
}
334+
}
335+
336+
private static void ThrowGWithPrecisionNotSupported() =>
337+
// With a precision, 'G' can produce exponential format, even for integers.
338+
throw new NotSupportedException(SR.Argument_GWithPrecisionNotSupported);
206339
}
207340
}

src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ private static bool TryCopyTo<TChar>(string source, Span<TChar> destination, out
907907
}
908908
}
909909

910-
private static char GetHexBase(char fmt)
910+
internal static char GetHexBase(char fmt)
911911
{
912912
// The fmt-(X-A+10) hack has the effect of dictating whether we produce uppercase or lowercase
913913
// 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
16751675
return result;
16761676
}
16771677

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

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

@@ -1999,7 +1999,7 @@ private static unsafe string UInt32ToDecStr(uint value, int digits)
19991999
return result;
20002000
}
20012001

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

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

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

@@ -2108,7 +2108,7 @@ private static unsafe string NegativeInt64ToDecStr(long value, int digits, strin
21082108
return result;
21092109
}
21102110

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

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

@@ -2427,7 +2427,7 @@ internal static unsafe string UInt64ToDecStr(ulong value, int digits)
24272427
return result;
24282428
}
24292429

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

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

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

0 commit comments

Comments
 (0)