Compile runtime async versions of synchronous task-returning methods#128384
Compile runtime async versions of synchronous task-returning methods#128384jakobbotsch wants to merge 106 commits into
Conversation
There was a problem hiding this comment.
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
AsyncHelpersto separate “suspend” helpers from typedTransparentAwait(...)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. |
|
Another pattern we may want to recognize: |
| [BypassReadyToRun] | ||
| [MethodImpl(MethodImplOptions.Async)] | ||
| private static void TransparentAwaitWithResult(Task task) |
| return opts.jitFlags->IsSet(JitFlags::JIT_FLAG_ASYNC); | ||
| } | ||
|
|
||
| // Is this the async version of a non-async method? IL belongs to non-async method. |
There was a problem hiding this comment.
Can there be suspension points?
Are all awaits tail awaits in these methods?
There was a problem hiding this comment.
While all awaits are in tail position, they still sometimes do result in suspension points:
- When not optimizing
- When
NoInliningis 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 fromTaskmethod cannot become a tail-await
There was a problem hiding this comment.
I see. It may have suspension points, but it is transparent (in context popping sense), so it can use tail awaits where possible.
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
IsAsync here means "async in IL sense" and thus RequiresSaveRestoreOfAsyncContexts does not include the new async versions of sync methods. Correct?
There was a problem hiding this comment.
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.
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
|
| // 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; |
There was a problem hiding this comment.
@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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
`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.
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.