Skip to content

Compile runtime async versions of synchronous task-returning methods#128384

Open
jakobbotsch wants to merge 95 commits into
dotnet:mainfrom
jakobbotsch:runtime-async-versions
Open

Compile runtime async versions of synchronous task-returning methods#128384
jakobbotsch wants to merge 95 commits into
dotnet:mainfrom
jakobbotsch:runtime-async-versions

Conversation

@jakobbotsch
Copy link
Copy Markdown
Member

@jakobbotsch jakobbotsch commented May 19, 2026

Instead of delegating from runtime async callable thunks to the original task returning methods this PR compiles a fully separate runtime async version. It then adds a guaranteed optimization to make tail calls in the synchronous task-returning methods into runtime async calls.

This is one potential approach to fix #115771.

Copilot AI review requested due to automatic review settings May 19, 2026 20:00
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label May 19, 2026
Copy link
Copy Markdown
Contributor

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 extends the JIT↔EE async infrastructure so the JIT can compile a dedicated “runtime async version” of synchronous Task/ValueTask-returning methods, including a tail-position optimization that turns eligible tail calls/returns into runtime-async awaits.

Changes:

  • Adds a new JIT↔EE interface API (getAwaitReturnCall) and plumbs it through SuperPMI and generated wrappers/shims.
  • Updates CoreCLR async thunk IL generation and AsyncHelpers to separate “suspend” helpers from typed TransparentAwait(...) helpers used by the JIT.
  • Updates the JIT importer to recognize “async-version tail await” patterns and to wrap async-version returns in an await via the new EE callback.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/tests/async/reflection/reflection.cs Disables several assertions related to reflection/stack behavior for await-on-task-returning paths.
src/coreclr/vm/prestub.cpp Adjusts IL header retrieval logic for async variant methods.
src/coreclr/vm/method.hpp Changes IL-header eligibility rules for async/thunk/return-dropping methods; exposes GetAsyncThunkResultTypeSig.
src/coreclr/vm/metasig.h Adds metasig entries for Task/ValueTask TransparentAwait helper signatures.
src/coreclr/vm/jitinterface.h Adds helper declarations related to runtime lookup computation for new await-return support.
src/coreclr/vm/jitinterface.cpp Implements getAwaitReturnCall and runtime-lookup computation for generic await helpers; sets CORINFO_ASYNC_VERSION for applicable methods.
src/coreclr/vm/corelib.h Adds/updates CoreLibBinder entries for new suspend/await AsyncHelpers methods with proper signatures.
src/coreclr/vm/asyncthunks.cpp Switches thunk-emitted calls from TransparentAwait* to TransparentSuspendFor* helpers.
src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp Records/replays the new getAwaitReturnCall API for SuperPMI.
src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp Forwards the new API in the simple shim.
src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp Counts/forwards the new API in the counter shim.
src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp Records the new API in the collector shim.
src/coreclr/tools/superpmi/superpmi-shared/spmirecordhelper.h Makes lookup restore helpers take const& and updates corresponding implementations.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h Adds recording/replay plumbing for getAwaitReturnCall.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp Implements record/dump/replay for getAwaitReturnCall.
src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h Registers the new lightweight-map packet for GetAwaitReturnCall.
src/coreclr/tools/superpmi/superpmi-shared/agnostic.h Adds agnostic structs for CORINFO_LOOKUP* and the await-return result payload.
src/coreclr/tools/Common/TypeSystem/IL/Stubs/AsyncThunks.cs Updates IL stub emitter to use TransparentSuspendFor* helper names.
src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt Adds the new interface method to thunk generation input.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs Adds a stub managed implementation of getAwaitReturnCall for the managed JIT interface.
src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs Adds unmanaged callback plumbing for getAwaitReturnCall.
src/coreclr/tools/aot/jitinterface/jitinterface_generated.h Adds the new callback and wrapper method to the AOT jitinterface wrapper.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs Renames “suspend” helpers and adds typed TransparentAwait(...) overloads used by the JIT.
src/coreclr/jit/importercalls.cpp Adds tail-await handling for async-version tail-await prefix and blocks inlining of async-version callees.
src/coreclr/jit/importer.cpp Adds async-version tail-call recognition, wraps async-version returns in an await via getAwaitReturnCall, and introduces impWrapTopOfStackInAwait.
src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp Adds wrapper forwarding for getAwaitReturnCall.
src/coreclr/jit/ICorJitInfo_names_generated.h Adds name entry for getAwaitReturnCall.
src/coreclr/jit/fginline.cpp Minor whitespace change near async flag handling for inlinee compilation.
src/coreclr/jit/compiler.h Introduces PREFIX_IS_ASYNC_VERSION_TAIL_AWAIT, impWrapTopOfStackInAwait, and compIsAsyncVersion().
src/coreclr/jit/compiler.cpp Adds verbose printing when compiling an async-version method.
src/coreclr/inc/jiteeversionguid.h Updates JIT↔EE version GUID due to interface change.
src/coreclr/inc/icorjitinfoimpl_generated.h Adds getAwaitReturnCall override to the generated ICorJitInfo impl header.
src/coreclr/inc/corinfo.h Adds CORINFO_ASYNC_VERSION and the new ICorStaticInfo::getAwaitReturnCall method.

Comment thread src/tests/async/reflection/reflection.cs
Comment thread src/tests/async/reflection/reflection.cs
Comment thread src/tests/async/reflection/reflection.cs
Comment thread src/coreclr/vm/jitinterface.cpp Outdated
Comment thread src/coreclr/jit/importer.cpp Outdated
Comment thread src/coreclr/inc/corinfo.h
Comment thread src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt
Comment thread src/coreclr/jit/importer.cpp
Copilot AI review requested due to automatic review settings May 20, 2026 10:46
Copy link
Copy Markdown
Contributor

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

