Skip to content

Optimize '[Guid]' parsing on Native AOT #116324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 22, 2025

Conversation

Sergio0694
Copy link
Contributor

We're going to need to use [Guid] in some scenarios in CsWinRT 3.0 (see #114179), and while looking at the Type.GUID logic in Native AOT I saw some potential small perf improvements we could do. Figured I'd give it a shot and see if it's well received 😄

@Copilot Copilot AI review requested due to automatic review settings June 4, 2025 22:08
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Jun 4, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR aims to optimize the parsing of [Guid] in Native AOT by improving the underlying UTF8 string decoding and Guid parsing logic. Key changes include:

  • Adding a new DecodeStringUtf8 method in NativeFormatReader.String.cs to efficiently decode UTF8 strings.
  • Introducing a GetRawStringDataUtf8 helper in ConstantStringValue to leverage the new decoding method.
  • Refactoring Guid parsing in NativeFormatRuntimeNamedTypeInfo.cs to use a static local method that processes UTF8 data for performance gains.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/coreclr/tools/Common/Internal/NativeFormat/NativeFormatReader.String.cs Added method for decoding UTF8 strings with boundary checks and copying to a destination span
src/coreclr/tools/Common/Internal/Metadata/NativeFormat/NativeFormatReaderGen.cs Added a helper that uses the new decode method to extract UTF8 encoded string data
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs Replaced direct Guid creation with a static local method that decodes and parses UTF8 data for Guid construction
Comments suppressed due to low confidence (1)

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/Runtime/TypeInfos/NativeFormat/NativeFormatRuntimeNamedTypeInfo.cs:88

  • [nitpick] Consider using a parameter name in the exception call that more clearly reflects the null context of the handle value rather than 'utf8Text', which might be misleading.
ArgumentNullException.Throw("utf8Text");

Copy link
Contributor

Tagging subscribers to this area: @agocke, @MichalStrehovsky, @jkotas
See info in area-owners.md if you want to be subscribed.

@jkotas
Copy link
Member

jkotas commented Jun 4, 2025

Any perf numbers?

@Sergio0694

This comment was marked as outdated.

@@ -63,6 +64,32 @@ public unsafe uint DecodeString(uint offset, out string value)
return endOffset;
}

