Compile runtime async versions of synchronous task-returning methods#128384
Compile runtime async versions of synchronous task-returning methods#128384jakobbotsch wants to merge 122 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: |
Ended up removing the check and then handling this on the JIT side (it was already handled, just not checked for where we use |
|
|
||
| // 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; |
There was a problem hiding this comment.
If we should only use this on EcmaMethods and AsyncMethodVariants, could we assert that it's a method definition?
| // 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; |
| MethodIL wrappedIL = asyncVariantImpl.IsAsync | ||
| ? EcmaMethodIL.Create(asyncVariantImpl.Target) | ||
| : GetMethodIL(asyncVariantImpl.Target); |
There was a problem hiding this comment.
AsyncVariantMethod.Target is an EcmaMethod, so GetMethodIL() should always return EcmaMethodIL. Could we cast here and keep AsyncMethodIL._wrappedIL as EcmaMethodIL?
| MethodIL wrappedIL = asyncVariantImpl.IsAsync | |
| ? EcmaMethodIL.Create(asyncVariantImpl.Target) | |
| : GetMethodIL(asyncVariantImpl.Target); | |
| EcmaMethodIL wrappedIL = asyncVariantImpl.IsAsync | |
| ? EcmaMethodIL.Create(asyncVariantImpl.Target) | |
| : (EcmaMethodIL)GetMethodIL(asyncVariantImpl.Target); |
There was a problem hiding this comment.
AsyncVariantMethod.Targetis anEcmaMethod, soGetMethodIL()should always returnEcmaMethodIL. Could we cast here and keepAsyncMethodIL._wrappedILasEcmaMethodIL?
EcmaMethod can have IL that is not EcmaMethodIL. The place I hit it with is UnsafeAccessor, but there are more exotic ones too:
runtime/src/coreclr/tools/Common/TypeSystem/IL/NativeAotILProvider.cs
Lines 283 to 322 in 3d40d73
|
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. |
|
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? |
| // 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; | ||
|
|
| printf("GetAwaitReturnCall key %016" PRIX64 " value methodHnd-%016" PRIX64 " instArg %s", | ||
| key, | ||
| value.methodHnd, | ||
| SpmiDumpHelper::DumpAgnostic_CORINFO_LOOKUP(value.instArg).c_str()); |
| // 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 |
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.TransparentAwaitWithResultaround the task that would have otherwise been returned.Fix #115771