Copilot reviewed 32 out of 32 changed files in this pull request and generated 6 comments.

Comment thread src/coreclr/jit/importer.cpp Outdated
Comment thread src/coreclr/jit/importer.cpp
Comment thread src/tests/async/reflection/reflection.cs
Comment thread src/coreclr/inc/corinfo.h Outdated
Comment thread src/coreclr/vm/jitinterface.cpp Outdated
Comment thread src/coreclr/vm/asyncthunks.cpp Outdated
@jakobbotsch
Copy link
Copy Markdown
Member Author

Another pattern we may want to recognize:

public static ValueTask<TSource?> MaxAsync<TSource>(
this IAsyncEnumerable<TSource> source,
IComparer<TSource>? comparer = null,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(source);
comparer ??= Comparer<TSource>.Default;
// Special-case float/double/float?/double? to maintain compatibility
// with System.Linq.Enumerable implementations.
#pragma warning disable CA2012 // Use ValueTasks correctly
if (typeof(TSource) == typeof(float) && comparer == Comparer<TSource>.Default)
{
return (ValueTask<TSource?>)(object)MaxAsync((IAsyncEnumerable<float>)(object)source, cancellationToken);

Copilot AI review requested due to automatic review settings May 20, 2026 11:47
Copy link
Copy Markdown
Contributor

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

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

Comment thread src/tests/async/reflection/reflection.cs
Comment thread src/coreclr/vm/asyncthunks.cpp Outdated
Comment thread src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
Copilot AI review requested due to automatic review settings May 20, 2026 14:18
{ 11, CEE_CALL},
{ 16, CEE_CALL },
{ 19, CEE_ILLEGAL } // End marker
{ 21, CEE_ILLEGAL } // End marker
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not sure if I am missing something or if we really have no coverage of this case -- as far as I can see we use this value to figure out how many IL bytes to skip after the pattern match, so this would almost certainly cause problems.

public override bool IsInitLocals => _ecmaIL.IsInitLocals;
public override int MaxStack => _ecmaIL.MaxStack;

public static MethodIL GetWrappedIfAsyncVersion(MethodDesc method, Compilation compilation)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could this logic be moved into the NativeAotILProvider.GetMethodIL() and ReadyToRunILProvider.GetMethodIL()? Or do we run into issues elsewhere if we do that?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This was on @MichalStrehovsky's suggestion -- since the IL from the synchronous version is mistyped (returns a Task<T> when the async variant expects T) we did not want this to leak out of the JIT-EE and IL scanner.

@jakobbotsch jakobbotsch marked this pull request as ready for review June 8, 2026 12:18
Copilot AI review requested due to automatic review settings June 8, 2026 12:18
@jakobbotsch
Copy link
Copy Markdown
Member Author

runtime-interpreter failures look like #128947.
runtime-nativeaot-outerloop failures look like #126867.

Comment on lines +711 to +720
private static T TransparentAwaitWithResult<T>(ValueTask<T> task)
{
if (!task.IsCompleted)
{
TailAwait();
return TransparentAwaitValueTaskOfT(task);
}

return task.Result;
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I want to rename these in a follow-up -- we have TransparentAwait already that always suspends and TransparentAwaitWithResult that does a check and then suspends. I think the "always suspend" variant should be called TransparentSuspend instead of TransparentAwait, and this version should be called TransparentAwait to match what the await keyword does. But I want to do that separately since this PR is already large.

_dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("FinishSuspensionNoContinuationContext"u8, null)), asyncReason);
_dependencies.Add(_factory.MethodEntrypoint(asyncHelpers.GetKnownMethod("FinishSuspensionWithContinuationContext"u8, null)), asyncReason);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Previously this relied on seeing a function call with async calling convention -- normally AsyncHelpers.Await -- but for the new async versions we don't see any calls like that. Instead we now do this from inside ImportCall when we did end up with an actual async variant to import.

Comment on lines +587 to +593
// Currently crossgen2 does not support compiling async versions of synchronous Task-returning functions.
// We would compile a wrapper thunk but that comes with different perf characteristics and diagnostics
// that we do not want to deal with.
if (methodNeedingCode.SupportsAsyncVersionCodegen())
{
return true;
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hoping to leave this for a follow-up.

Copy link
Copy Markdown
Contributor

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

Copilot reviewed 43 out of 43 changed files in this pull request and generated 2 comments.

Comment on lines +9036 to +9040
if ((codeAddr + sz < codeEndp) && (getU1LittleEndian(codeAddr + sz) == CEE_RET))
{
JITDUMP("\nRecognized tail-call in async version\n");
awaitOffset = (IL_OFFSET)(codeAddr - info.compCode);
isAwait = true;
Comment thread src/coreclr/inc/corinfo.h
Comment on lines +3143 to +3146
// Get information about which await call to use to await the return type
// of the non-async version of an async call.
virtual CORINFO_METHOD_HANDLE getAwaitReturnCall(CORINFO_METHOD_HANDLE callerHandle, CORINFO_LOOKUP* instArg) = 0;

&callInfo);

if (isAwait && (callInfo.kind == CORINFO_CALL))
// TODO: crossgen2 cannot handle us removing this
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@jtschuster I was still running into some problems here in the full tests, even with the crossgen2 fix suggested. I would suggest we remove this and add the crossgen2 support separately in a follow up.

// Consume the ret opcode. Note `codeAddr` points at the unconsumed token;
// the main loop will still do `codeAddr += sz` (token size) after this case.
codeAddrAfterMatch = codeAddr + 1;
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We need handling for methods that end with tail. Call() and JMP, here and in the interpreter.


asyncInfo.IsTailAwait = inlCall->GetAsyncInfo().IsTailAwait;
asyncInfo.IsTailAwait =
inlCall->GetAsyncInfo().IsTailAwait && (m_nextAwaitIsTail || (call->gtReturnType == info.compRetType));
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We should consider whether we should call canTailcall JIT-EE function to get these checks:

// Do not tailcall from the application entrypoint.
// We want Main to be visible in stack traces.
if (callerToken == pCaller->GetModule()->GetEntryPointToken())
{
result = false;
szFailReason = "Caller is the entry point";
goto exit;
}
if (!pCaller->IsNoMetadata())
{
// Do not tailcall from methods that are marked as NoInlining (people often use no-inline
// to mean "I want to always see this method in stacktrace")
DWORD dwImplFlags = 0;
IfFailThrow(pCaller->GetMDImport()->GetMethodImplProps(callerToken, NULL, &dwImplFlags));
if (IsMiNoInlining(dwImplFlags))
{
result = false;
szFailReason = "Caller is marked as NoInlining";
goto exit;
}

Otherwise these methods won't show up in async stacktraces. OTOH tail-awaiting is a significantly more impactful optimization than tail-calling since it removes a continuation from the chain.

@jakobbotsch
Copy link
Copy Markdown
Member Author

jakobbotsch commented Jun 8, 2026

I've marked this as ready, but there are a couple of caveats:

  1. For debug IL codegen Roslyn produces something that we do not recognize. In particular it rewrites
Task Foo()
{
  if (x)
    return Bar();
  return Baz();
}

into

Task Foo()
{
  Task t;
  if (x)
  {
    t = Bar();
    goto RET;
  }
  t = Baz();
  goto RET;

  RET: return t;
}

In general the solution here is not ideal, but after some discussion with @davidwrighton I do believe it is the most pragmatic way of solving the problem for 11.0. We can consider some more robust solutions in the future where we provide some kind of syntax that allows users to make these wrapper functions async and still retain the old EH/context behavior.
I think it is ok that debug IL codegen does not get the same level of optimization here as release IL codegen will, as long as we do recognize it in tier0/tier1 and as long as the diagnostic experience is reasonable and consistent.

I can also do something best-effort in the JIT/interpreter to recognize some of those specific patterns emitted, even in debug IL codegen, but like @jkotas has said before we do not really want to have to start recognizing specific Roslyn IL patterns.

  1. On that note there are a couple patterns used for returning ValueTask from Task methods (Foo().AsTask()) and Task from ValueTask methods (new ValueTask(Foo())). I think these are reasonable to also recognize, and it will be simple enough to do so.

  2. There is still missing diagnostic work here. I know that @tommcdon has been working on that on the side, and seemed to have something that worked. @tommcdon do you want to get that included here, or are you ok with merging this before having the diagnostic part?

  3. I haven't renamed IsAsyncThunk() which is a bit misleading now since these aren't thunks. Open to suggestions on what to do here. The managed type system has a SupportsAsyncVersionCodegen which checks for these specific methods, since it needed this check in more places. Let me push a commit to add a method by that name in the VM too.

  4. Crossgen2 support is missing. For now this just skips compiling the new async version functions and leaves it to runtime codegen. I was hoping to get some help from @jtschuster in a follow-up to implement this.

  5. I've left a few TODOs as comments that I expect to fix before merge, but this should be good for review and comments.

  6. Also reposting the NativeAOT test sizes from above. They regress by a bit, but not all that much:

NativeAOT test sizes
BaseSize DiffSize Delta Percent Improved Name
121,959,936 123,415,040 +1,455,104 +1.193% System.Text.Json.SourceGeneration.Roslyn4.4.Tests.exe
26,296,320 26,409,472 +113,152 +0.430% System.Text.Json.SourceGeneration.Roslyn3.11.Tests.exe
22,708,224 22,799,360 +91,136 +0.401% System.Net.Sockets.Tests.exe
9,981,440 10,018,304 +36,864 +0.369% System.IO.Compression.ZipFile.Tests.exe
11,894,272 11,935,232 +40,960 +0.344% System.IO.Tests.exe
11,228,160 11,261,952 +33,792 +0.301% System.IO.Compression.Brotli.Tests.exe
19,445,760 19,494,912 +49,152 +0.253% System.IO.Compression.Tests.exe
10,209,792 10,234,368 +24,576 +0.241% System.Threading.Channels.Tests.exe
21,561,344 21,613,056 +51,712 +0.240% System.IO.FileSystem.Tests.exe
11,717,632 11,745,792 +28,160 +0.240% System.Net.Http.Json.Unit.Tests.exe
9,683,968 9,706,496 +22,528 +0.233% System.Threading.RateLimiting.Tests.exe
14,467,584 14,500,864 +33,280 +0.230% System.Net.WebClient.Tests.exe
21,101,568 21,148,672 +47,104 +0.223% System.Net.Requests.Tests.exe
19,836,416 19,878,400 +41,984 +0.212% System.Net.WebSockets.Client.Tests.exe
19,171,840 19,211,776 +39,936 +0.208% System.IO.Pipes.Tests.exe
16,630,784 16,664,064 +33,280 +0.200% System.Xml.Linq.Misc.Tests.exe
29,227,008 29,284,864 +57,856 +0.198% System.Net.Http.WinHttpHandler.Functional.Tests.exe
20,096,000 20,134,912 +38,912 +0.194% System.Threading.Tasks.Dataflow.Tests.exe
10,467,328 10,486,784 +19,456 +0.186% System.Net.Http.WinHttpHandler.Unit.Tests.exe
18,908,160 18,942,464 +34,304 +0.181% System.Net.HttpListener.Tests.exe
27,860,992 27,911,168 +50,176 +0.180% System.Net.Security.Tests.exe
11,268,096 11,288,064 +19,968 +0.177% System.Net.WebSockets.Tests.exe
10,595,328 10,613,248 +17,920 +0.169% System.IO.Pipelines.Tests.exe
30,835,200 30,887,424 +52,224 +0.169% System.Net.Http.Functional.Tests.exe
15,676,416 15,699,968 +23,552 +0.150% System.Windows.Extensions.Tests.exe
19,425,280 19,454,464 +29,184 +0.150% System.Diagnostics.Process.Tests.exe
17,878,528 17,904,128 +25,600 +0.143% System.Threading.ThreadPool.Tests.exe
9,507,328 9,520,640 +13,312 +0.140% System.IO.Hashing.Tests.exe
17,879,040 17,904,128 +25,088 +0.140% System.Threading.ThreadPool.WindowsThreadPool.Tests.exe
14,044,672 14,064,128 +19,456 +0.139% System.DirectoryServices.Protocols.Tests.exe
19,745,792 19,772,928 +27,136 +0.137% System.Text.Encoding.Tests.exe
12,240,384 12,256,768 +16,384 +0.134% System.Net.NetworkInformation.Functional.Tests.exe
18,328,064 18,351,616 +23,552 +0.129% System.Threading.Tasks.Parallel.Tests.exe
14,768,128 14,785,536 +17,408 +0.118% System.Xml.Linq.Streaming.Tests.exe
9,582,080 9,593,344 +11,264 +0.118% System.Memory.Data.Tests.exe
9,619,456 9,630,208 +10,752 +0.112% Microsoft.Extensions.Hosting.Functional.Tests.exe
10,057,216 10,068,480 +11,264 +0.112% Microsoft.Extensions.Caching.Memory.Tests.exe
16,534,016 16,551,424 +17,408 +0.105% System.Xml.Linq.Properties.Tests.exe
16,926,720 16,944,128 +17,408 +0.103% System.Xml.Linq.xNodeBuilder.Tests.exe
16,387,072 16,403,968 +16,896 +0.103% System.Xml.Linq.Events.Tests.exe
16,870,912 16,887,808 +16,896 +0.100% System.Xml.Linq.xNodeReader.Tests.exe
16,983,040 16,999,936 +16,896 +0.099% System.Xml.Linq.TreeManipulation.Tests.exe
9,011,712 9,020,416 +8,704 +0.097% System.Net.NameResolution.Pal.Tests.exe
8,747,008 8,755,200 +8,192 +0.094% Microsoft.Bcl.TimeProvider.Tests.exe
20,775,424 20,794,880 +19,456 +0.094% System.Net.Quic.Functional.Tests.exe
9,203,200 9,211,392 +8,192 +0.089% Microsoft.Bcl.AsyncInterfaces.Tests.exe
22,945,792 22,963,712 +17,920 +0.078% System.Threading.Tasks.Tests.exe
17,956,352 17,970,176 +13,824 +0.077% System.Console.Tests.exe
9,284,096 9,291,264 +7,168 +0.077% System.Net.Primitives.UnitTests.Tests.exe
11,340,800 11,349,504 +8,704 +0.077% System.Net.Security.Unit.Tests.exe
23,267,840 23,285,248 +17,408 +0.075% System.Memory.Tests.exe
18,659,840 18,673,664 +13,824 +0.074% Microsoft.Extensions.FileProviders.Physical.Tests.exe
27,127,296 27,147,264 +19,968 +0.074% Microsoft.Extensions.Hosting.Unit.Tests.exe
10,038,272 10,045,440 +7,168 +0.071% System.Net.ServerSentEvents.Tests.exe
17,597,440 17,609,728 +12,288 +0.070% System.Threading.Tasks.Extensions.Tests.exe
13,154,304 13,163,520 +9,216 +0.070% System.IO.Packaging.Tests.exe
8,900,096 8,906,240 +6,144 +0.069% System.IO.Pipes.AccessControl.Tests.exe
32,963,072 32,985,600 +22,528 +0.068% System.Private.Xml.Tests.exe
17,229,312 17,241,088 +11,776 +0.068% System.Threading.Timer.Tests.exe
18,271,744 18,284,032 +12,288 +0.067% System.Threading.Tests.exe
9,342,464 9,348,608 +6,144 +0.066% System.Net.Mail.Unit.Tests.exe
11,768,320 11,776,000 +7,680 +0.065% System.Security.Cryptography.Cose.Tests.exe
8,692,736 8,698,368 +5,632 +0.065% System.Private.Uri.Unit.Tests.exe
8,784,384 8,790,016 +5,632 +0.064% System.Resources.Reader.Tests.exe
8,961,024 8,966,656 +5,632 +0.063% System.Net.Primitives.Pal.Tests.exe
11,577,344 11,584,512 +7,168 +0.062% System.Resources.Extensions.Tests.exe
9,186,304 9,191,936 +5,632 +0.061% System.ServiceProcess.ServiceController.Tests.exe
8,520,704 8,525,824 +5,120 +0.060% System.ComponentModel.Tests.exe
8,531,456 8,536,576 +5,120 +0.060% System.Composition.AttributeModel.Tests.exe
8,596,992 8,602,112 +5,120 +0.060% System.Composition.Runtime.Tests.exe
8,516,096 8,521,216 +5,120 +0.060% System.IO.FileSystem.Primitives.Tests.exe
9,402,880 9,408,512 +5,632 +0.060% Microsoft.Extensions.Logging.Testing.Tests.exe
8,594,944 8,600,064 +5,120 +0.060% System.Xml.Linq.Axes.Tests.exe
8,732,160 8,737,280 +5,120 +0.059% Invariant.Tests.exe
17,214,464 17,224,704 +10,240 +0.059% System.Security.Principal.Windows.Tests.exe
8,642,560 8,647,680 +5,120 +0.059% System.Runtime.Handles.Tests.exe
8,620,032 8,625,152 +5,120 +0.059% Microsoft.Win32.Registry.AccessControl.Tests.exe
8,636,416 8,641,536 +5,120 +0.059% Microsoft.Win32.Primitives.Tests.exe
8,789,504 8,794,624 +5,120 +0.058% System.Web.HttpUtility.Tests.exe
9,770,496 9,776,128 +5,632 +0.058% System.Net.WebProxy.Tests.exe
9,675,264 9,680,896 +5,632 +0.058% System.Text.RegularExpressions.Unit.Tests.exe
8,930,304 8,935,424 +5,120 +0.057% System.Threading.AccessControl.Tests.exe
9,820,160 9,825,792 +5,632 +0.057% System.Formats.Nrbf.Tests.exe
9,025,536 9,030,656 +5,120 +0.057% Microsoft.Extensions.Configuration.CommandLine.Tests.exe
8,963,072 8,968,192 +5,120 +0.057% Microsoft.Win32.Registry.Tests.exe
18,265,600 18,275,840 +10,240 +0.056% Microsoft.Extensions.Primitives.Tests.exe
9,333,248 9,338,368 +5,120 +0.055% System.Security.Permissions.Tests.exe
9,333,248 9,338,368 +5,120 +0.055% System.Reflection.Context.Tests.exe
9,295,360 9,300,480 +5,120 +0.055% Microsoft.Extensions.Diagnostics.Abstractions.Tests.exe
20,471,808 20,483,072 +11,264 +0.055% System.Diagnostics.DiagnosticSource.Tests.exe
8,573,952 8,578,560 +4,608 +0.054% Microsoft.Extensions.Hosting.Abstractions.Tests.exe
8,530,944 8,535,552 +4,608 +0.054% System.Text.Encoding.Extensions.Tests.exe
8,535,552 8,540,160 +4,608 +0.054% System.Diagnostics.Contracts.Tests.exe
8,519,680 8,524,288 +4,608 +0.054% System.Runtime.CompilerServices.VisualC.Tests.exe
8,578,560 8,583,168 +4,608 +0.054% System.Resources.Writer.Tests.exe
8,565,248 8,569,856 +4,608 +0.054% Microsoft.Bcl.Numerics.Tests.exe
18,337,280 18,347,008 +9,728 +0.053% Microsoft.Extensions.Configuration.FileExtensions.Tests.exe
17,334,272 17,343,488 +9,216 +0.053% System.Diagnostics.TraceSource.Tests.exe
18,417,152 18,426,880 +9,728 +0.053% System.IO.MemoryMappedFiles.Tests.exe
8,741,376 8,745,984 +4,608 +0.053% System.IO.FileSystem.DriveInfo.Tests.exe
8,672,256 8,676,864 +4,608 +0.053% System.IO.FileSystem.Manual.Tests.exe
8,613,888 8,618,496 +4,608 +0.053% System.Net.WebHeaderCollection.Tests.exe
9,590,272 9,595,392 +5,120 +0.053% System.Composition.Convention.Tests.exe
17,522,176 17,531,392 +9,216 +0.053% System.Threading.Thread.Tests.exe
18,372,608 18,382,336 +9,728 +0.053% Microsoft.Extensions.Configuration.Tests.exe
8,653,824 8,658,432 +4,608 +0.053% System.Runtime.CompilerServices.Unsafe.Tests.exe
8,659,968 8,664,576 +4,608 +0.053% System.Runtime.InteropServices.ComDisabled.Tests.exe
8,673,792 8,678,400 +4,608 +0.053% System.Security.Cryptography.ProtectedData.Tests.exe
8,676,864 8,681,472 +4,608 +0.053% System.Security.SecureString.Tests.exe
8,625,664 8,630,272 +4,608 +0.053% System.Globalization.CalendarsWithConfigSwitch.Tests.exe
8,809,984 8,814,592 +4,608 +0.052% System.Console.Manual.Tests.exe
8,923,648 8,928,256 +4,608 +0.052% Microsoft.Bcl.Memory.Tests.exe
9,914,368 9,919,488 +5,120 +0.052% System.Collections.NonGeneric.Tests.exe
8,836,608 8,841,216 +4,608 +0.052% System.Reflection.Extensions.Tests.exe
9,929,728 9,934,848 +5,120 +0.052% LibraryImportGenerator.Tests.exe
8,996,352 9,000,960 +4,608 +0.051% System.Reflection.TypeExtensions.Tests.exe
8,974,848 8,979,456 +4,608 +0.051% System.Runtime.Serialization.Primitives.Tests.exe
9,116,672 9,121,280 +4,608 +0.051% System.Security.AccessControl.Tests.exe
9,079,296 9,083,904 +4,608 +0.051% System.IO.FileSystem.AccessControl.Tests.exe
17,135,104 17,143,808 +8,704 +0.051% System.ComponentModel.EventBasedAsync.Tests.exe
24,130,560 24,142,848 +12,288 +0.051% Microsoft.Extensions.Http.Tests.exe
9,094,656 9,099,264 +4,608 +0.051% System.ValueTuple.Tests.exe
9,008,128 9,012,736 +4,608 +0.051% Microsoft.Extensions.FileSystemGlobbing.Tests.exe
17,265,664 17,274,368 +8,704 +0.050% System.Diagnostics.StackTrace.Tests.exe
9,183,744 9,188,352 +4,608 +0.050% Microsoft.Extensions.Configuration.EnvironmentVariables.Tests.exe
9,137,664 9,142,272 +4,608 +0.050% System.Globalization.Calendars.Tests.exe
17,330,688 17,339,392 +8,704 +0.050% System.Net.Ping.Functional.Tests.exe
9,133,568 9,138,176 +4,608 +0.050% System.ComponentModel.Primitives.Tests.exe
11,164,160 11,169,792 +5,632 +0.050% System.Text.Encodings.Web.Tests.exe
9,455,104 9,459,712 +4,608 +0.049% Microsoft.Extensions.Configuration.Ini.Tests.exe
19,299,840 19,309,056 +9,216 +0.048% System.Net.Primitives.Functional.Tests.exe
9,591,296 9,595,904 +4,608 +0.048% System.Diagnostics.EventLog.Tests.exe
16,050,688 16,058,368 +7,680 +0.048% ilc.exe
8,521,728 8,525,824 +4,096 +0.048% System.Diagnostics.Tools.Tests.exe
9,616,384 9,620,992 +4,608 +0.048% System.Private.Uri.Functional.Tests.exe
17,018,880 17,027,072 +8,192 +0.048% System.Private.CoreLib.dll
9,714,176 9,718,784 +4,608 +0.047% System.Collections.Specialized.Tests.exe
8,690,688 8,694,784 +4,096 +0.047% System.Diagnostics.FileVersionInfo.Tests.exe
9,735,168 9,739,776 +4,608 +0.047% System.Formats.Asn1.Tests.exe
11,018,752 11,023,872 +5,120 +0.046% System.Runtime.Numerics.Tests.exe
10,026,496 10,031,104 +4,608 +0.046% System.Diagnostics.Debug.Tests.exe
16,791,040 16,798,720 +7,680 +0.046% System.Linq.AsyncEnumerable.Tests.exe
12,245,504 12,251,136 +5,632 +0.046% System.Reflection.Metadata.Tests.exe
19,852,288 19,861,504 +9,216 +0.046% Microsoft.Extensions.Logging.Tests.exe
11,449,856 11,454,976 +5,120 +0.045% System.IO.Ports.Tests.exe
45,389,824 45,410,304 +20,480 +0.045% System.Security.Cryptography.Tests.exe
17,202,688 17,210,368 +7,680 +0.045% System.Security.Claims.Tests.exe
10,465,280 10,469,888 +4,608 +0.044% Microsoft.Extensions.Options.Tests.exe
2,310,656 2,311,680 +1,024 +0.044% clrjit.dll
17,469,952 17,477,632 +7,680 +0.044% MetricOuterLoop.Tests.exe
17,473,024 17,480,704 +7,680 +0.044% MetricOuterLoop1.Tests.exe
19,567,616 19,576,320 +8,704 +0.044% System.Net.Http.Unit.Tests.exe
17,119,232 17,126,400 +7,168 +0.042% System.Net.ServicePoint.Tests.exe
17,187,840 17,195,008 +7,168 +0.042% System.Threading.Overlapped.Tests.exe
18,081,792 18,089,472 +7,680 +0.042% System.Text.Encoding.CodePages.Tests.exe
18,391,040 18,398,720 +7,680 +0.042% Microsoft.Extensions.Hosting.Systemd.Tests.exe
18,242,560 18,250,240 +7,680 +0.042% Microsoft.Extensions.FileProviders.Composite.Tests.exe
17,141,760 17,148,928 +7,168 +0.042% System.Runtime.InvariantTimezone.Tests.exe
17,260,544 17,267,712 +7,168 +0.042% Microsoft.Win32.SystemEvents.Tests.exe
26,289,152 26,299,904 +10,752 +0.041% System.ComponentModel.TypeConverter.Tests.exe
17,933,824 17,940,992 +7,168 +0.040% Microsoft.Extensions.Diagnostics.Tests.exe
12,770,304 12,775,424 +5,120 +0.040% System.Xml.Schema.Extensions.Tests.exe
18,978,816 18,986,496 +7,680 +0.040% Microsoft.Extensions.Hosting.WindowsServices.Tests.exe
20,610,048 20,618,240 +8,192 +0.040% System.Collections.Concurrent.Tests.exe
29,885,952 29,897,728 +11,776 +0.039% Microsoft.Extensions.DependencyModel.Tests.exe
18,572,288 18,579,456 +7,168 +0.039% Microsoft.Extensions.Logging.Console.Tests.exe
17,194,496 17,201,152 +6,656 +0.039% System.Runtime.InteropServices.RuntimeInformation.Tests.exe
17,060,352 17,067,008 +6,656 +0.039% IcuAppLocal.Tests.exe
10,911,232 10,915,328 +4,096 +0.038% System.Runtime.Intrinsics.Tests.exe
17,565,184 17,571,840 +6,656 +0.038% System.IO.FileSystem.Watcher.Tests.exe
17,697,280 17,703,936 +6,656 +0.038% System.Formats.Tar.Manual.Tests.exe
17,568,768 17,575,424 +6,656 +0.038% System.Buffers.Tests.exe
15,013,376 15,019,008 +5,632 +0.038% System.Drawing.Primitives.Tests.exe
13,780,480 13,785,600 +5,120 +0.037% Microsoft.Extensions.Configuration.Functional.Tests.exe
12,339,200 12,343,808 +4,608 +0.037% System.Xml.Linq.SDMSample.Tests.exe
18,496,512 18,503,168 +6,656 +0.036% ComInterfaceGenerator.Tests.exe
20,134,400 20,141,568 +7,168 +0.036% System.Runtime.InteropServices.Tests.exe
19,648,000 19,654,656 +6,656 +0.034% System.Resources.ResourceManager.Tests.exe
17,065,984 17,071,616 +5,632 +0.033% System.Runtime.Serialization.Formatters.Disabled.Tests.exe
18,816,512 18,822,656 +6,144 +0.033% System.Runtime.Caching.Tests.exe
15,371,264 15,376,384 +5,120 +0.033% System.Globalization.Extensions.Tests.exe
15,371,264 15,376,384 +5,120 +0.033% System.Globalization.Extensions.Nls.Tests.exe
19,898,880 19,892,224 -6,656 -0.033% System.Formats.Tar.Tests.exe
9,318,912 9,321,984 +3,072 +0.033% System.IO.IsolatedStorage.Tests.exe
25,993,728 26,001,920 +8,192 +0.032% System.Globalization.Tests.exe
20,818,944 20,825,600 +6,656 +0.032% System.Runtime.Extensions.Tests.exe
4,902,912 4,904,448 +1,536 +0.031% coreclr.dll
44,648,448 44,662,272 +13,824 +0.031% System.Runtime.Tests.exe
18,029,568 18,035,200 +5,632 +0.031% System.Diagnostics.TextWriterTraceListener.Tests.exe
15,541,760 15,546,368 +4,608 +0.030% System.Linq.Tests.exe
18,760,704 18,766,336 +5,632 +0.030% System.Numerics.Vectors.Tests.exe
20,352,000 20,358,144 +6,144 +0.030% System.Reflection.Tests.exe
17,754,112 17,759,232 +5,120 +0.029% System.Collections.Tests.exe
9,639,936 9,642,496 +2,560 +0.027% System.IO.UnmanagedMemoryStream.Tests.exe
14,952,448 14,956,544 +4,096 +0.027% System.Data.DataSetExtensions.Tests.exe
15,004,160 15,008,256 +4,096 +0.027% System.Security.Cryptography.Cng.Tests.exe
19,990,016 19,995,136 +5,120 +0.026% Microsoft.Extensions.Configuration.Json.Tests.exe
16,018,432 16,022,528 +4,096 +0.026% System.Data.Odbc.Tests.exe
25,904,128 25,910,784 +6,656 +0.026% System.Globalization.Nls.Tests.exe
13,856,768 13,860,352 +3,584 +0.026% Microsoft.Extensions.Configuration.Xml.Tests.exe
26,505,728 26,511,872 +6,144 +0.023% Microsoft.Extensions.Logging.EventSource.Tests.exe
19,930,112 19,934,720 +4,608 +0.023% Microsoft.Extensions.Configuration.UserSecrets.Tests.exe
22,298,112 22,303,232 +5,120 +0.023% System.Security.Cryptography.Pkcs.Tests.exe
33,289,728 33,296,896 +7,168 +0.022% Microsoft.Bcl.Cryptography.Tests.exe
20,789,248 20,793,856 +4,608 +0.022% System.Security.Cryptography.Csp.Tests.exe
32,596,480 32,602,624 +6,144 +0.019% System.Linq.Expressions.Tests.exe
13,587,456 13,588,992 +1,536 +0.011% crossgen2.exe
21,508,096 21,510,144 +2,048 +0.010% System.Data.OleDb.Tests.exe
10,560,512 10,560,000 -512 -0.005% Common.Tests.exe
1,935,296 1,935,296 0 0.000% Microsoft.DiaSymReader.Native.x86.dll
2,309,152 2,309,152 0 0.000% Microsoft.DiaSymReader.Native.amd64.dll
108,032 108,032 0 0.000% CMakeCXXCompilerId.exe
84,480 84,480 0 0.000% System.Globalization.Native.dll
2,247,784 2,247,784 0 0.000% Microsoft.DiaSymReader.Native.arm64.dll
1,754,512 1,754,512 0 0.000% Microsoft.DiaSymReader.Native.arm.dll
1,377,280 1,377,280 0 0.000% System.IO.Compression.Native.dll
1,935,296 1,935,296 0 0.000% Microsoft.DiaSymReader.Native.x86.dll
2,247,784 2,247,784 0 0.000% Microsoft.DiaSymReader.Native.arm64.dll
7,457,280 7,457,280 0 0.000% XUnitLogChecker.exe
1,754,512 1,754,512 0 0.000% Microsoft.DiaSymReader.Native.arm.dll
108,032 108,032 0 0.000% CMakeCCompilerId.exe
2,309,152 2,309,152 0 0.000% Microsoft.DiaSymReader.Native.amd64.dll
536,096 536,096 0 0.000% msquic.dll
1,935,296 1,935,296 0 0.000% Microsoft.DiaSymReader.Native.x86.dll
307,200 307,200 0 0.000% clretwrc.dll
1,935,296 1,935,296 0 0.000% Microsoft.DiaSymReader.Native.x86.dll
2,309,152 2,309,152 0 0.000% Microsoft.DiaSymReader.Native.amd64.dll
2,247,784 2,247,784 0 0.000% Microsoft.DiaSymReader.Native.arm64.dll
1,754,512 1,754,512 0 0.000% Microsoft.DiaSymReader.Native.arm.dll
1,935,296 1,935,296 0 0.000% Microsoft.DiaSymReader.Native.x86.dll
678,400 678,400 0 0.000% clrgc.dll
2,247,784 2,247,784 0 0.000% Microsoft.DiaSymReader.Native.arm64.dll
2,309,152 2,309,152 0 0.000% Microsoft.DiaSymReader.Native.amd64.dll
7,809,536 7,809,536 0 0.000% ilasm.exe
9,956,864 9,956,864 0 0.000% llvm-mca.exe
696,320 696,320 0 0.000% FileCheck.exe
1,377,280 1,377,280 0 0.000% System.IO.Compression.Native.dll
84,480 84,480 0 0.000% System.Globalization.Native.dll
1,754,512 1,754,512 0 0.000% Microsoft.DiaSymReader.Native.arm.dll
696,320 696,320 0 0.000% FileCheck.exe
723,968 723,968 0 0.000% clrgcexp.dll
62,464 62,464 0 0.000% createdump.exe
2,309,152 2,309,152 0 0.000% Microsoft.DiaSymReader.Native.amd64.dll
2,247,784 2,247,784 0 0.000% Microsoft.DiaSymReader.Native.arm64.dll
1,754,512 1,754,512 0 0.000% Microsoft.DiaSymReader.Native.arm.dll
1,935,296 1,935,296 0 0.000% Microsoft.DiaSymReader.Native.x86.dll
2,247,784 2,247,784 0 0.000% Microsoft.DiaSymReader.Native.arm64.dll
1,754,512 1,754,512 0 0.000% Microsoft.DiaSymReader.Native.arm.dll
210,944 210,944 0 0.000% corerun.exe
2,309,152 2,309,152 0 0.000% Microsoft.DiaSymReader.Native.amd64.dll
536,096 536,096 0 0.000% msquic.dll
1,287,680 1,287,680 0 0.000% mscordbi.dll
1,463,808 1,463,808 0 0.000% mscordaccore.dll
1,463,808 1,463,808 0 0.000% mscordaccore_amd64_amd64_42.42.42.42424.dll
361,984 361,984 0 0.000% hostpolicy.dll
350,208 350,208 0 0.000% hostfxr.dll
1,377,280 1,377,280 0 0.000% System.IO.Compression.Native.dll
9,956,864 9,956,864 0 0.000% llvm-mca.exe

cc @AndyAyersMS @VSadov @jkotas @davidwrighton @MichalStrehovsky @jtschuster @rcj1

Copilot AI review requested due to automatic review settings June 8, 2026 14:45
Comment thread src/coreclr/vm/prestub.cpp Outdated
}

if (TryGenerateAsyncThunk(resolver, methodILDecoder))
if (TryGenerateUnsafeAccessor(resolver, methodILDecoder))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need to swap the order here? Unsafe accessors are less common than async thunks, so this is deoptimization.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The situation from the test looks like:

public class PrivateAsync
{
    private static async Task a_task1(int i)
    {
    }
}

public class PrivateAsync2
{
    private static async Task a_task2(int i)
    {
        return await Accessors1.accessor(null, i - 1);
    }
}

public class Accessors1
{
    [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "a_task1")]
    public extern static Task accessor(PrivateAsync1 o, int i);
}

public class Accessors2
{
    [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "a_task2")]
    public extern static Task accessor(PrivateAsync2 o, int i);
}

