Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Remove use of Unsafe in UTF-8 formatters, plus performance improvements #26289

Merged

Conversation

GrabYourPitchforks
Copy link
Member

@GrabYourPitchforks GrabYourPitchforks commented Jan 12, 2018

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.

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);
Copy link

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);

Copy link
Member Author

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! :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now addressed.

@ghost
Copy link

ghost commented Jan 12, 2018

What are the code coverage numbers after this change?

{
bytesWritten = 0;
return false;
const uint FalsValueUppercase = ('F' << 24) + ('a' << 16) + ('l' << 8) + ('s' << 0);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "Fals" -> "False"

Copy link

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>());
Copy link
Member

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,

Copy link
Member

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>
@GrabYourPitchforks
Copy link
Member Author

@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.

@ghost
Copy link

ghost commented Jan 16, 2018

@GrabYourPitchforks - You can get code coverage results by doing this:

   cd <repo>\src\System.Memory
   msbuild /t:build /p:DebugType=pdbonly
   cd <repo>\src\System.Memory\tests

   msbuild -t:clean;build;test -p:Coverage=true
   start <repo>\bin\tests\coverage\index.htm

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%.

@MattGal
Copy link
Member

MattGal commented Jan 16, 2018

@dotnet-bot test Linux x64 Release Build please
@dotnet-bot test OSX x64 Debug Build please
@dotnet-bot test UWP CoreCLR x64 Debug Build please
@dotnet-bot test Windows x64 Debug Build please
@dotnet-bot test Windows x86 Release Build please

@GrabYourPitchforks
Copy link
Member Author

@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))
Copy link
Member

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

Copy link
Member

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().
Copy link
Member

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;
Copy link
Member

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it here: #26598

@karelz karelz added this to the 2.1.0 milestone Jan 20, 2018
@GrabYourPitchforks GrabYourPitchforks deleted the levib/remove_unsafe_4 branch January 29, 2018 02:27
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…ts (dotnet/corefx#26289)

Remove use of Unsafe in UTF-8 formatters, plus performance improvements

Commit migrated from dotnet/corefx@534af93
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
6 participants