Skip to content

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

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

Compile runtime async versions of synchronous task-returning methods#128384
jakobbotsch wants to merge 106 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. 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

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

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

Comment thread src/coreclr/inc/corinfo.h
Comment on lines +667 to +669
[BypassReadyToRun]
[MethodImpl(MethodImplOptions.Async)]
private static void TransparentAwaitWithResult(Task task)
Comment thread src/coreclr/inc/corinfo.h
Comment thread src/coreclr/jit/compiler.cpp Outdated
return opts.jitFlags->IsSet(JitFlags::JIT_FLAG_ASYNC);
}

// Is this the async version of a non-async method? IL belongs to non-async method.

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.

Can there be suspension points?
Are all awaits tail awaits in these methods?

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.

While all awaits are in tail position, they still sometimes do result in suspension points:

  • When not optimizing
  • When NoInlining is applied we should do that too since that's often used to ensure a method shows up in stack traces (we disable normal tailcalling based on this)
  • Due to covariance a tail await may not be legal, e.g. Task<int> returned from Task method cannot become a tail-await

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.

I see. It may have suspension points, but it is transparent (in context popping sense), so it can use tail awaits where possible.

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.

Right

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.

The PR description says: It then adds a guaranteed optimization to make tail calls in the synchronous task-returning methods into runtime async calls.

It is not "guaranteed" then. More like "highly likely".

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.

It isn't guaranteed that they become tail awaits, but it is guaranteed that eligible calls become runtime async calls.


public static bool RequiresSaveRestoreOfAsyncContexts(this MethodDesc method)
{
return method.IsAsync && method.IsAsyncVariant();

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.

IsAsync here means "async in IL sense" and thus RequiresSaveRestoreOfAsyncContexts does not include the new async versions of sync methods. Correct?

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.

Correct. This was just factoring out the test since I noticed the IL scanner was a bit imprecise with this, and to align it with the VM.

@VSadov

VSadov commented Jun 9, 2026

Copy link
Copy Markdown
Member

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.

They are different implementation strategy of what was async thunks. I think the issue is just in naming.

In a sense - they are still "synthetic methods with async call convention and the same functionality as corresponding sync method". Except they no longer do that by forwarding to the sync method, but by embedding it entirely and unwrapping (or not wrapping in the first place) the return values.

I guess they are not thunks, since they do not forward. Maybe we can come up with a new term that means "synthetic parallel implementation" that does not imply forwarding?

Maybe IsAsyncEntry?
(As in "async entry point for a sync method". How much of original method is embedded is implementation detail)

IsAsyncAlternative?

// we cannot resolve to an Async variant in such case.
// return NULL, so that caller would re-resolve as a regular method call
pMD = pMD->ReturnsTaskOrValueTask() ? pMD->GetAsyncVariant(/*allowInstParam*/FALSE) : NULL;
pMD = pMD->ReturnsTaskOrValueTask() && !pMD->IsCLRToCOMCall() ? pMD->GetAsyncVariant(/*allowInstParam*/FALSE) : NULL;

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.

@AaronRobinsonMSFT @jkotas When I removed EmitAsyncMethodThunk I ran into the problem that we were relying on that transient IL when calling an async variant of one of these, and the non-async variant does not have any IL so we cannot create an async version of it.

I wonder if you think this is a sufficient fix. Can we guarantee that we never try to call the async variant of these? If not do you have other suggestions for solving this problem, or should I bring back EmitAsyncMethodThunk?

@VSadov VSadov Jun 11, 2026

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.

It appears that what we have is basically the same thunk as before, but with the call to sync variant aggressively inlined and then applying tail-awaiting optimizations in the thunk where possible.
Maybe it is ok to keep calee uninlined into a thunk in some cases? (i.e. if sync variant has no IL)

@VSadov VSadov Jun 11, 2026

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.

Actually - why not simply keep the thunk as it was, but mark somehow the sync call in it as AggressivelyInline and do the same tail-await optimization after inlining?

Is it a requirement to do the optimization in importer - like it must happen before async lowering, for example?
(I am mostly just wondering why we choose one approach vs. another)

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.

That was my original suggestion in #115771, see #115771 (comment) for some more discussion around it. Basically we wanted this optimization to be guaranteed for tier0 too.

Copilot AI review requested due to automatic review settings June 10, 2026 15:42

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 44 out of 44 changed files in this pull request and generated 1 comment.

Comment thread src/coreclr/vm/jitinterface.cpp Outdated
`EcmaMethodIL.Create` bypasses unsafe accessor handling, so call `GetMethodIL` recursively to compensate for them or anything else we might be special casing.
The `scope ??=` path is reached when the method is abstract and the `GetMethodIL` above didn't return anything. We don't strictly need `EcmaMethodILScope` to be for an ECMA method, we just need to be able to resolve tokens in such scope.
Copilot AI review requested due to automatic review settings June 11, 2026 03: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 46 out of 46 changed files in this pull request and generated 1 comment.

Comment thread src/coreclr/inc/corinfo.h
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

9 participants