Skip to content

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

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

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

Conversation

@jakobbotsch

@jakobbotsch jakobbotsch commented May 19, 2026

Copy link
Copy Markdown
Member

Instead of delegating from runtime async callable thunks to the original task returning methods this PR compiles a fully separate runtime async version. The JIT gets passed the IL of the synchronous task-returning method and is then responsible for adapting that to fit with the async variant. It does so by adding a (guaranteed) optimization to make tail calls in the synchronous task-returning methods into runtime async calls, and by otherwise introducing an async call to AsyncHelpers.TransparentAwaitWithResult around the task that would have otherwise been returned.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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

Copy link
Copy Markdown
Member Author

I added the:

if (instArg.lookupKind.runtimeLookupKind == CORINFO_RUNTIME_LOOKUP_KIND.CORINFO_LOOKUP_NOT_SUPPORTED)
                    return null;

check in my commit, but maybe that's something to be handled on the RyuJIT side instead. I was hoping that would fix this, but it didn't.

Ended up removing the check and then handling this on the JIT side (it was already handled, just not checked for where we use getAwaitReturnCall)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 51 out of 51 changed files in this pull request and generated 6 comments.

Comment thread src/coreclr/vm/jitinterface.cpp
Comment thread src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
Comment thread src/libraries/Directory.Build.targets
Comment thread eng/testing/tests.targets
Comment thread src/coreclr/inc/corinfo.h
Comment on lines +158 to +161

// We allow MethodDesc so that this is usable with e.g. async variants too,
// but the owning module needs to be an EcmaModule.
_module = ((EcmaType)method.OwningType).Module;

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.

If we should only use this on EcmaMethods and AsyncMethodVariants, could we assert that it's a method definition?

Suggested change
// We allow MethodDesc so that this is usable with e.g. async variants too,
// but the owning module needs to be an EcmaModule.
_module = ((EcmaType)method.OwningType).Module;
Debug.Assert(method.IsMethodDefinition);
// We allow MethodDesc so that this is usable with e.g. async variants too,
// but the owning module needs to be an EcmaModule.
_module = ((EcmaType)method.OwningType).Module;

Comment on lines +365 to +367
MethodIL wrappedIL = asyncVariantImpl.IsAsync
? EcmaMethodIL.Create(asyncVariantImpl.Target)
: GetMethodIL(asyncVariantImpl.Target);

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.

AsyncVariantMethod.Target is an EcmaMethod, so GetMethodIL() should always return EcmaMethodIL. Could we cast here and keep AsyncMethodIL._wrappedIL as EcmaMethodIL?

Suggested change
MethodIL wrappedIL = asyncVariantImpl.IsAsync
? EcmaMethodIL.Create(asyncVariantImpl.Target)
: GetMethodIL(asyncVariantImpl.Target);
EcmaMethodIL wrappedIL = asyncVariantImpl.IsAsync
? EcmaMethodIL.Create(asyncVariantImpl.Target)
: (EcmaMethodIL)GetMethodIL(asyncVariantImpl.Target);

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.

AsyncVariantMethod.Target is an EcmaMethod, so GetMethodIL() should always return EcmaMethodIL. Could we cast here and keep AsyncMethodIL._wrappedIL as EcmaMethodIL?

EcmaMethod can have IL that is not EcmaMethodIL. The place I hit it with is UnsafeAccessor, but there are more exotic ones too:

if (method is EcmaMethod ecmaMethod)
{
if (ecmaMethod.IsIntrinsic)
{
MethodIL result = TryGetIntrinsicMethodIL(ecmaMethod);
if (result != null)
return result;
}
if (ecmaMethod.IsRuntimeImplemented)
{
MethodIL result = TryGetRuntimeImplementedMethodIL(ecmaMethod);
if (result != null)
return result;
}
if (ecmaMethod.IsAsync)
{
if (ecmaMethod.Signature.ReturnsTaskOrValueTask())
{
return AsyncThunkILEmitter.EmitTaskReturningThunk(ecmaMethod, ((CompilerTypeSystemContext)ecmaMethod.Context).GetAsyncVariantMethod(ecmaMethod));
}
else
{
// We only allow non-Task returning runtime async methods in CoreLib
if (ecmaMethod.OwningType.Module != ecmaMethod.Context.SystemModule)
ThrowHelper.ThrowBadImageFormatException();
}
}
MethodIL methodIL = EcmaMethodIL.Create(ecmaMethod);
if (methodIL != null)
return methodIL;
methodIL = UnsafeAccessors.TryGetIL(ecmaMethod);
if (methodIL != null)
return methodIL;
return null;
}

@jakobbotsch

Copy link
Copy Markdown
Member Author

I think all the failures are expected. Going to push a commit where the tests are runtime async but the libraries aren't, to get some more testing of a mix of async variants.

Copilot AI review requested due to automatic review settings June 16, 2026 15:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 50 out of 50 changed files in this pull request and generated 3 comments.

Comment thread src/coreclr/inc/corinfo.h
Comment thread src/coreclr/vm/jitinterface.cpp
Comment thread src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
Copilot AI review requested due to automatic review settings June 17, 2026 12:00
@jakobbotsch

Copy link
Copy Markdown
Member Author

Testing looked good. Reverted the disabling of runtime-async and also added handling for tail. and jmp IL instructions to JIT and interpreter.

Still looking for reviews here, @AndyAyersMS can you PTAL at the JIT side, @VSadov @jkotas at the VM/NativeAOT side (@MichalStrehovsky wrote most of that), @BrzVlad @davidwrighton at the interpreter side?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 49 out of 49 changed files in this pull request and generated 4 comments.

Comment thread src/coreclr/jit/importer.cpp Outdated
Comment thread src/coreclr/jit/importer.cpp Outdated
Comment thread src/coreclr/jit/importer.cpp Outdated
Comment thread src/coreclr/inc/corinfo.h

@BrzVlad BrzVlad left a comment

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.

Interpreter bits LGTM

Copilot AI review requested due to automatic review settings June 17, 2026 20:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 50 out of 50 changed files in this pull request and generated 3 comments.

Comment thread src/coreclr/inc/corinfo.h
Comment on lines +3144 to +3147
// 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 +4534 to +4537
printf("GetAwaitReturnCall key %016" PRIX64 " value methodHnd-%016" PRIX64 " instArg %s",
key,
value.methodHnd,
SpmiDumpHelper::DumpAgnostic_CORINFO_LOOKUP(value.instArg).c_str());
Comment on lines +10469 to +10473
// Compute the runtime lookup for the instantiation argument for an
// AsyncHelpers.TransparentAwait call, to be used for wrapping a return value
// from pCallerMD for its runtime async version.
// For example, if pCallerMD is ValueTask<List<T>> Foo<T>(), then we call this
// for the runtime async version of Foo and it gives a runtime lookup that
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