Uh oh!
There was an error while loading. Please reload this page.
Restore ExecutionContext from dispatcher instead of from interpreter/JIT'ed code - #127511
Conversation
Remove `RestoreExecutionContext` call from JIT resumption code and from the interpreter. Instead encode the `ExecutionContext`'s location in the continuation and let the dispatcher restore it. This removes a TLS access + helper call from the resumption path (avoiding the TLS and inlining the helper call in the dispatcher). Improves suspension/resumption performance by about 5-6% and should also be a small size improvement.
Tagging subscribers to this area: @agocke |
There was a problem hiding this comment.
Pull request overview
This PR changes CoreCLR runtime-async suspension/resumption so the dispatcher (managed AsyncHelpers) restores ExecutionContext, instead of having interpreter/JIT resumption stubs call a restore helper. The continuation’s flags now encode the ExecutionContext storage slot, enabling the dispatcher to restore without an extra TLS lookup + helper call on the hot resumption path.
Changes:
- Add continuation support for locating/storing the
ExecutionContextvia encoded indices in continuation flags (VM + managed + JIT + interpreter). - Remove
RestoreExecutionContextcalls from interpreter and JIT resumption code; add restoration to the dispatcher loop. - Preserve
BBF_ASYNC_RESUMPTIONacross certain flow optimizations that may bypass resumption stubs.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/vm/object.h | Adds VM helper to locate ExecutionContext storage in a continuation via encoded flag bits. |
| src/coreclr/vm/interpexec.cpp | Writes captured ExecutionContext via the encoded slot; removes interpreter-side restore on resume. |
| src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs | Moves ExecutionContext restore into dispatcher; adjusts flag layout and RestoreExecutionContext signature to avoid TLS lookup. |
| src/coreclr/jit/fgopt.cpp | Copies BBF_ASYNC_RESUMPTION during certain jump/switch/flow optimizations to preserve async side-entry affordances. |
| src/coreclr/jit/async.cpp | Updates continuation layout/flag encoding for ExecutionContext; removes JIT-side restore on resumption. |
| src/coreclr/interpreter/inc/interpretershared.h | Removes interpreter suspend-data field that stored a raw execution-context offset. |
| src/coreclr/interpreter/compiler.cpp | Encodes ExecutionContext slot in flags and updates interpreter continuation layout accordingly. |
| src/coreclr/inc/corinfo.h | Updates CorInfoContinuationFlags bit assignments to include ExecutionContext index encoding. |
Comments suppressed due to low confidence (2)
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs:554
RuntimeAsyncStackStatecontains managed reference fields, but this local is left uninitialized before its address is stored in TLS (awaitState.Push(&stackState)). This can expose garbage references to the GC (andPushonly initializes a subset of fields), risking GC crashes/corruption. InitializestackStatetodefault(or otherwise fully clear all reference fields before any GC safepoint) before callingPush.
RuntimeAsyncStackState stackState;
ref RuntimeAsyncAwaitState awaitState = ref t_runtimeAsyncAwaitState;
awaitState.Push(&stackState);
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs:655
- Same issue as in
DispatchContinuations:RuntimeAsyncStackState stackState;is uninitialized even though the struct contains managed references and its address is pushed into TLS viaawaitState.Push(&stackState). This can leave garbage object references visible to the GC until fields are explicitly set. EnsurestackStateis zero-initialized (e.g.,= default) or otherwise cleared before pushing.
RuntimeAsyncStackState stackState;
ref RuntimeAsyncAwaitState awaitState = ref t_runtimeAsyncAwaitState;
awaitState.Push(&stackState);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/coreclr/interpreter/compiler.cpp:6007
InterpAsyncSuspendData.restoreExecutionContextMethodis still populated, but after removing the interpreter-sideRestoreExecutionContextcall there don’t appear to be any remaining reads of this field. Consider removing the field fromInterpAsyncSuspendData(and this assignment) to reduce per-suspension metadata size and avoid dead state.
suspendData->keepAliveOffset = keepAliveOffset + OFFSETOF__CORINFO_Continuation__data;
suspendData->captureSyncContextMethod = asyncInfo.captureContinuationContextMethHnd;
suspendData->restoreExecutionContextMethod = asyncInfo.restoreExecutionContextMethHnd;
suspendData->restoreContextsOnSuspensionMethod = asyncInfo.restoreContextsOnSuspensionMethHnd;
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 16 out of 16 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (1)
src/coreclr/interpreter/compiler.cpp:6007
CORINFO_ASYNC_INFOno longer exposesrestoreExecutionContextMethHnd, but the interpreter still storesasyncInfo.restoreExecutionContextMethHndintoInterpAsyncSuspendData. This will fail to compile and should be removed/rewired now that execution context restoration is done by the dispatcher.
suspendData->keepAliveOffset = keepAliveOffset + OFFSETOF__CORINFO_Continuation__data;
suspendData->captureSyncContextMethod = asyncInfo.captureContinuationContextMethHnd;
suspendData->restoreExecutionContextMethod = asyncInfo.restoreExecutionContextMethHnd;
suspendData->restoreContextsOnSuspensionMethod = asyncInfo.restoreContextsOnSuspensionMethHnd;
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
jakobbotsch
commented
Apr 30, 2026
PTAL @VSadov (async) @AndyAyersMS (JIT) @davidwrighton /@janvorli (interpreter) |
Uh oh!
There was an error while loading. Please reload this page.
jakobbotsch
commented
May 4, 2026
Will merge this, @janvorli@davidwrighton let me know if you have any comments on the interpreter change and I can address it in a follow-up. |
Remove
RestoreExecutionContextcall from JIT resumption code and from the interpreter. Instead encode theExecutionContext's location in the continuation and let the dispatcher restore it. This removes a TLS access + helper call from the resumption path (avoiding the TLS and inlining the helper call in the dispatcher).Improves suspension/resumption performance by about 7-8% and is also a good size improvement. Removing the call from the resumption path allows keeping the continuation in a volatile register, which also helps register allocation in a number of cases.
Diffs before JIT-EE GUID update
Micro benchmark with warmup
About 7-8% improvement:
NativeAOT test sizes