The call in a_task2 ends up requesting an async variant of Accessors1.accessor. That was going into this path and hitting TryGenerateAsyncThunk, which ends up creating the delegating thunks that we should be able to remove the code for with this PR. If we swap the order we instead create an async unsafe accessor that directly calls the async variant of a_task1.

Let me try an alternative which instead compiles the async variant of Accessors1.accessor as an async version that is passed the IL of the Task unsafe accessor. Then we shouldn't end up in this path at all, but it needs a bit more refactoring.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Actually there is a simpler fix in TryGenerateAsyncThunk

Copy link
Copy Markdown
Contributor

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

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

Comment thread src/coreclr/inc/corinfo.h
Comment on lines +3143 to +3146
// Get information about which await call to use to await the return type
// of the non-async version of an async call.
virtual CORINFO_METHOD_HANDLE getAwaitReturnCall(CORINFO_METHOD_HANDLE callerHandle, CORINFO_LOOKUP* instArg) = 0;

Comment on lines 686 to 693
// For a Runtime Async method the methoddef maps to a Task-returning thunk with runtime-provided implementation,
// while the default IL belongs to the Async implementation variant.
// By default the config captures the default methoddesc, which would be a thunk, thus no IL header.
// So, if config provides no header and we see an implementation method desc, then just ask the method desc itself.
if (ilHeader == NULL && pMD->IsAsyncVariantMethod())
{
_ASSERTE(!pMD->IsAsyncThunkMethod());
ilHeader = pMD->GetILHeader();
}
Comment thread src/coreclr/vm/method.hpp
Comment on lines 1544 to 1550
BOOL MayHaveILHeader()
{
LIMITED_METHOD_DAC_CONTRACT;

// methods with transient IL bodies do not have IL headers
return IsIL() && !IsUnboxingStub() && !IsAsyncThunkMethod();
return IsIL() && !IsUnboxingStub() && (!IsAsyncThunkMethod() || SupportsAsyncVersionCodegen());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI runtime-async

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[runtime-async] Optimize synchronous Task-returning wrappers used in async context

7 participants