-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Remove use of Unsafe in UTF-8 formatters, plus performance improvements #26289
Remove use of Unsafe in UTF-8 formatters, plus performance improvements #26289
Conversation
Also optimize code: on Win10 amd64 this results in +46% throughput (GUIDs formatted per second)
On Win10 amd64 this results in +82% throughput (bools formatted per second)
Does not significantly impact perf; measurements are within +/-5% on Win10 amd64 test box
Also optimize code: on Win10 amd64 this results in +25% to +35% throughput (DateTimes formatted per second) depending on format
Also add missing test case for DateTimeOffset formatter Also optimize code: on Win10 amd64 this results in +20% to +30% throughput (TimeSpans formatted per second) depending on magnitude of value
Also optimize code: on Win10 amd64 this results in +45% throughput (integers formatted per second) for signed integers formatted as D or N, +60% throughput for unsigned integers formatted as D or N
@@ -28,21 +28,21 @@ private static bool TryFormatUInt64(ulong value, Span<byte> buffer, out int byte | |||
case 'g': | |||
if (format.HasPrecision) | |||
throw new NotSupportedException(SR.Argument_GWithPrecisionNotSupported); // With a precision, 'G' can produce exponential format, even for integers. | |||
return TryFormatUInt64D(value, format.Precision, buffer, out bytesWritten); | |||
return TryFormatUInt64D(value, format.Precision, buffer, false /* insertNegationSign */, out bytesWritten); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
C# now allows name-tagging arguments without name-tagging every argument after it:
return TryFormatUInt64D(value, format.Precision, buffer, false /* insertNegationSign */, out bytesWritten);
==>
return TryFormatUInt64D(value, format.Precision, buffer, insertNegationSign: false, out bytesWritten);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Didn't know that - nifty! :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now addressed.
What are the code coverage numbers after this change? |
{ | ||
bytesWritten = 0; | ||
return false; | ||
const uint FalsValueUppercase = ('F' << 24) + ('a' << 16) + ('l' << 8) + ('s' << 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: "Fals" -> "False"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nvm - I see the "e" doesn't fit into a uint :-)
ThrowHelper.ThrowArgumentException_InvalidTypeWithPointersNotSupported(typeof(T)); | ||
} | ||
#endif | ||
return Span<byte>.DangerousCreate(null, ref Unsafe.As<T, byte>(ref value), Unsafe.SizeOf<T>()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not valid - it crash with slow Span.
It is not valid to pass null as first argument to DangerousCreate,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previous closed issue about this https://github.com/dotnet/corefx/issues/26124
This provides approximately equal (within noise) performance to the original code which turned a ref Guid into a Span<byte>
@atsushikan I don't have code coverage numbers available. I'm riding on top of the existing (quite comprehensive) unit tests for this functionality, plus I added a few extra test cases for situations where the original code produced incorrect results. |
@GrabYourPitchforks - You can get code coverage results by doing this:
The coverage numbers for Utf8Formatting were at a 100% when I checked in the original stuff - it's probably regressed some since then but it'd be good to know at least that the stuff you touched is at 100%. |
@dotnet-bot test Linux x64 Release Build please |
@atsushikan It has regressed slightly (89%), but those were changes separate from this PR. Everything looks good here from a code coverage perspective. :) |
else if (symbol == 'l') | ||
{ | ||
const uint TrueValueLowercase = ('t' << 24) + ('r' << 16) + ('u' << 8) + ('e' << 0); | ||
if (!BinaryPrimitives.TryWriteUInt32BigEndian(buffer, TrueValueLowercase)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would writing TryFormat using BinaryPrimitives still be necessary once TryCopyTo is optimized?
https://github.com/dotnet/coreclr/issues/15076
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | ||
for (int i = FractionDigits; i > digitCount; i--) | ||
value /= 10; | ||
// This is a faster implementation of Span<T>.Fill(). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this faster? Is it due to buffer.Length being too small in this case?
Wouldn't Fill using InitBlockUnaligned be just as fast, if not faster:
https://github.com/dotnet/corefx/blob/master/src/System.Memory/src/System/Span.cs#L219-L231
@@ -2,74 +2,54 @@ | |||
// The .NET Foundation licenses this file to you under the MIT license. | |||
// See the LICENSE file in the project root for more information. | |||
|
|||
using System.Diagnostics; | |||
using System.Runtime.CompilerServices; | |||
|
|||
#if !netstandard | |||
using Internal.Runtime.CompilerServices; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need this using directive?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed it here: #26598
…ts (dotnet/corefx#26289) Remove use of Unsafe in UTF-8 formatters, plus performance improvements Commit migrated from dotnet/corefx@534af93
Fixes https://github.com/dotnet/corefx/issues/25648.
My first attempt to switch the implementations from unsafe code to safe buffer-based code was a bit naive and resulted in considerable performance degradation (double-digit percentage loss of throughput across many APIs). I've spent time reworking the implementations to to work around the performance loss, and in many cases the new implementations are faster than the originals.
For reviewers: I recommend looking at each commit in isolation, as each commit deals with a very specific formatter. It'll also be easier to see which helper routines in
FormatterHelpers
correlate with which implementations. This also allows individual commits to be backed out without affecting the rest of the PR if reviewers deem a particular commit as unwanted.