public unsafe Guid ParseGuid(uint offset)
{
#if NETFX_45
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 dead ifdef. You can delete it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's another one in the method above, should I remove that one too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mmh if I remove it, building fails with this at the Guid.Parse call:

D:\git\runtime\src\coreclr\tools\Common\Internal\NativeFormat\NativeFormatReader.String.cs(75,31): error CS1503: Argume
nt 1: cannot convert from 'System.ReadOnlySpan<byte>' to 'System.ReadOnlySpan<char>' [D:\git\runtime\src\coreclr\tools\
aot\ILCompiler.MetadataTransform\ILCompiler.MetadataTransform.csproj]

Copy link
Member

Choose a reason for hiding this comment

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

Hmm, that's because we have not picked up #105654 in the LKG SDK yet.

It should be fixed by https://github.com/dotnet/runtime/pull/116328/files#diff-8df3cd354bc584349d04ad5675b33c042d8b99b741b8b95af394c55e0f5001bfR3

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems like it's fine now after doing the changes Michal suggested. I removed that ifdef 👍

Copy link
Member

Choose a reason for hiding this comment

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

Could you please delete the ifdef (and the define in .csproj) while you are on in, even though it is not strictly required for the rest?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup, of course, but just to clarify, I need to wait for that PR you linked to be merged first, and then to update this branch, right?

Copy link
Member

Choose a reason for hiding this comment

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

I do not think you need to wait for that PR. The earlier build break was caused by the new code that you have introduced. The existing code should compile just fine with the ifdef deleted.

{
// We don't really have a parameter, so just match the name of the 'Guid.ctor' parameter
#if SYSTEM_PRIVATE_CORELIB
ArgumentNullException.Throw("input");
Copy link
Member

Choose a reason for hiding this comment

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

Where was this exception thrown before?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By the Guid constructor I think. The code was getting a ConstantStringValue value, which immediatley returns in the constructor if the input handle is null. That leaves the string field set to default, and that's then directly passed to the Guid constructor. I added this check here to try to match the semantics in case of failures as close as possible.

@MichalStrehovsky
Copy link
Member

/azp run runtime-nativeaot-outerloop

Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jkotas jkotas added the tenet-performance Performance related issue label Jun 5, 2025
@jkotas
Copy link
Member

jkotas commented Jun 5, 2025

Could you please measure the perf improvement for Type.GUID?

@Sergio0694
Copy link
Contributor Author

Yup yup, will set some time aside to whip together a small benchmark. Was chatting with @EgorBo who said he might look into adding NAOT support to the bot, but I might just do a simple thing locally for now to unblock this sooner 🙂

@Sergio0694
Copy link
Contributor Author

I'm struggling to figure out how to run BenchmarkDotNet with two local Native AOT builds, are there any docs? 😅
The docs here seem outdated (they mention .NET 7) and point to an output path that doesn't exist.

What I did is:

  • build.cmd clr.aot+libs -rc Release both on main and this PR

And then I have:

using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<Benchmark>();

[MemoryDiagnoser]
public class Benchmark
{
    private readonly Type _type = typeof(IFoo);

    [Benchmark]
    public Guid GetGUID() => _type.GUID;
}

[Guid("1DB7A2FC-9CB3-4A95-BEED-758C77F7715B")]
public interface IFoo;

I would imagine I'd need a way to setup two jobs, each with IlcSdkPath set for \artifacts\bin\coreclr\windows.x64.Release\aotsdk\ in the build for either main or the PR branch, but it's not clear to me how to do this. Are there docs or workflow instructions for this specific scenario anywhere, by any chance? Thank you!

@jkotas
Copy link
Member

jkotas commented Jun 10, 2025

point to an output path that doesn't exist.

artifacts\packages\Release\Shipping gets built only in subsets that build packages. The easiest way to build it is by running build -c release.

It is the same as what's documented in
https://github.com/dotnet/runtime/blob/main/docs/workflow/building/coreclr/nativeaot.md#building-packages .

@Sergio0694
Copy link
Contributor Author

Thank you! Made some progress but things are still not working. I have this code:

using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;
using BenchmarkDotNet.Toolchains.NativeAot;

var config = DefaultConfig.Instance
    .AddJob(Job.ShortRun
        .WithToolchain(NativeAotToolchain.CreateBuilder()
            .UseLocalBuild(new DirectoryInfo(@"D:\diffs\nativeaot\guid_optimize\main\Shipping\"))
            .DisplayName("NativeAOT main")
            .TargetFrameworkMoniker("net10.0")
            .ToToolchain()))
    .AddJob(Job.ShortRun
        .WithToolchain(NativeAotToolchain.CreateBuilder()
            .UseLocalBuild(new DirectoryInfo(@"D:\diffs\nativeaot\guid_optimize\pr\Shipping\"))
            .DisplayName("NativeAOT PR")
            .TargetFrameworkMoniker("net10.0")
            .ToToolchain()));

BenchmarkRunner.Run<Benchmark>(config);

[MemoryDiagnoser]
public class Benchmark
{
    private readonly Type _type = typeof(IFoo);

    [Benchmark]
    public Guid GetGUID() => _type.GUID;
}

[Guid("1DB7A2FC-9CB3-4A95-BEED-758C77F7715B")]
public interface IFoo;

I built the two packages in those two directories.
Then I setup a .csproj with .NET 10, and I'm doing dotnet run -c Release using the stable public .NET 10 Preview 4.

When BDN tries building the two benchmarks, it fails with this error:

EXEC : error : Unrecognized instruction set sse [C:\Users\sergiopedri\AppData\Local\Temp\GuidBenchmark-ShortRun-1\BenchmarkDotNet.Autogenerated.csproj]
  System.CommandLine.CommandLineException: Unrecognized instruction set sse
     at System.CommandLine.Helpers.ConfigureInstructionSetSupport(String, Int32, Boolean, TargetArchitecture, TargetOS, String, String, Logger, Boolean) + 0xced
     at ILCompiler.Program.Run() + 0x777
     at ILCompiler.ILCompilerRootCommand.<>c__DisplayClass255_0.<.ctor>b__0(ParseResult) + 0x314

@EgorBo mentioned it's likely because SSE was removed, but BDN tries to use it here.

Any possible workaround I could use? 😅

@jkotas
Copy link
Member

jkotas commented Jun 10, 2025

mentioned it's likely because SSE was removed, but BDN tries to use it here.

Why is it not taking the native instruction set path a few lines above?

@EgorBo
Copy link
Member

EgorBo commented Jun 10, 2025

When I was trying to follow the steps, I ended up with this error:

error LNK2001: unresolved external symbol CompressionNative_InflateReset2_

🙁

@jkotas
Copy link
Member

jkotas commented Jun 10, 2025

I am fine if you just build a trivial manual test as a console app and run it before/after. Personally, I tend to do that for quick measurements - it is faster, guaranteed to work, and you can tell exactly what it is measuring.

using System.Diagnostics;
using System.Runtime.InteropServices;

Stopwatch sw = new();
for (;;)
{
    sw.Restart();
    for (int i = 0; i < 10000000; i++)
    {
        Guid g = typeof(MyType).GUID;
    }
    Console.WriteLine(sw.ElapsedMilliseconds);
}

[Guid("690FCAC9-4CEC-406A-88DE-DA86B7914EA7")]
class MyType
{
}

@Sergio0694 Sergio0694 force-pushed the user/sergiopedri/optimize-guid-parsing branch from 028c367 to 04c482c Compare June 17, 2025 22:13
@Sergio0694
Copy link
Contributor Author

Results aren't making any sense to me given the changes:

main PR
4840 5226
4865 5159
4843 5164
4831 5169
4833 5163

I'll re-test. I don't see how this could possibly be the case.

@Sergio0694 Sergio0694 force-pushed the user/sergiopedri/optimize-guid-parsing branch from 04c482c to 41870df Compare June 19, 2025 21:27
@Sergio0694
Copy link
Contributor Author

Double checked after rebasing. Same exact results. I did not accidentally swap them, the numbers are correct.

main PR
4860 5230
4894 5331
4905 5168
4886 5174
4893 5195

I genuinely don't understand how the changes here could possibly regress perf.
Could anyone help double check? Or, does anyone have any ideas? Could this be something else..? 😅

@jkotas
Copy link
Member

jkotas commented Jun 20, 2025

I see a very small (~1.5%) improvement (win-x64, default PublishAot, AMD EPYC 7763 - standard MS DevBox). It is not unexpected since this is not improving where the time is actually spent in Type.GUID.

@jkotas
Copy link
Member

jkotas commented Jun 20, 2025

Or, does anyone have any ideas?

What are the machine specs that you run this on? Just want to double check - do you run this on release builds of the runtime (build -c release)?

@Sergio0694
Copy link
Contributor Author

"I see a very small (~1.5%) improvement"

That's in line with what I was expecting too: small perf improvement, and less memory use (which we can't see here though).

"What are the machine specs that you run this on?"

Local desktop, i9-12900K.

"do you run this on release builds of the runtime"

Yeah I just did build.cmd -c release on both commits, and then published with the latest nightly + IlcAotSdkPath.

Co-authored-by: kasperk81 <83082615+kasperk81@users.noreply.github.com>
@jkotas
Copy link
Member

jkotas commented Jun 22, 2025

and less memory use (which we can't see here though).

Sounds good, making the property zero allocation (when called repeatedly) is a reasonable justification.

@jkotas jkotas merged commit 9a4be5b into dotnet:main Jun 22, 2025
92 of 94 checks passed
@Sergio0694 Sergio0694 deleted the user/sergiopedri/optimize-guid-parsing branch June 22, 2025 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-NativeAOT-coreclr community-contribution Indicates that the PR has been added by a community member tenet-performance Performance related issue
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants