Remove boxing for awaited custom awaiters - #131342
Conversation
|
Azure Pipelines: Successfully started running 6 pipeline(s). 10 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Pull request overview
This PR extends CoreCLR runtime-async to avoid boxing custom struct awaiters on suspension by storing the awaiter value into the continuation object and later invoking OnCompleted/UnsafeOnCompleted by extracting the awaiter from that continuation using a JIT-computed byte offset. It introduces a symbolic “continuation member offset” node so the importer can reference the eventual offset before the async transformation finalizes the continuation layout, and adds a new JIT↔EE query to resolve the specialized helper method.
Changes:
- Add new CoreLib helpers to await via “awaiter-in-continuation” with an offset, and update suspension handling to call
OnCompleted/UnsafeOnCompletedby reading the awaiter out of the continuation. - Add
GT_CONTINUATION_MEMBER_OFFSETplus continuation-member layout plumbing in the JIT async transformation; update importer to rewrite struct-await helper calls to the new helper and record the member offset. - Extend the JIT↔EE interface and supporting tooling (VM, SuperPMI, ILC/ReadyToRun) to resolve the new helper method; add a regression test covering safe/unsafe custom awaiters.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/tests/async/struct/struct.cs | Adds a regression test for safe and unsafe custom struct awaiters under runtime async. |
| src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs | Adds new private awaiter-in-continuation helpers and extraction-based OnCompleted/UnsafeOnCompleted paths. |
| src/coreclr/vm/jitinterface.h | Adds VM declaration for the new JIT↔EE query to resolve the awaiter-in-continuation helper. |
| src/coreclr/vm/jitinterface.cpp | Implements helper resolution + runtime lookup computation for the new awaiter-in-continuation call. |
| src/coreclr/vm/corelib.h | Adds CoreLibBinder method IDs for the new AsyncHelpers helper methods. |
| src/coreclr/tools/superpmi/superpmi/icorjitinfo.cpp | Plumbs the new ICorJitInfo method through SuperPMI. |
| src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cpp | Updates generated shim to forward the new ICorJitInfo entrypoint. |
| src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cpp | Updates generated counter shim to track/forward the new call. |
| src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cpp | Updates collector shim to record/replay the new query. |
| src/coreclr/tools/superpmi/superpmi-shared/methodcontext.h | Adds recording/replay declarations and a new packet kind for the query. |
| src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cpp | Implements record/dump/replay for the new query. |
| src/coreclr/tools/superpmi/superpmi-shared/lwmlist.h | Adds LWM map entry for the query. |
| src/coreclr/tools/superpmi/superpmi-shared/agnostic.h | Adds agnostic key struct for recording the query inputs. |
| src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txt | Adds the new API to the JIT interface thunk generator input. |
| src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs | Adds ILC implementation to resolve the new helper method handle. |
| src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.cs | Adds generated unmanaged callback plumbing for the new API. |
| src/coreclr/tools/aot/jitinterface/jitinterface_generated.h | Updates NativeAOT jitinterface wrapper with the new callback. |
| src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.cs | Ensures R2R token availability for the new helper methods. |
| src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs | Adds runtime stack-state fields + dispatcher logic to call awaiter continuation callbacks. |
| src/coreclr/jit/wellknownargs.h | Introduces a new well-known arg kind for the async awaiter pseudo-arg. |
| src/coreclr/jit/importercalls.cpp | Adds importer rewrite to swap struct awaiter calls to the new helper and attach symbolic continuation-member offset. |
| src/coreclr/jit/ICorJitInfo_wrapper_generated.hpp | Adds wrapper method for the new JIT↔EE query. |
| src/coreclr/jit/ICorJitInfo_names_generated.h | Adds the new API name for logging/tracing. |
| src/coreclr/jit/gtstructs.h | Extends GenTreeVal struct mapping to include GT_CONTINUATION_MEMBER_OFFSET. |
| src/coreclr/jit/gtlist.h | Adds the GT_CONTINUATION_MEMBER_OFFSET node kind and documentation. |
| src/coreclr/jit/gentree.cpp | Updates pseudo-arg handling and debug printing for the new node. |
| src/coreclr/jit/compiler.h | Adds compiler state for tracking continuation members and the importer helper prototype. |
| src/coreclr/jit/async.h | Adds continuation-member abstractions and extends layout builder to allocate member offsets. |
| src/coreclr/jit/async.cpp | Implements continuation-member tracking, reserves storage in continuation layouts, and bashes symbolic offsets to constants. |
| src/coreclr/inc/jiteeversionguid.h | Bumps JIT↔EE version GUID due to interface change. |
| src/coreclr/inc/icorjitinfoimpl_generated.h | Adds the new virtual method to the generated ICorJitInfo impl surface. |
| src/coreclr/inc/corinfo.h | Adds the new ICorStaticInfo virtual method for helper resolution. |
|
/azp run runtime-coreclr jitstress, runtime-coreclr libraries-jitstress |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 76 out of 76 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs:3759
- The exception message thrown from getAwaitAwaiterInContinuationCall still says "getAwaitReturnCall", which is misleading for diagnostics and log triage (this path is specifically for awaiter-in-continuation).
throw new RequiresRuntimeJitException($"getAwaitReturnCall: runtime-determined exact instantiation requires runtime JIT ({runtimeDeterminedResult})");
Conflicts in the runtime async transformation with the new "resumed?" indicator variable from dotnet#128152: - async.h: combined the new AsyncResumedUse/AsyncResumedDef parameters and FindAndRemoveCommonAsyncResumedDef with the ContinuationLayout return value and continuation member offset output of CreateResumptionsAndSuspensions. - async.cpp: call FindAndRemoveCommonAsyncResumedDef before creating resumptions and suspensions and pass the result to CreateResumptionSwitch, and add AsyncAwaiter to the well known argument lists used when validating and removing arguments for reused continuations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 757a9e4c-44f3-4255-86f6-065ebb823ed8
…nostic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 757a9e4c-44f3-4255-86f6-065ebb823ed8
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 76 out of 76 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/coreclr/inc/corinfo.h:3191
- New ICorStaticInfo virtual method should be appended at the end of the interface to preserve vtable slot ordering. Even with the JIT/EE GUID bump, keeping existing slots stable reduces risk for out-of-tree JITs/tools and matches the runtime convention for these interfaces. Consider moving getAwaitAwaiterInContinuationCall to the end of ICorStaticInfo (just before the class closes), rather than inserting it before the diagnostic-method block.
// Get the method to use to await a struct awaiter that is stored inside the
// continuation instead of being boxed. 'pResolvedToken' is the resolved
// token of the AsyncHelpers.AwaitAwaiter/UnsafeAwaitAwaiter call site that
// is being replaced, and 'isUnsafe' indicates whether the unsafe variant is
// being replaced.
//
// Returns the method handle of the call to insert, or NULL if the
// transformation cannot be performed. 'contextHandle' is set to the context
// to use when inlining the call, exactly as getCallInfo would report it for
// a direct call to it (it may be an approximate/shared instantiation when
// 'instArg' requires a runtime lookup). 'instArg' is filled with the
// (potentially runtime-looked-up) instantiation argument that must be
// passed to the call.
virtual CORINFO_METHOD_HANDLE getAwaitAwaiterInContinuationCall(
CORINFO_METHOD_HANDLE callerHandle,
CORINFO_RESOLVED_TOKEN* pResolvedToken,
bool isUnsafe,
CORINFO_CONTEXT_HANDLE* contextHandle,
CORINFO_LOOKUP* instArg
) = 0;
| throw new InternalCompilerErrorException("System module failed to validate all types:" + Environment.NewLine | ||
| + string.Join(Environment.NewLine, _typeLoadValidationErrors.Select(e => e.type.ToString()))); |
There was a problem hiding this comment.
Had some random failure here (went away on a clean rebuild), but I didn't get any information about what the problem was so I expanded this a bit.
…iation arg ComputeRuntimeLookupForAwaitAwaiterInContinuationCall was hand rolling a MethodDescSlot dictionary signature that the shared helper already knows how to build. The resolved token of the AsyncHelpers.AwaitAwaiter/UnsafeAwaitAwaiter call being replaced has the same owning type and the same instantiation as the replacement method, so it can be passed straight through. The template method has to be the instantiating stub rather than the shared method desc, since that is what determines whether the helper sets ENCODE_METHOD_SIG_InstantiatingStub, and therefore whether the lookup produces the exact method desc or the shared one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 757a9e4c-44f3-4255-86f6-065ebb823ed8
It is a GenTreeVal like GT_RECORD_ASYNC_RESUME and GT_ASYNC_RESUME_INFO, but was missing from gtCloneExpr, so cloning one hit 'Cloning of node not supported'. Reachable whenever such a node ends up somewhere that gets cloned. The node is created during import and only bashed to a constant in the async transformation, which runs after rationalization, so it is live across all of the optimizer. Ported from 34a8784 on the async-inlining branch, where it was found by Fuzzlyn. Also add it to GenTree::Compare next to GT_ASYNC_RESUME_INFO. It was falling into the default case, which is conservatively correct but means two nodes for the same continuation member never compare equal. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 757a9e4c-44f3-4255-86f6-065ebb823ed8
|
Azure Pipelines: Successfully started running 6 pipeline(s). 10 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 75 out of 75 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:58
- Reading a struct awaiter out of the continuation via
Unsafe.As<byte, TAwaiter>creates a typed ref from abyteref, which can introduce alignment assumptions forTAwaiter(and is generally riskier for structs with stricter alignment). Prefer an explicit unaligned read when materializing the copy.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:115 - Same as above: extracting the struct awaiter via
Unsafe.As<byte, TAwaiter>can rely on alignment that isn't guaranteed by the byte-based offset computation. Use an explicit unaligned read when copying the awaiter out of the continuation buffer.
This optimizes calls to
AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter)with struct awaiters to instead call a new functionAsyncHelpers.UnsafeAwaitAwaiterInContinuation<TAwaiter>(int offset). The idea is that the JIT ensures that the awaiter will be present in the continuation at the specified offset. The laterUnsafeOnCompletedcall can then be done without any boxing by extracting it from the continuation.This introduces a new
GT_CONTINUATION_MEMBER_OFFSETwhich is used to solve the linking problem where we do not know the offset into the continuation until much later. The async transformation is responsible for replacing this node with a constant after it knows the offset.I have a couple of use cases for
GT_CONTINUATION_MEMBER_OFFSETin mind, so I have made the mechanism to represent the type of member easily expandable.This PR also removes configurable continuation reuse. This is now unconditionally enabled, to simplify the expansion of
GT_CONTINUATION_MEMBER_OFFSETnodes.Fix #119842
Microbenchmark
About 10% improvement, and more importantly, avoids the box that async1 also avoids to gain parity.