Remove boxing for awaited custom awaiters - #131342

Merged
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2
Aug 7, 2026
Merged

Remove boxing for awaited custom awaiters#131342
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jul 24, 2026

Copy link
Copy Markdown
Member

This optimizes calls to AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter) with struct awaiters to instead call a new function AsyncHelpers.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 later UnsafeOnCompleted call can then be done without any boxing by extracting it from the continuation.

This introduces a new GT_CONTINUATION_MEMBER_OFFSET which 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.
There is a new AsyncAwaiter pseudo arg passed to UnsafeAwaitAwaiterInContinuation, and expanded in the suspension path by the async transformation to be stored in the continuation at the right offset.

I have a couple of use cases for GT_CONTINUATION_MEMBER_OFFSET in mind, so I have made the mechanism to represent the type of member easily expandable (particularly inlining needs this as well).

This PR also removes configurable continuation reuse. This is now unconditionally enabled, to simplify the expansion of GT_CONTINUATION_MEMBER_OFFSET nodes.

Fix#119842

Microbenchmark

About 10% improvement, and more importantly, avoids the box that async1 also avoids to gain parity.

usingSystem;usingSystem.Diagnostics;usingSystem.Runtime.CompilerServices;usingSystem.Threading;usingSystem.Threading.Tasks;publicstaticclassProgram{staticActions_continuation;staticlongs_value;publicstaticvoidMain(){for(inti=0;i<10;i++){for(intj=0;j<100;j++){Taskt=Foo(100);while(!t.IsCompleted)s_continuation();}Thread.Sleep(100);}for(inti=0;i<50;i++){Taskt=Foo(10_000_000);while(!t.IsCompleted)s_continuation();}}privatestaticasyncTaskFoo(intn){s_value=0;Stopwatchtimer=Stopwatch.StartNew();for(inti=0;i<n;i++){awaitnewAwaiter(i);}if(n>100)Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);Trace.Assert(s_value==((long)n*(n-1))/2);}privatestructAwaiter:ICriticalNotifyCompletion{publicintX;publicAwaiter(intx)=>X=x;publicboolIsCompleted=>false;publicAwaiterGetAwaiter()=>this;publicvoidGetResult(){}publicvoidOnCompleted(Actioncontinuation){}publicvoidUnsafeOnCompleted(Actioncontinuation){s_value+=X;s_continuation=continuation;}}}
-Took 323 ms-Took 318 ms-Took 320 ms-Took 323 ms-Took 322 ms-Took 318 ms-Took 320 ms-Took 318 ms-Took 317 ms-Took 319 ms+Took 290 ms+Took 291 ms+Took 292 ms+Took 288 ms+Took 292 ms+Took 291 ms+Took 290 ms+Took 290 ms+Took 287 ms+Took 292 ms

CopilotAI review requested due to automatic review settings July 24, 2026 19:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 24, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI 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 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/UnsafeOnCompleted by reading the awaiter out of the continuation.
  • Add GT_CONTINUATION_MEMBER_OFFSET plus 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
FileDescription
src/tests/async/struct/struct.csAdds a regression test for safe and unsafe custom struct awaiters under runtime async.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.csAdds new private awaiter-in-continuation helpers and extraction-based OnCompleted/UnsafeOnCompleted paths.
src/coreclr/vm/jitinterface.hAdds VM declaration for the new JIT↔EE query to resolve the awaiter-in-continuation helper.
src/coreclr/vm/jitinterface.cppImplements helper resolution + runtime lookup computation for the new awaiter-in-continuation call.
src/coreclr/vm/corelib.hAdds CoreLibBinder method IDs for the new AsyncHelpers helper methods.
src/coreclr/tools/superpmi/superpmi/icorjitinfo.cppPlumbs the new ICorJitInfo method through SuperPMI.
src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cppUpdates generated shim to forward the new ICorJitInfo entrypoint.
src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cppUpdates generated counter shim to track/forward the new call.
src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cppUpdates collector shim to record/replay the new query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.hAdds recording/replay declarations and a new packet kind for the query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cppImplements record/dump/replay for the new query.
src/coreclr/tools/superpmi/superpmi-shared/lwmlist.hAdds LWM map entry for the query.
src/coreclr/tools/superpmi/superpmi-shared/agnostic.hAdds agnostic key struct for recording the query inputs.
src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txtAdds the new API to the JIT interface thunk generator input.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.csAdds ILC implementation to resolve the new helper method handle.
src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.csAdds generated unmanaged callback plumbing for the new API.
src/coreclr/tools/aot/jitinterface/jitinterface_generated.hUpdates NativeAOT jitinterface wrapper with the new callback.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.csEnsures R2R token availability for the new helper methods.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds runtime stack-state fields + dispatcher logic to call awaiter continuation callbacks.
src/coreclr/jit/wellknownargs.hIntroduces a new well-known arg kind for the async awaiter pseudo-arg.
src/coreclr/jit/importercalls.cppAdds importer rewrite to swap struct awaiter calls to the new helper and attach symbolic continuation-member offset.
src/coreclr/jit/ICorJitInfo_wrapper_generated.hppAdds wrapper method for the new JIT↔EE query.
src/coreclr/jit/ICorJitInfo_names_generated.hAdds the new API name for logging/tracing.
src/coreclr/jit/gtstructs.hExtends GenTreeVal struct mapping to include GT_CONTINUATION_MEMBER_OFFSET.
src/coreclr/jit/gtlist.hAdds the GT_CONTINUATION_MEMBER_OFFSET node kind and documentation.
src/coreclr/jit/gentree.cppUpdates pseudo-arg handling and debug printing for the new node.
src/coreclr/jit/compiler.hAdds compiler state for tracking continuation members and the importer helper prototype.
src/coreclr/jit/async.hAdds continuation-member abstractions and extends layout builder to allocate member offsets.
src/coreclr/jit/async.cppImplements continuation-member tracking, reserves storage in continuation layouts, and bashes symbolic offsets to constants.
src/coreclr/inc/jiteeversionguid.hBumps JIT↔EE version GUID due to interface change.
src/coreclr/inc/icorjitinfoimpl_generated.hAdds the new virtual method to the generated ICorJitInfo impl surface.
src/coreclr/inc/corinfo.hAdds the new ICorStaticInfo virtual method for helper resolution.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings July 24, 2026 19:57

CopilotAI 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 33 out of 33 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 13:41

CopilotAI 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 73 out of 73 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 15:31
Comment threadsrc/coreclr/jit/morph.cpp

@AndyAyersMSAndyAyersMS left a comment

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.

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?
  • Could the embedded awaiters hold onto GC refs longer than before?

@jtschusterjtschuster left a comment

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.

Just one question about test coverage, otherwise crossgen2 changes lgtm.

CopilotAI review requested due to automatic review settings August 4, 2026 10:46

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

Suppressed comments (1)

src/tests/async/custom-struct-awaiters/custom-struct-awaiters.cs:19

  • This xUnit test blocks on an async method via Run().Wait(). xUnit supports async test methods directly, and blocking waits can introduce deadlocks when a synchronization context is present (or hide hangs as AggregateException). Prefer returning Task from the [Fact] instead of blocking.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings August 4, 2026 10:55

CopilotAI 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 74 out of 74 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:117

  • Same as the safe path: this reads TAwaiter from raw continuation data via Unsafe.As, which can generate misaligned struct loads if the awaiter type has alignment requirements beyond pointer-size. Prefer Unsafe.ReadUnaligned<TAwaiter> here as well.
    src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:60
  • Unsafe.As<byte, TAwaiter> assumes the source address is suitably aligned for TAwaiter. Continuation member offsets are only guaranteed to be pointer-aligned, so on architectures with stricter alignment requirements this can result in misaligned struct loads. Use Unsafe.ReadUnaligned<TAwaiter> to safely read the awaiter from raw object data.
    src/coreclr/jit/async.cpp:2475
  • Stores into the continuation member don't currently propagate GTF_IND_UNALIGNED when the awaiter type's alignment exceeds the object/pointer alignment. Other continuation stores compute indirFlags based on required vs heap alignment; this path should do the same to avoid generating aligned stores to potentially unaligned offsets (especially for SIMD-containing structs).
 GenTreeFieldList* fieldList = awaiter->AsFieldList();
for (GenTreeFieldList::Use& use : fieldList->Uses())
{
if (!use.GetNode()->IsInvariant() && !use.GetNode()->OperIs(GT_LCL_VAR))
{
LIR::Use lirUse(LIR::AsRange(callBlock), &use.NodeRef(), fieldList);
lirUse.ReplaceWithLclVar(m_compiler);

src/coreclr/jit/async.cpp:2506

  • This struct store uses GTF_IND_NONFAULTING but omits GTF_IND_UNALIGNED when the awaiter layout has stricter alignment than the continuation/object can guarantee. Mirror the existing pattern used for locals/return stores by computing indirFlags and passing it to gtNewStoreValueNode.
 GenTree* continuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF);
unsigned offset = OFFSETOF__CORINFO_Continuation__data + layout.ContinuationMemberOffsets[memberIndex];
GenTree* offsetNode = m_compiler->gtNewIconNode((ssize_t)offset, TYP_I_IMPL);
GenTree* address = m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, continuation, offsetNode);
GenTree* store = m_compiler->gtNewStoreValueNode(awaiterLayout, address, awaiter, GTF_IND_NONFAULTING);
LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_compiler, store));

CopilotAI review requested due to automatic review settings August 4, 2026 11:23

CopilotAI 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 76 out of 76 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/coreclr/jit/async.h:7

  • async.h defines non-inline types (ContinuationMemberType, ContinuationMember, etc.) but still has no include guard / #pragma once. Now that more translation units include this header (e.g., gentree.cpp, importercalls.cpp), an accidental double-include in any TU would become a hard compile error (redefinition). Adding a conventional include guard would make this robust.
enum class ContinuationMemberType
{
CustomAwaiterOfLayout,
};

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?

  • Could the embedded awaiters hold onto GC refs longer than before?

I think it is unlikely that we grow the continuation size by a lot here since the awaiter storage is shared across await sites and is keyed on the awaiter layout. I don't think there will be that many different awaiter layouts in the same method, even with lots of different await sites.

For the GC question: the C# emitted code for the helper that is getting optimized looks like this:

TAwaiterawaiter=somethingAwaitable.GetAwaiter();if(!awaiter.IsCompleted){AsyncHelpers.UnsafeAwaitAwaiter(awaiter);}TResult=awaiter.GetResult();

So the GC refs would already be retained by this.

It also means that there is actually already the necessary state to reconstruct the awaiter stored in the continuation, so we are being a bit wasteful not reusing that. But that is quite complicated and error prone to do:

  1. With promotion and optimizations we cannot really guarantee that the state in the continuation is an exact TAwaiter, and I don't see a non-error prone way to guarantee that
  2. Without 1 I think we need something like Investigate generating resumption stubs as funclet-like code in runtime async functions #121013 where the JIT generates some funclet, but even if we could generate such funclets I also don't see how we would easily represent this.

So I went with this fix for .NET 11, mostly from worrying about runtime async boxing in cases where async1 wouldn't. I would like to eventually implement 2, but it is definitely going to take some work.

@VSadovVSadov left a comment

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.

LGTM!

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Failure in run was #131330

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in run was #131330

@jakobbotsch
jakobbotsch merged commit 174a8af into dotnet:mainAug 7, 2026
160 of 163 checks passed
@jakobbotsch
jakobbotsch deleted the fix-119842-2 branch August 7, 2026 01:56
jakobbotsch added a commit to jakobbotsch/runtime that referenced this pull request Aug 7, 2026
Upstream landed the reviewed version of the custom awaiter work this branch had
prototyped in f448161 (dotnet#131342), so the branch's copy is dropped in favor of
it everywhere the two collided:
* getAwaitAwaiterInContinuationCall now takes a CORINFO_RESOLVED_TOKEN rather
than a CORINFO_SIG_INFO, through corinfo.h, the generated thunks, the superpmi
shims and both JIT and VM sides.
* StoreAsyncAwaiter uses upstream's version, which handles under-aligned
awaiters by passing GTF_IND_UNALIGNED.
* The custom awaiter layout allocation uses ClassLayout::GetAlignmentRequirement.
* The YieldAwaiter special case this branch had added to
UnsafeAwaiterOnCompletedFromContinuation is dropped; upstream keeps that
optimization only on the boxing path in UnsafeAwaitAwaiter.
The continuation member abstraction stays as this branch has it, since async
inlining needs members that are not custom awaiters: ContinuationMember keeps
the inlined frame member types, GetStorageType and the inline depth keying, and
the layout keeps grouping a frame's members so they are allocated together.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
@dotnet-milestone-botdotnet-milestone-botBot modified the milestones: 11.0.0, 11.0-rc1Aug 7, 2026
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
This optimizes calls to
`AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter)` with struct
awaiters to instead call a new function
`AsyncHelpers.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 later `UnsafeOnCompleted` call
can then be done without any boxing by extracting it from the
continuation.
This introduces a new `GT_CONTINUATION_MEMBER_OFFSET` which 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.
There is a new `AsyncAwaiter` pseudo arg passed to
`UnsafeAwaitAwaiterInContinuation`, and expanded in the suspension path
by the async transformation to be stored in the continuation at the
right offset.
I have a couple of use cases for `GT_CONTINUATION_MEMBER_OFFSET` in
mind, so I have made the mechanism to represent the type of member
easily expandable (particularly inlining needs this as well).
This PR also removes configurable continuation reuse. This is now
unconditionally enabled, to simplify the expansion of
`GT_CONTINUATION_MEMBER_OFFSET` nodes.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIPriority:1Work that is critical for the release, but we could probably ship withoutruntime-async

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runtime async should avoid boxing custom awaiters on suspension

7 participants

@jakobbotsch@VSadov@AndyAyersMS@jtschuster@am11@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Remove boxing for awaited custom awaiters - #131342

Merged
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2
Aug 7, 2026
Merged

Remove boxing for awaited custom awaiters#131342
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jul 24, 2026

Copy link
Copy Markdown
Member

This optimizes calls to AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter) with struct awaiters to instead call a new function AsyncHelpers.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 later UnsafeOnCompleted call can then be done without any boxing by extracting it from the continuation.

This introduces a new GT_CONTINUATION_MEMBER_OFFSET which 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.
There is a new AsyncAwaiter pseudo arg passed to UnsafeAwaitAwaiterInContinuation, and expanded in the suspension path by the async transformation to be stored in the continuation at the right offset.

I have a couple of use cases for GT_CONTINUATION_MEMBER_OFFSET in mind, so I have made the mechanism to represent the type of member easily expandable (particularly inlining needs this as well).

This PR also removes configurable continuation reuse. This is now unconditionally enabled, to simplify the expansion of GT_CONTINUATION_MEMBER_OFFSET nodes.

Fix#119842

Microbenchmark

About 10% improvement, and more importantly, avoids the box that async1 also avoids to gain parity.

usingSystem;usingSystem.Diagnostics;usingSystem.Runtime.CompilerServices;usingSystem.Threading;usingSystem.Threading.Tasks;publicstaticclassProgram{staticActions_continuation;staticlongs_value;publicstaticvoidMain(){for(inti=0;i<10;i++){for(intj=0;j<100;j++){Taskt=Foo(100);while(!t.IsCompleted)s_continuation();}Thread.Sleep(100);}for(inti=0;i<50;i++){Taskt=Foo(10_000_000);while(!t.IsCompleted)s_continuation();}}privatestaticasyncTaskFoo(intn){s_value=0;Stopwatchtimer=Stopwatch.StartNew();for(inti=0;i<n;i++){awaitnewAwaiter(i);}if(n>100)Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);Trace.Assert(s_value==((long)n*(n-1))/2);}privatestructAwaiter:ICriticalNotifyCompletion{publicintX;publicAwaiter(intx)=>X=x;publicboolIsCompleted=>false;publicAwaiterGetAwaiter()=>this;publicvoidGetResult(){}publicvoidOnCompleted(Actioncontinuation){}publicvoidUnsafeOnCompleted(Actioncontinuation){s_value+=X;s_continuation=continuation;}}}
-Took 323 ms-Took 318 ms-Took 320 ms-Took 323 ms-Took 322 ms-Took 318 ms-Took 320 ms-Took 318 ms-Took 317 ms-Took 319 ms+Took 290 ms+Took 291 ms+Took 292 ms+Took 288 ms+Took 292 ms+Took 291 ms+Took 290 ms+Took 290 ms+Took 287 ms+Took 292 ms

CopilotAI review requested due to automatic review settings July 24, 2026 19:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 24, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI 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 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/UnsafeOnCompleted by reading the awaiter out of the continuation.
  • Add GT_CONTINUATION_MEMBER_OFFSET plus 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
FileDescription
src/tests/async/struct/struct.csAdds a regression test for safe and unsafe custom struct awaiters under runtime async.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.csAdds new private awaiter-in-continuation helpers and extraction-based OnCompleted/UnsafeOnCompleted paths.
src/coreclr/vm/jitinterface.hAdds VM declaration for the new JIT↔EE query to resolve the awaiter-in-continuation helper.
src/coreclr/vm/jitinterface.cppImplements helper resolution + runtime lookup computation for the new awaiter-in-continuation call.
src/coreclr/vm/corelib.hAdds CoreLibBinder method IDs for the new AsyncHelpers helper methods.
src/coreclr/tools/superpmi/superpmi/icorjitinfo.cppPlumbs the new ICorJitInfo method through SuperPMI.
src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cppUpdates generated shim to forward the new ICorJitInfo entrypoint.
src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cppUpdates generated counter shim to track/forward the new call.
src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cppUpdates collector shim to record/replay the new query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.hAdds recording/replay declarations and a new packet kind for the query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cppImplements record/dump/replay for the new query.
src/coreclr/tools/superpmi/superpmi-shared/lwmlist.hAdds LWM map entry for the query.
src/coreclr/tools/superpmi/superpmi-shared/agnostic.hAdds agnostic key struct for recording the query inputs.
src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txtAdds the new API to the JIT interface thunk generator input.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.csAdds ILC implementation to resolve the new helper method handle.
src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.csAdds generated unmanaged callback plumbing for the new API.
src/coreclr/tools/aot/jitinterface/jitinterface_generated.hUpdates NativeAOT jitinterface wrapper with the new callback.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.csEnsures R2R token availability for the new helper methods.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds runtime stack-state fields + dispatcher logic to call awaiter continuation callbacks.
src/coreclr/jit/wellknownargs.hIntroduces a new well-known arg kind for the async awaiter pseudo-arg.
src/coreclr/jit/importercalls.cppAdds importer rewrite to swap struct awaiter calls to the new helper and attach symbolic continuation-member offset.
src/coreclr/jit/ICorJitInfo_wrapper_generated.hppAdds wrapper method for the new JIT↔EE query.
src/coreclr/jit/ICorJitInfo_names_generated.hAdds the new API name for logging/tracing.
src/coreclr/jit/gtstructs.hExtends GenTreeVal struct mapping to include GT_CONTINUATION_MEMBER_OFFSET.
src/coreclr/jit/gtlist.hAdds the GT_CONTINUATION_MEMBER_OFFSET node kind and documentation.
src/coreclr/jit/gentree.cppUpdates pseudo-arg handling and debug printing for the new node.
src/coreclr/jit/compiler.hAdds compiler state for tracking continuation members and the importer helper prototype.
src/coreclr/jit/async.hAdds continuation-member abstractions and extends layout builder to allocate member offsets.
src/coreclr/jit/async.cppImplements continuation-member tracking, reserves storage in continuation layouts, and bashes symbolic offsets to constants.
src/coreclr/inc/jiteeversionguid.hBumps JIT↔EE version GUID due to interface change.
src/coreclr/inc/icorjitinfoimpl_generated.hAdds the new virtual method to the generated ICorJitInfo impl surface.
src/coreclr/inc/corinfo.hAdds the new ICorStaticInfo virtual method for helper resolution.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings July 24, 2026 19:57

CopilotAI 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 33 out of 33 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 13:41

CopilotAI 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 73 out of 73 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 15:31
Comment threadsrc/coreclr/jit/morph.cpp

@AndyAyersMSAndyAyersMS left a comment

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.

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?
  • Could the embedded awaiters hold onto GC refs longer than before?

@jtschusterjtschuster left a comment

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.

Just one question about test coverage, otherwise crossgen2 changes lgtm.

CopilotAI review requested due to automatic review settings August 4, 2026 10:46

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

Suppressed comments (1)

src/tests/async/custom-struct-awaiters/custom-struct-awaiters.cs:19

  • This xUnit test blocks on an async method via Run().Wait(). xUnit supports async test methods directly, and blocking waits can introduce deadlocks when a synchronization context is present (or hide hangs as AggregateException). Prefer returning Task from the [Fact] instead of blocking.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings August 4, 2026 10:55

CopilotAI 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 74 out of 74 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:117

  • Same as the safe path: this reads TAwaiter from raw continuation data via Unsafe.As, which can generate misaligned struct loads if the awaiter type has alignment requirements beyond pointer-size. Prefer Unsafe.ReadUnaligned<TAwaiter> here as well.
    src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:60
  • Unsafe.As<byte, TAwaiter> assumes the source address is suitably aligned for TAwaiter. Continuation member offsets are only guaranteed to be pointer-aligned, so on architectures with stricter alignment requirements this can result in misaligned struct loads. Use Unsafe.ReadUnaligned<TAwaiter> to safely read the awaiter from raw object data.
    src/coreclr/jit/async.cpp:2475
  • Stores into the continuation member don't currently propagate GTF_IND_UNALIGNED when the awaiter type's alignment exceeds the object/pointer alignment. Other continuation stores compute indirFlags based on required vs heap alignment; this path should do the same to avoid generating aligned stores to potentially unaligned offsets (especially for SIMD-containing structs).
 GenTreeFieldList* fieldList = awaiter->AsFieldList();
for (GenTreeFieldList::Use& use : fieldList->Uses())
{
if (!use.GetNode()->IsInvariant() && !use.GetNode()->OperIs(GT_LCL_VAR))
{
LIR::Use lirUse(LIR::AsRange(callBlock), &use.NodeRef(), fieldList);
lirUse.ReplaceWithLclVar(m_compiler);

src/coreclr/jit/async.cpp:2506

  • This struct store uses GTF_IND_NONFAULTING but omits GTF_IND_UNALIGNED when the awaiter layout has stricter alignment than the continuation/object can guarantee. Mirror the existing pattern used for locals/return stores by computing indirFlags and passing it to gtNewStoreValueNode.
 GenTree* continuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF);
unsigned offset = OFFSETOF__CORINFO_Continuation__data + layout.ContinuationMemberOffsets[memberIndex];
GenTree* offsetNode = m_compiler->gtNewIconNode((ssize_t)offset, TYP_I_IMPL);
GenTree* address = m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, continuation, offsetNode);
GenTree* store = m_compiler->gtNewStoreValueNode(awaiterLayout, address, awaiter, GTF_IND_NONFAULTING);
LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_compiler, store));

CopilotAI review requested due to automatic review settings August 4, 2026 11:23

CopilotAI 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 76 out of 76 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/coreclr/jit/async.h:7

  • async.h defines non-inline types (ContinuationMemberType, ContinuationMember, etc.) but still has no include guard / #pragma once. Now that more translation units include this header (e.g., gentree.cpp, importercalls.cpp), an accidental double-include in any TU would become a hard compile error (redefinition). Adding a conventional include guard would make this robust.
enum class ContinuationMemberType
{
CustomAwaiterOfLayout,
};

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?

  • Could the embedded awaiters hold onto GC refs longer than before?

I think it is unlikely that we grow the continuation size by a lot here since the awaiter storage is shared across await sites and is keyed on the awaiter layout. I don't think there will be that many different awaiter layouts in the same method, even with lots of different await sites.

For the GC question: the C# emitted code for the helper that is getting optimized looks like this:

TAwaiterawaiter=somethingAwaitable.GetAwaiter();if(!awaiter.IsCompleted){AsyncHelpers.UnsafeAwaitAwaiter(awaiter);}TResult=awaiter.GetResult();

So the GC refs would already be retained by this.

It also means that there is actually already the necessary state to reconstruct the awaiter stored in the continuation, so we are being a bit wasteful not reusing that. But that is quite complicated and error prone to do:

  1. With promotion and optimizations we cannot really guarantee that the state in the continuation is an exact TAwaiter, and I don't see a non-error prone way to guarantee that
  2. Without 1 I think we need something like Investigate generating resumption stubs as funclet-like code in runtime async functions #121013 where the JIT generates some funclet, but even if we could generate such funclets I also don't see how we would easily represent this.

So I went with this fix for .NET 11, mostly from worrying about runtime async boxing in cases where async1 wouldn't. I would like to eventually implement 2, but it is definitely going to take some work.

@VSadovVSadov left a comment

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.

LGTM!

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Failure in run was #131330

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in run was #131330

@jakobbotsch
jakobbotsch merged commit 174a8af into dotnet:mainAug 7, 2026
160 of 163 checks passed
@jakobbotsch
jakobbotsch deleted the fix-119842-2 branch August 7, 2026 01:56
jakobbotsch added a commit to jakobbotsch/runtime that referenced this pull request Aug 7, 2026
Upstream landed the reviewed version of the custom awaiter work this branch had
prototyped in f448161 (dotnet#131342), so the branch's copy is dropped in favor of
it everywhere the two collided:
* getAwaitAwaiterInContinuationCall now takes a CORINFO_RESOLVED_TOKEN rather
than a CORINFO_SIG_INFO, through corinfo.h, the generated thunks, the superpmi
shims and both JIT and VM sides.
* StoreAsyncAwaiter uses upstream's version, which handles under-aligned
awaiters by passing GTF_IND_UNALIGNED.
* The custom awaiter layout allocation uses ClassLayout::GetAlignmentRequirement.
* The YieldAwaiter special case this branch had added to
UnsafeAwaiterOnCompletedFromContinuation is dropped; upstream keeps that
optimization only on the boxing path in UnsafeAwaitAwaiter.
The continuation member abstraction stays as this branch has it, since async
inlining needs members that are not custom awaiters: ContinuationMember keeps
the inlined frame member types, GetStorageType and the inline depth keying, and
the layout keeps grouping a frame's members so they are allocated together.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
@dotnet-milestone-botdotnet-milestone-botBot modified the milestones: 11.0.0, 11.0-rc1Aug 7, 2026
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
This optimizes calls to
`AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter)` with struct
awaiters to instead call a new function
`AsyncHelpers.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 later `UnsafeOnCompleted` call
can then be done without any boxing by extracting it from the
continuation.
This introduces a new `GT_CONTINUATION_MEMBER_OFFSET` which 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.
There is a new `AsyncAwaiter` pseudo arg passed to
`UnsafeAwaitAwaiterInContinuation`, and expanded in the suspension path
by the async transformation to be stored in the continuation at the
right offset.
I have a couple of use cases for `GT_CONTINUATION_MEMBER_OFFSET` in
mind, so I have made the mechanism to represent the type of member
easily expandable (particularly inlining needs this as well).
This PR also removes configurable continuation reuse. This is now
unconditionally enabled, to simplify the expansion of
`GT_CONTINUATION_MEMBER_OFFSET` nodes.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIPriority:1Work that is critical for the release, but we could probably ship withoutruntime-async

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runtime async should avoid boxing custom awaiters on suspension

7 participants

@jakobbotsch@VSadov@AndyAyersMS@jtschuster@am11@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Remove boxing for awaited custom awaiters - #131342

Merged
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2
Aug 7, 2026
Merged

Remove boxing for awaited custom awaiters#131342
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jul 24, 2026

Copy link
Copy Markdown
Member

This optimizes calls to AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter) with struct awaiters to instead call a new function AsyncHelpers.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 later UnsafeOnCompleted call can then be done without any boxing by extracting it from the continuation.

This introduces a new GT_CONTINUATION_MEMBER_OFFSET which 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.
There is a new AsyncAwaiter pseudo arg passed to UnsafeAwaitAwaiterInContinuation, and expanded in the suspension path by the async transformation to be stored in the continuation at the right offset.

I have a couple of use cases for GT_CONTINUATION_MEMBER_OFFSET in mind, so I have made the mechanism to represent the type of member easily expandable (particularly inlining needs this as well).

This PR also removes configurable continuation reuse. This is now unconditionally enabled, to simplify the expansion of GT_CONTINUATION_MEMBER_OFFSET nodes.

Fix#119842

Microbenchmark

About 10% improvement, and more importantly, avoids the box that async1 also avoids to gain parity.

usingSystem;usingSystem.Diagnostics;usingSystem.Runtime.CompilerServices;usingSystem.Threading;usingSystem.Threading.Tasks;publicstaticclassProgram{staticActions_continuation;staticlongs_value;publicstaticvoidMain(){for(inti=0;i<10;i++){for(intj=0;j<100;j++){Taskt=Foo(100);while(!t.IsCompleted)s_continuation();}Thread.Sleep(100);}for(inti=0;i<50;i++){Taskt=Foo(10_000_000);while(!t.IsCompleted)s_continuation();}}privatestaticasyncTaskFoo(intn){s_value=0;Stopwatchtimer=Stopwatch.StartNew();for(inti=0;i<n;i++){awaitnewAwaiter(i);}if(n>100)Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);Trace.Assert(s_value==((long)n*(n-1))/2);}privatestructAwaiter:ICriticalNotifyCompletion{publicintX;publicAwaiter(intx)=>X=x;publicboolIsCompleted=>false;publicAwaiterGetAwaiter()=>this;publicvoidGetResult(){}publicvoidOnCompleted(Actioncontinuation){}publicvoidUnsafeOnCompleted(Actioncontinuation){s_value+=X;s_continuation=continuation;}}}
-Took 323 ms-Took 318 ms-Took 320 ms-Took 323 ms-Took 322 ms-Took 318 ms-Took 320 ms-Took 318 ms-Took 317 ms-Took 319 ms+Took 290 ms+Took 291 ms+Took 292 ms+Took 288 ms+Took 292 ms+Took 291 ms+Took 290 ms+Took 290 ms+Took 287 ms+Took 292 ms

CopilotAI review requested due to automatic review settings July 24, 2026 19:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 24, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI 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 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/UnsafeOnCompleted by reading the awaiter out of the continuation.
  • Add GT_CONTINUATION_MEMBER_OFFSET plus 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
FileDescription
src/tests/async/struct/struct.csAdds a regression test for safe and unsafe custom struct awaiters under runtime async.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.csAdds new private awaiter-in-continuation helpers and extraction-based OnCompleted/UnsafeOnCompleted paths.
src/coreclr/vm/jitinterface.hAdds VM declaration for the new JIT↔EE query to resolve the awaiter-in-continuation helper.
src/coreclr/vm/jitinterface.cppImplements helper resolution + runtime lookup computation for the new awaiter-in-continuation call.
src/coreclr/vm/corelib.hAdds CoreLibBinder method IDs for the new AsyncHelpers helper methods.
src/coreclr/tools/superpmi/superpmi/icorjitinfo.cppPlumbs the new ICorJitInfo method through SuperPMI.
src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cppUpdates generated shim to forward the new ICorJitInfo entrypoint.
src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cppUpdates generated counter shim to track/forward the new call.
src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cppUpdates collector shim to record/replay the new query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.hAdds recording/replay declarations and a new packet kind for the query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cppImplements record/dump/replay for the new query.
src/coreclr/tools/superpmi/superpmi-shared/lwmlist.hAdds LWM map entry for the query.
src/coreclr/tools/superpmi/superpmi-shared/agnostic.hAdds agnostic key struct for recording the query inputs.
src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txtAdds the new API to the JIT interface thunk generator input.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.csAdds ILC implementation to resolve the new helper method handle.
src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.csAdds generated unmanaged callback plumbing for the new API.
src/coreclr/tools/aot/jitinterface/jitinterface_generated.hUpdates NativeAOT jitinterface wrapper with the new callback.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.csEnsures R2R token availability for the new helper methods.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds runtime stack-state fields + dispatcher logic to call awaiter continuation callbacks.
src/coreclr/jit/wellknownargs.hIntroduces a new well-known arg kind for the async awaiter pseudo-arg.
src/coreclr/jit/importercalls.cppAdds importer rewrite to swap struct awaiter calls to the new helper and attach symbolic continuation-member offset.
src/coreclr/jit/ICorJitInfo_wrapper_generated.hppAdds wrapper method for the new JIT↔EE query.
src/coreclr/jit/ICorJitInfo_names_generated.hAdds the new API name for logging/tracing.
src/coreclr/jit/gtstructs.hExtends GenTreeVal struct mapping to include GT_CONTINUATION_MEMBER_OFFSET.
src/coreclr/jit/gtlist.hAdds the GT_CONTINUATION_MEMBER_OFFSET node kind and documentation.
src/coreclr/jit/gentree.cppUpdates pseudo-arg handling and debug printing for the new node.
src/coreclr/jit/compiler.hAdds compiler state for tracking continuation members and the importer helper prototype.
src/coreclr/jit/async.hAdds continuation-member abstractions and extends layout builder to allocate member offsets.
src/coreclr/jit/async.cppImplements continuation-member tracking, reserves storage in continuation layouts, and bashes symbolic offsets to constants.
src/coreclr/inc/jiteeversionguid.hBumps JIT↔EE version GUID due to interface change.
src/coreclr/inc/icorjitinfoimpl_generated.hAdds the new virtual method to the generated ICorJitInfo impl surface.
src/coreclr/inc/corinfo.hAdds the new ICorStaticInfo virtual method for helper resolution.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings July 24, 2026 19:57

CopilotAI 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 33 out of 33 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 13:41

CopilotAI 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 73 out of 73 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 15:31
Comment threadsrc/coreclr/jit/morph.cpp

@AndyAyersMSAndyAyersMS left a comment

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.

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?
  • Could the embedded awaiters hold onto GC refs longer than before?

@jtschusterjtschuster left a comment

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.

Just one question about test coverage, otherwise crossgen2 changes lgtm.

CopilotAI review requested due to automatic review settings August 4, 2026 10:46

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

Suppressed comments (1)

src/tests/async/custom-struct-awaiters/custom-struct-awaiters.cs:19

  • This xUnit test blocks on an async method via Run().Wait(). xUnit supports async test methods directly, and blocking waits can introduce deadlocks when a synchronization context is present (or hide hangs as AggregateException). Prefer returning Task from the [Fact] instead of blocking.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings August 4, 2026 10:55

CopilotAI 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 74 out of 74 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:117

  • Same as the safe path: this reads TAwaiter from raw continuation data via Unsafe.As, which can generate misaligned struct loads if the awaiter type has alignment requirements beyond pointer-size. Prefer Unsafe.ReadUnaligned<TAwaiter> here as well.
    src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:60
  • Unsafe.As<byte, TAwaiter> assumes the source address is suitably aligned for TAwaiter. Continuation member offsets are only guaranteed to be pointer-aligned, so on architectures with stricter alignment requirements this can result in misaligned struct loads. Use Unsafe.ReadUnaligned<TAwaiter> to safely read the awaiter from raw object data.
    src/coreclr/jit/async.cpp:2475
  • Stores into the continuation member don't currently propagate GTF_IND_UNALIGNED when the awaiter type's alignment exceeds the object/pointer alignment. Other continuation stores compute indirFlags based on required vs heap alignment; this path should do the same to avoid generating aligned stores to potentially unaligned offsets (especially for SIMD-containing structs).
 GenTreeFieldList* fieldList = awaiter->AsFieldList();
for (GenTreeFieldList::Use& use : fieldList->Uses())
{
if (!use.GetNode()->IsInvariant() && !use.GetNode()->OperIs(GT_LCL_VAR))
{
LIR::Use lirUse(LIR::AsRange(callBlock), &use.NodeRef(), fieldList);
lirUse.ReplaceWithLclVar(m_compiler);

src/coreclr/jit/async.cpp:2506

  • This struct store uses GTF_IND_NONFAULTING but omits GTF_IND_UNALIGNED when the awaiter layout has stricter alignment than the continuation/object can guarantee. Mirror the existing pattern used for locals/return stores by computing indirFlags and passing it to gtNewStoreValueNode.
 GenTree* continuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF);
unsigned offset = OFFSETOF__CORINFO_Continuation__data + layout.ContinuationMemberOffsets[memberIndex];
GenTree* offsetNode = m_compiler->gtNewIconNode((ssize_t)offset, TYP_I_IMPL);
GenTree* address = m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, continuation, offsetNode);
GenTree* store = m_compiler->gtNewStoreValueNode(awaiterLayout, address, awaiter, GTF_IND_NONFAULTING);
LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_compiler, store));

CopilotAI review requested due to automatic review settings August 4, 2026 11:23

CopilotAI 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 76 out of 76 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/coreclr/jit/async.h:7

  • async.h defines non-inline types (ContinuationMemberType, ContinuationMember, etc.) but still has no include guard / #pragma once. Now that more translation units include this header (e.g., gentree.cpp, importercalls.cpp), an accidental double-include in any TU would become a hard compile error (redefinition). Adding a conventional include guard would make this robust.
enum class ContinuationMemberType
{
CustomAwaiterOfLayout,
};

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?

  • Could the embedded awaiters hold onto GC refs longer than before?

I think it is unlikely that we grow the continuation size by a lot here since the awaiter storage is shared across await sites and is keyed on the awaiter layout. I don't think there will be that many different awaiter layouts in the same method, even with lots of different await sites.

For the GC question: the C# emitted code for the helper that is getting optimized looks like this:

TAwaiterawaiter=somethingAwaitable.GetAwaiter();if(!awaiter.IsCompleted){AsyncHelpers.UnsafeAwaitAwaiter(awaiter);}TResult=awaiter.GetResult();

So the GC refs would already be retained by this.

It also means that there is actually already the necessary state to reconstruct the awaiter stored in the continuation, so we are being a bit wasteful not reusing that. But that is quite complicated and error prone to do:

  1. With promotion and optimizations we cannot really guarantee that the state in the continuation is an exact TAwaiter, and I don't see a non-error prone way to guarantee that
  2. Without 1 I think we need something like Investigate generating resumption stubs as funclet-like code in runtime async functions #121013 where the JIT generates some funclet, but even if we could generate such funclets I also don't see how we would easily represent this.

So I went with this fix for .NET 11, mostly from worrying about runtime async boxing in cases where async1 wouldn't. I would like to eventually implement 2, but it is definitely going to take some work.

@VSadovVSadov left a comment

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.

LGTM!

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Failure in run was #131330

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in run was #131330

@jakobbotsch
jakobbotsch merged commit 174a8af into dotnet:mainAug 7, 2026
160 of 163 checks passed
@jakobbotsch
jakobbotsch deleted the fix-119842-2 branch August 7, 2026 01:56
jakobbotsch added a commit to jakobbotsch/runtime that referenced this pull request Aug 7, 2026
Upstream landed the reviewed version of the custom awaiter work this branch had
prototyped in f448161 (dotnet#131342), so the branch's copy is dropped in favor of
it everywhere the two collided:
* getAwaitAwaiterInContinuationCall now takes a CORINFO_RESOLVED_TOKEN rather
than a CORINFO_SIG_INFO, through corinfo.h, the generated thunks, the superpmi
shims and both JIT and VM sides.
* StoreAsyncAwaiter uses upstream's version, which handles under-aligned
awaiters by passing GTF_IND_UNALIGNED.
* The custom awaiter layout allocation uses ClassLayout::GetAlignmentRequirement.
* The YieldAwaiter special case this branch had added to
UnsafeAwaiterOnCompletedFromContinuation is dropped; upstream keeps that
optimization only on the boxing path in UnsafeAwaitAwaiter.
The continuation member abstraction stays as this branch has it, since async
inlining needs members that are not custom awaiters: ContinuationMember keeps
the inlined frame member types, GetStorageType and the inline depth keying, and
the layout keeps grouping a frame's members so they are allocated together.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
@dotnet-milestone-botdotnet-milestone-botBot modified the milestones: 11.0.0, 11.0-rc1Aug 7, 2026
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
This optimizes calls to
`AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter)` with struct
awaiters to instead call a new function
`AsyncHelpers.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 later `UnsafeOnCompleted` call
can then be done without any boxing by extracting it from the
continuation.
This introduces a new `GT_CONTINUATION_MEMBER_OFFSET` which 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.
There is a new `AsyncAwaiter` pseudo arg passed to
`UnsafeAwaitAwaiterInContinuation`, and expanded in the suspension path
by the async transformation to be stored in the continuation at the
right offset.
I have a couple of use cases for `GT_CONTINUATION_MEMBER_OFFSET` in
mind, so I have made the mechanism to represent the type of member
easily expandable (particularly inlining needs this as well).
This PR also removes configurable continuation reuse. This is now
unconditionally enabled, to simplify the expansion of
`GT_CONTINUATION_MEMBER_OFFSET` nodes.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIPriority:1Work that is critical for the release, but we could probably ship withoutruntime-async

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runtime async should avoid boxing custom awaiters on suspension

7 participants

@jakobbotsch@VSadov@AndyAyersMS@jtschuster@am11@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Remove boxing for awaited custom awaiters - #131342

Merged
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2
Aug 7, 2026
Merged

Remove boxing for awaited custom awaiters#131342
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jul 24, 2026

Copy link
Copy Markdown
Member

This optimizes calls to AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter) with struct awaiters to instead call a new function AsyncHelpers.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 later UnsafeOnCompleted call can then be done without any boxing by extracting it from the continuation.

This introduces a new GT_CONTINUATION_MEMBER_OFFSET which 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.
There is a new AsyncAwaiter pseudo arg passed to UnsafeAwaitAwaiterInContinuation, and expanded in the suspension path by the async transformation to be stored in the continuation at the right offset.

I have a couple of use cases for GT_CONTINUATION_MEMBER_OFFSET in mind, so I have made the mechanism to represent the type of member easily expandable (particularly inlining needs this as well).

This PR also removes configurable continuation reuse. This is now unconditionally enabled, to simplify the expansion of GT_CONTINUATION_MEMBER_OFFSET nodes.

Fix#119842

Microbenchmark

About 10% improvement, and more importantly, avoids the box that async1 also avoids to gain parity.

usingSystem;usingSystem.Diagnostics;usingSystem.Runtime.CompilerServices;usingSystem.Threading;usingSystem.Threading.Tasks;publicstaticclassProgram{staticActions_continuation;staticlongs_value;publicstaticvoidMain(){for(inti=0;i<10;i++){for(intj=0;j<100;j++){Taskt=Foo(100);while(!t.IsCompleted)s_continuation();}Thread.Sleep(100);}for(inti=0;i<50;i++){Taskt=Foo(10_000_000);while(!t.IsCompleted)s_continuation();}}privatestaticasyncTaskFoo(intn){s_value=0;Stopwatchtimer=Stopwatch.StartNew();for(inti=0;i<n;i++){awaitnewAwaiter(i);}if(n>100)Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);Trace.Assert(s_value==((long)n*(n-1))/2);}privatestructAwaiter:ICriticalNotifyCompletion{publicintX;publicAwaiter(intx)=>X=x;publicboolIsCompleted=>false;publicAwaiterGetAwaiter()=>this;publicvoidGetResult(){}publicvoidOnCompleted(Actioncontinuation){}publicvoidUnsafeOnCompleted(Actioncontinuation){s_value+=X;s_continuation=continuation;}}}
-Took 323 ms-Took 318 ms-Took 320 ms-Took 323 ms-Took 322 ms-Took 318 ms-Took 320 ms-Took 318 ms-Took 317 ms-Took 319 ms+Took 290 ms+Took 291 ms+Took 292 ms+Took 288 ms+Took 292 ms+Took 291 ms+Took 290 ms+Took 290 ms+Took 287 ms+Took 292 ms

CopilotAI review requested due to automatic review settings July 24, 2026 19:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 24, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI 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 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/UnsafeOnCompleted by reading the awaiter out of the continuation.
  • Add GT_CONTINUATION_MEMBER_OFFSET plus 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
FileDescription
src/tests/async/struct/struct.csAdds a regression test for safe and unsafe custom struct awaiters under runtime async.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.csAdds new private awaiter-in-continuation helpers and extraction-based OnCompleted/UnsafeOnCompleted paths.
src/coreclr/vm/jitinterface.hAdds VM declaration for the new JIT↔EE query to resolve the awaiter-in-continuation helper.
src/coreclr/vm/jitinterface.cppImplements helper resolution + runtime lookup computation for the new awaiter-in-continuation call.
src/coreclr/vm/corelib.hAdds CoreLibBinder method IDs for the new AsyncHelpers helper methods.
src/coreclr/tools/superpmi/superpmi/icorjitinfo.cppPlumbs the new ICorJitInfo method through SuperPMI.
src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cppUpdates generated shim to forward the new ICorJitInfo entrypoint.
src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cppUpdates generated counter shim to track/forward the new call.
src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cppUpdates collector shim to record/replay the new query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.hAdds recording/replay declarations and a new packet kind for the query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cppImplements record/dump/replay for the new query.
src/coreclr/tools/superpmi/superpmi-shared/lwmlist.hAdds LWM map entry for the query.
src/coreclr/tools/superpmi/superpmi-shared/agnostic.hAdds agnostic key struct for recording the query inputs.
src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txtAdds the new API to the JIT interface thunk generator input.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.csAdds ILC implementation to resolve the new helper method handle.
src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.csAdds generated unmanaged callback plumbing for the new API.
src/coreclr/tools/aot/jitinterface/jitinterface_generated.hUpdates NativeAOT jitinterface wrapper with the new callback.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.csEnsures R2R token availability for the new helper methods.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds runtime stack-state fields + dispatcher logic to call awaiter continuation callbacks.
src/coreclr/jit/wellknownargs.hIntroduces a new well-known arg kind for the async awaiter pseudo-arg.
src/coreclr/jit/importercalls.cppAdds importer rewrite to swap struct awaiter calls to the new helper and attach symbolic continuation-member offset.
src/coreclr/jit/ICorJitInfo_wrapper_generated.hppAdds wrapper method for the new JIT↔EE query.
src/coreclr/jit/ICorJitInfo_names_generated.hAdds the new API name for logging/tracing.
src/coreclr/jit/gtstructs.hExtends GenTreeVal struct mapping to include GT_CONTINUATION_MEMBER_OFFSET.
src/coreclr/jit/gtlist.hAdds the GT_CONTINUATION_MEMBER_OFFSET node kind and documentation.
src/coreclr/jit/gentree.cppUpdates pseudo-arg handling and debug printing for the new node.
src/coreclr/jit/compiler.hAdds compiler state for tracking continuation members and the importer helper prototype.
src/coreclr/jit/async.hAdds continuation-member abstractions and extends layout builder to allocate member offsets.
src/coreclr/jit/async.cppImplements continuation-member tracking, reserves storage in continuation layouts, and bashes symbolic offsets to constants.
src/coreclr/inc/jiteeversionguid.hBumps JIT↔EE version GUID due to interface change.
src/coreclr/inc/icorjitinfoimpl_generated.hAdds the new virtual method to the generated ICorJitInfo impl surface.
src/coreclr/inc/corinfo.hAdds the new ICorStaticInfo virtual method for helper resolution.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings July 24, 2026 19:57

CopilotAI 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 33 out of 33 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 13:41

CopilotAI 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 73 out of 73 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 15:31
Comment threadsrc/coreclr/jit/morph.cpp

@AndyAyersMSAndyAyersMS left a comment

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.

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?
  • Could the embedded awaiters hold onto GC refs longer than before?

@jtschusterjtschuster left a comment

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.

Just one question about test coverage, otherwise crossgen2 changes lgtm.

CopilotAI review requested due to automatic review settings August 4, 2026 10:46

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

Suppressed comments (1)

src/tests/async/custom-struct-awaiters/custom-struct-awaiters.cs:19

  • This xUnit test blocks on an async method via Run().Wait(). xUnit supports async test methods directly, and blocking waits can introduce deadlocks when a synchronization context is present (or hide hangs as AggregateException). Prefer returning Task from the [Fact] instead of blocking.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings August 4, 2026 10:55

CopilotAI 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 74 out of 74 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:117

  • Same as the safe path: this reads TAwaiter from raw continuation data via Unsafe.As, which can generate misaligned struct loads if the awaiter type has alignment requirements beyond pointer-size. Prefer Unsafe.ReadUnaligned<TAwaiter> here as well.
    src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:60
  • Unsafe.As<byte, TAwaiter> assumes the source address is suitably aligned for TAwaiter. Continuation member offsets are only guaranteed to be pointer-aligned, so on architectures with stricter alignment requirements this can result in misaligned struct loads. Use Unsafe.ReadUnaligned<TAwaiter> to safely read the awaiter from raw object data.
    src/coreclr/jit/async.cpp:2475
  • Stores into the continuation member don't currently propagate GTF_IND_UNALIGNED when the awaiter type's alignment exceeds the object/pointer alignment. Other continuation stores compute indirFlags based on required vs heap alignment; this path should do the same to avoid generating aligned stores to potentially unaligned offsets (especially for SIMD-containing structs).
 GenTreeFieldList* fieldList = awaiter->AsFieldList();
for (GenTreeFieldList::Use& use : fieldList->Uses())
{
if (!use.GetNode()->IsInvariant() && !use.GetNode()->OperIs(GT_LCL_VAR))
{
LIR::Use lirUse(LIR::AsRange(callBlock), &use.NodeRef(), fieldList);
lirUse.ReplaceWithLclVar(m_compiler);

src/coreclr/jit/async.cpp:2506

  • This struct store uses GTF_IND_NONFAULTING but omits GTF_IND_UNALIGNED when the awaiter layout has stricter alignment than the continuation/object can guarantee. Mirror the existing pattern used for locals/return stores by computing indirFlags and passing it to gtNewStoreValueNode.
 GenTree* continuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF);
unsigned offset = OFFSETOF__CORINFO_Continuation__data + layout.ContinuationMemberOffsets[memberIndex];
GenTree* offsetNode = m_compiler->gtNewIconNode((ssize_t)offset, TYP_I_IMPL);
GenTree* address = m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, continuation, offsetNode);
GenTree* store = m_compiler->gtNewStoreValueNode(awaiterLayout, address, awaiter, GTF_IND_NONFAULTING);
LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_compiler, store));

CopilotAI review requested due to automatic review settings August 4, 2026 11:23

CopilotAI 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 76 out of 76 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/coreclr/jit/async.h:7

  • async.h defines non-inline types (ContinuationMemberType, ContinuationMember, etc.) but still has no include guard / #pragma once. Now that more translation units include this header (e.g., gentree.cpp, importercalls.cpp), an accidental double-include in any TU would become a hard compile error (redefinition). Adding a conventional include guard would make this robust.
enum class ContinuationMemberType
{
CustomAwaiterOfLayout,
};

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?

  • Could the embedded awaiters hold onto GC refs longer than before?

I think it is unlikely that we grow the continuation size by a lot here since the awaiter storage is shared across await sites and is keyed on the awaiter layout. I don't think there will be that many different awaiter layouts in the same method, even with lots of different await sites.

For the GC question: the C# emitted code for the helper that is getting optimized looks like this:

TAwaiterawaiter=somethingAwaitable.GetAwaiter();if(!awaiter.IsCompleted){AsyncHelpers.UnsafeAwaitAwaiter(awaiter);}TResult=awaiter.GetResult();

So the GC refs would already be retained by this.

It also means that there is actually already the necessary state to reconstruct the awaiter stored in the continuation, so we are being a bit wasteful not reusing that. But that is quite complicated and error prone to do:

  1. With promotion and optimizations we cannot really guarantee that the state in the continuation is an exact TAwaiter, and I don't see a non-error prone way to guarantee that
  2. Without 1 I think we need something like Investigate generating resumption stubs as funclet-like code in runtime async functions #121013 where the JIT generates some funclet, but even if we could generate such funclets I also don't see how we would easily represent this.

So I went with this fix for .NET 11, mostly from worrying about runtime async boxing in cases where async1 wouldn't. I would like to eventually implement 2, but it is definitely going to take some work.

@VSadovVSadov left a comment

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.

LGTM!

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Failure in run was #131330

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in run was #131330

@jakobbotsch
jakobbotsch merged commit 174a8af into dotnet:mainAug 7, 2026
160 of 163 checks passed
@jakobbotsch
jakobbotsch deleted the fix-119842-2 branch August 7, 2026 01:56
jakobbotsch added a commit to jakobbotsch/runtime that referenced this pull request Aug 7, 2026
Upstream landed the reviewed version of the custom awaiter work this branch had
prototyped in f448161 (dotnet#131342), so the branch's copy is dropped in favor of
it everywhere the two collided:
* getAwaitAwaiterInContinuationCall now takes a CORINFO_RESOLVED_TOKEN rather
than a CORINFO_SIG_INFO, through corinfo.h, the generated thunks, the superpmi
shims and both JIT and VM sides.
* StoreAsyncAwaiter uses upstream's version, which handles under-aligned
awaiters by passing GTF_IND_UNALIGNED.
* The custom awaiter layout allocation uses ClassLayout::GetAlignmentRequirement.
* The YieldAwaiter special case this branch had added to
UnsafeAwaiterOnCompletedFromContinuation is dropped; upstream keeps that
optimization only on the boxing path in UnsafeAwaitAwaiter.
The continuation member abstraction stays as this branch has it, since async
inlining needs members that are not custom awaiters: ContinuationMember keeps
the inlined frame member types, GetStorageType and the inline depth keying, and
the layout keeps grouping a frame's members so they are allocated together.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
@dotnet-milestone-botdotnet-milestone-botBot modified the milestones: 11.0.0, 11.0-rc1Aug 7, 2026
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
This optimizes calls to
`AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter)` with struct
awaiters to instead call a new function
`AsyncHelpers.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 later `UnsafeOnCompleted` call
can then be done without any boxing by extracting it from the
continuation.
This introduces a new `GT_CONTINUATION_MEMBER_OFFSET` which 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.
There is a new `AsyncAwaiter` pseudo arg passed to
`UnsafeAwaitAwaiterInContinuation`, and expanded in the suspension path
by the async transformation to be stored in the continuation at the
right offset.
I have a couple of use cases for `GT_CONTINUATION_MEMBER_OFFSET` in
mind, so I have made the mechanism to represent the type of member
easily expandable (particularly inlining needs this as well).
This PR also removes configurable continuation reuse. This is now
unconditionally enabled, to simplify the expansion of
`GT_CONTINUATION_MEMBER_OFFSET` nodes.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIPriority:1Work that is critical for the release, but we could probably ship withoutruntime-async

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runtime async should avoid boxing custom awaiters on suspension

7 participants

@jakobbotsch@VSadov@AndyAyersMS@jtschuster@am11@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Remove boxing for awaited custom awaiters - #131342

Merged
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2
Aug 7, 2026
Merged

Remove boxing for awaited custom awaiters#131342
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jul 24, 2026

Copy link
Copy Markdown
Member

This optimizes calls to AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter) with struct awaiters to instead call a new function AsyncHelpers.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 later UnsafeOnCompleted call can then be done without any boxing by extracting it from the continuation.

This introduces a new GT_CONTINUATION_MEMBER_OFFSET which 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.
There is a new AsyncAwaiter pseudo arg passed to UnsafeAwaitAwaiterInContinuation, and expanded in the suspension path by the async transformation to be stored in the continuation at the right offset.

I have a couple of use cases for GT_CONTINUATION_MEMBER_OFFSET in mind, so I have made the mechanism to represent the type of member easily expandable (particularly inlining needs this as well).

This PR also removes configurable continuation reuse. This is now unconditionally enabled, to simplify the expansion of GT_CONTINUATION_MEMBER_OFFSET nodes.

Fix#119842

Microbenchmark

About 10% improvement, and more importantly, avoids the box that async1 also avoids to gain parity.

usingSystem;usingSystem.Diagnostics;usingSystem.Runtime.CompilerServices;usingSystem.Threading;usingSystem.Threading.Tasks;publicstaticclassProgram{staticActions_continuation;staticlongs_value;publicstaticvoidMain(){for(inti=0;i<10;i++){for(intj=0;j<100;j++){Taskt=Foo(100);while(!t.IsCompleted)s_continuation();}Thread.Sleep(100);}for(inti=0;i<50;i++){Taskt=Foo(10_000_000);while(!t.IsCompleted)s_continuation();}}privatestaticasyncTaskFoo(intn){s_value=0;Stopwatchtimer=Stopwatch.StartNew();for(inti=0;i<n;i++){awaitnewAwaiter(i);}if(n>100)Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);Trace.Assert(s_value==((long)n*(n-1))/2);}privatestructAwaiter:ICriticalNotifyCompletion{publicintX;publicAwaiter(intx)=>X=x;publicboolIsCompleted=>false;publicAwaiterGetAwaiter()=>this;publicvoidGetResult(){}publicvoidOnCompleted(Actioncontinuation){}publicvoidUnsafeOnCompleted(Actioncontinuation){s_value+=X;s_continuation=continuation;}}}
-Took 323 ms-Took 318 ms-Took 320 ms-Took 323 ms-Took 322 ms-Took 318 ms-Took 320 ms-Took 318 ms-Took 317 ms-Took 319 ms+Took 290 ms+Took 291 ms+Took 292 ms+Took 288 ms+Took 292 ms+Took 291 ms+Took 290 ms+Took 290 ms+Took 287 ms+Took 292 ms

CopilotAI review requested due to automatic review settings July 24, 2026 19:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 24, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI 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 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/UnsafeOnCompleted by reading the awaiter out of the continuation.
  • Add GT_CONTINUATION_MEMBER_OFFSET plus 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
FileDescription
src/tests/async/struct/struct.csAdds a regression test for safe and unsafe custom struct awaiters under runtime async.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.csAdds new private awaiter-in-continuation helpers and extraction-based OnCompleted/UnsafeOnCompleted paths.
src/coreclr/vm/jitinterface.hAdds VM declaration for the new JIT↔EE query to resolve the awaiter-in-continuation helper.
src/coreclr/vm/jitinterface.cppImplements helper resolution + runtime lookup computation for the new awaiter-in-continuation call.
src/coreclr/vm/corelib.hAdds CoreLibBinder method IDs for the new AsyncHelpers helper methods.
src/coreclr/tools/superpmi/superpmi/icorjitinfo.cppPlumbs the new ICorJitInfo method through SuperPMI.
src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cppUpdates generated shim to forward the new ICorJitInfo entrypoint.
src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cppUpdates generated counter shim to track/forward the new call.
src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cppUpdates collector shim to record/replay the new query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.hAdds recording/replay declarations and a new packet kind for the query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cppImplements record/dump/replay for the new query.
src/coreclr/tools/superpmi/superpmi-shared/lwmlist.hAdds LWM map entry for the query.
src/coreclr/tools/superpmi/superpmi-shared/agnostic.hAdds agnostic key struct for recording the query inputs.
src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txtAdds the new API to the JIT interface thunk generator input.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.csAdds ILC implementation to resolve the new helper method handle.
src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.csAdds generated unmanaged callback plumbing for the new API.
src/coreclr/tools/aot/jitinterface/jitinterface_generated.hUpdates NativeAOT jitinterface wrapper with the new callback.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.csEnsures R2R token availability for the new helper methods.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds runtime stack-state fields + dispatcher logic to call awaiter continuation callbacks.
src/coreclr/jit/wellknownargs.hIntroduces a new well-known arg kind for the async awaiter pseudo-arg.
src/coreclr/jit/importercalls.cppAdds importer rewrite to swap struct awaiter calls to the new helper and attach symbolic continuation-member offset.
src/coreclr/jit/ICorJitInfo_wrapper_generated.hppAdds wrapper method for the new JIT↔EE query.
src/coreclr/jit/ICorJitInfo_names_generated.hAdds the new API name for logging/tracing.
src/coreclr/jit/gtstructs.hExtends GenTreeVal struct mapping to include GT_CONTINUATION_MEMBER_OFFSET.
src/coreclr/jit/gtlist.hAdds the GT_CONTINUATION_MEMBER_OFFSET node kind and documentation.
src/coreclr/jit/gentree.cppUpdates pseudo-arg handling and debug printing for the new node.
src/coreclr/jit/compiler.hAdds compiler state for tracking continuation members and the importer helper prototype.
src/coreclr/jit/async.hAdds continuation-member abstractions and extends layout builder to allocate member offsets.
src/coreclr/jit/async.cppImplements continuation-member tracking, reserves storage in continuation layouts, and bashes symbolic offsets to constants.
src/coreclr/inc/jiteeversionguid.hBumps JIT↔EE version GUID due to interface change.
src/coreclr/inc/icorjitinfoimpl_generated.hAdds the new virtual method to the generated ICorJitInfo impl surface.
src/coreclr/inc/corinfo.hAdds the new ICorStaticInfo virtual method for helper resolution.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings July 24, 2026 19:57

CopilotAI 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 33 out of 33 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 13:41

CopilotAI 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 73 out of 73 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 15:31
Comment threadsrc/coreclr/jit/morph.cpp

@AndyAyersMSAndyAyersMS left a comment

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.

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?
  • Could the embedded awaiters hold onto GC refs longer than before?

@jtschusterjtschuster left a comment

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.

Just one question about test coverage, otherwise crossgen2 changes lgtm.

CopilotAI review requested due to automatic review settings August 4, 2026 10:46

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

Suppressed comments (1)

src/tests/async/custom-struct-awaiters/custom-struct-awaiters.cs:19

  • This xUnit test blocks on an async method via Run().Wait(). xUnit supports async test methods directly, and blocking waits can introduce deadlocks when a synchronization context is present (or hide hangs as AggregateException). Prefer returning Task from the [Fact] instead of blocking.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings August 4, 2026 10:55

CopilotAI 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 74 out of 74 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:117

  • Same as the safe path: this reads TAwaiter from raw continuation data via Unsafe.As, which can generate misaligned struct loads if the awaiter type has alignment requirements beyond pointer-size. Prefer Unsafe.ReadUnaligned<TAwaiter> here as well.
    src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:60
  • Unsafe.As<byte, TAwaiter> assumes the source address is suitably aligned for TAwaiter. Continuation member offsets are only guaranteed to be pointer-aligned, so on architectures with stricter alignment requirements this can result in misaligned struct loads. Use Unsafe.ReadUnaligned<TAwaiter> to safely read the awaiter from raw object data.
    src/coreclr/jit/async.cpp:2475
  • Stores into the continuation member don't currently propagate GTF_IND_UNALIGNED when the awaiter type's alignment exceeds the object/pointer alignment. Other continuation stores compute indirFlags based on required vs heap alignment; this path should do the same to avoid generating aligned stores to potentially unaligned offsets (especially for SIMD-containing structs).
 GenTreeFieldList* fieldList = awaiter->AsFieldList();
for (GenTreeFieldList::Use& use : fieldList->Uses())
{
if (!use.GetNode()->IsInvariant() && !use.GetNode()->OperIs(GT_LCL_VAR))
{
LIR::Use lirUse(LIR::AsRange(callBlock), &use.NodeRef(), fieldList);
lirUse.ReplaceWithLclVar(m_compiler);

src/coreclr/jit/async.cpp:2506

  • This struct store uses GTF_IND_NONFAULTING but omits GTF_IND_UNALIGNED when the awaiter layout has stricter alignment than the continuation/object can guarantee. Mirror the existing pattern used for locals/return stores by computing indirFlags and passing it to gtNewStoreValueNode.
 GenTree* continuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF);
unsigned offset = OFFSETOF__CORINFO_Continuation__data + layout.ContinuationMemberOffsets[memberIndex];
GenTree* offsetNode = m_compiler->gtNewIconNode((ssize_t)offset, TYP_I_IMPL);
GenTree* address = m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, continuation, offsetNode);
GenTree* store = m_compiler->gtNewStoreValueNode(awaiterLayout, address, awaiter, GTF_IND_NONFAULTING);
LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_compiler, store));

CopilotAI review requested due to automatic review settings August 4, 2026 11:23

CopilotAI 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 76 out of 76 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/coreclr/jit/async.h:7

  • async.h defines non-inline types (ContinuationMemberType, ContinuationMember, etc.) but still has no include guard / #pragma once. Now that more translation units include this header (e.g., gentree.cpp, importercalls.cpp), an accidental double-include in any TU would become a hard compile error (redefinition). Adding a conventional include guard would make this robust.
enum class ContinuationMemberType
{
CustomAwaiterOfLayout,
};

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?

  • Could the embedded awaiters hold onto GC refs longer than before?

I think it is unlikely that we grow the continuation size by a lot here since the awaiter storage is shared across await sites and is keyed on the awaiter layout. I don't think there will be that many different awaiter layouts in the same method, even with lots of different await sites.

For the GC question: the C# emitted code for the helper that is getting optimized looks like this:

TAwaiterawaiter=somethingAwaitable.GetAwaiter();if(!awaiter.IsCompleted){AsyncHelpers.UnsafeAwaitAwaiter(awaiter);}TResult=awaiter.GetResult();

So the GC refs would already be retained by this.

It also means that there is actually already the necessary state to reconstruct the awaiter stored in the continuation, so we are being a bit wasteful not reusing that. But that is quite complicated and error prone to do:

  1. With promotion and optimizations we cannot really guarantee that the state in the continuation is an exact TAwaiter, and I don't see a non-error prone way to guarantee that
  2. Without 1 I think we need something like Investigate generating resumption stubs as funclet-like code in runtime async functions #121013 where the JIT generates some funclet, but even if we could generate such funclets I also don't see how we would easily represent this.

So I went with this fix for .NET 11, mostly from worrying about runtime async boxing in cases where async1 wouldn't. I would like to eventually implement 2, but it is definitely going to take some work.

@VSadovVSadov left a comment

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.

LGTM!

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Failure in run was #131330

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in run was #131330

@jakobbotsch
jakobbotsch merged commit 174a8af into dotnet:mainAug 7, 2026
160 of 163 checks passed
@jakobbotsch
jakobbotsch deleted the fix-119842-2 branch August 7, 2026 01:56
jakobbotsch added a commit to jakobbotsch/runtime that referenced this pull request Aug 7, 2026
Upstream landed the reviewed version of the custom awaiter work this branch had
prototyped in f448161 (dotnet#131342), so the branch's copy is dropped in favor of
it everywhere the two collided:
* getAwaitAwaiterInContinuationCall now takes a CORINFO_RESOLVED_TOKEN rather
than a CORINFO_SIG_INFO, through corinfo.h, the generated thunks, the superpmi
shims and both JIT and VM sides.
* StoreAsyncAwaiter uses upstream's version, which handles under-aligned
awaiters by passing GTF_IND_UNALIGNED.
* The custom awaiter layout allocation uses ClassLayout::GetAlignmentRequirement.
* The YieldAwaiter special case this branch had added to
UnsafeAwaiterOnCompletedFromContinuation is dropped; upstream keeps that
optimization only on the boxing path in UnsafeAwaitAwaiter.
The continuation member abstraction stays as this branch has it, since async
inlining needs members that are not custom awaiters: ContinuationMember keeps
the inlined frame member types, GetStorageType and the inline depth keying, and
the layout keeps grouping a frame's members so they are allocated together.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
@dotnet-milestone-botdotnet-milestone-botBot modified the milestones: 11.0.0, 11.0-rc1Aug 7, 2026
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
This optimizes calls to
`AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter)` with struct
awaiters to instead call a new function
`AsyncHelpers.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 later `UnsafeOnCompleted` call
can then be done without any boxing by extracting it from the
continuation.
This introduces a new `GT_CONTINUATION_MEMBER_OFFSET` which 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.
There is a new `AsyncAwaiter` pseudo arg passed to
`UnsafeAwaitAwaiterInContinuation`, and expanded in the suspension path
by the async transformation to be stored in the continuation at the
right offset.
I have a couple of use cases for `GT_CONTINUATION_MEMBER_OFFSET` in
mind, so I have made the mechanism to represent the type of member
easily expandable (particularly inlining needs this as well).
This PR also removes configurable continuation reuse. This is now
unconditionally enabled, to simplify the expansion of
`GT_CONTINUATION_MEMBER_OFFSET` nodes.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIPriority:1Work that is critical for the release, but we could probably ship withoutruntime-async

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runtime async should avoid boxing custom awaiters on suspension

7 participants

@jakobbotsch@VSadov@AndyAyersMS@jtschuster@am11@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Remove boxing for awaited custom awaiters - #131342

Merged
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2
Aug 7, 2026
Merged

Remove boxing for awaited custom awaiters#131342
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jul 24, 2026

Copy link
Copy Markdown
Member

This optimizes calls to AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter) with struct awaiters to instead call a new function AsyncHelpers.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 later UnsafeOnCompleted call can then be done without any boxing by extracting it from the continuation.

This introduces a new GT_CONTINUATION_MEMBER_OFFSET which 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.
There is a new AsyncAwaiter pseudo arg passed to UnsafeAwaitAwaiterInContinuation, and expanded in the suspension path by the async transformation to be stored in the continuation at the right offset.

I have a couple of use cases for GT_CONTINUATION_MEMBER_OFFSET in mind, so I have made the mechanism to represent the type of member easily expandable (particularly inlining needs this as well).

This PR also removes configurable continuation reuse. This is now unconditionally enabled, to simplify the expansion of GT_CONTINUATION_MEMBER_OFFSET nodes.

Fix#119842

Microbenchmark

About 10% improvement, and more importantly, avoids the box that async1 also avoids to gain parity.

usingSystem;usingSystem.Diagnostics;usingSystem.Runtime.CompilerServices;usingSystem.Threading;usingSystem.Threading.Tasks;publicstaticclassProgram{staticActions_continuation;staticlongs_value;publicstaticvoidMain(){for(inti=0;i<10;i++){for(intj=0;j<100;j++){Taskt=Foo(100);while(!t.IsCompleted)s_continuation();}Thread.Sleep(100);}for(inti=0;i<50;i++){Taskt=Foo(10_000_000);while(!t.IsCompleted)s_continuation();}}privatestaticasyncTaskFoo(intn){s_value=0;Stopwatchtimer=Stopwatch.StartNew();for(inti=0;i<n;i++){awaitnewAwaiter(i);}if(n>100)Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);Trace.Assert(s_value==((long)n*(n-1))/2);}privatestructAwaiter:ICriticalNotifyCompletion{publicintX;publicAwaiter(intx)=>X=x;publicboolIsCompleted=>false;publicAwaiterGetAwaiter()=>this;publicvoidGetResult(){}publicvoidOnCompleted(Actioncontinuation){}publicvoidUnsafeOnCompleted(Actioncontinuation){s_value+=X;s_continuation=continuation;}}}
-Took 323 ms-Took 318 ms-Took 320 ms-Took 323 ms-Took 322 ms-Took 318 ms-Took 320 ms-Took 318 ms-Took 317 ms-Took 319 ms+Took 290 ms+Took 291 ms+Took 292 ms+Took 288 ms+Took 292 ms+Took 291 ms+Took 290 ms+Took 290 ms+Took 287 ms+Took 292 ms

CopilotAI review requested due to automatic review settings July 24, 2026 19:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 24, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI 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 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/UnsafeOnCompleted by reading the awaiter out of the continuation.
  • Add GT_CONTINUATION_MEMBER_OFFSET plus 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
FileDescription
src/tests/async/struct/struct.csAdds a regression test for safe and unsafe custom struct awaiters under runtime async.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.csAdds new private awaiter-in-continuation helpers and extraction-based OnCompleted/UnsafeOnCompleted paths.
src/coreclr/vm/jitinterface.hAdds VM declaration for the new JIT↔EE query to resolve the awaiter-in-continuation helper.
src/coreclr/vm/jitinterface.cppImplements helper resolution + runtime lookup computation for the new awaiter-in-continuation call.
src/coreclr/vm/corelib.hAdds CoreLibBinder method IDs for the new AsyncHelpers helper methods.
src/coreclr/tools/superpmi/superpmi/icorjitinfo.cppPlumbs the new ICorJitInfo method through SuperPMI.
src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cppUpdates generated shim to forward the new ICorJitInfo entrypoint.
src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cppUpdates generated counter shim to track/forward the new call.
src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cppUpdates collector shim to record/replay the new query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.hAdds recording/replay declarations and a new packet kind for the query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cppImplements record/dump/replay for the new query.
src/coreclr/tools/superpmi/superpmi-shared/lwmlist.hAdds LWM map entry for the query.
src/coreclr/tools/superpmi/superpmi-shared/agnostic.hAdds agnostic key struct for recording the query inputs.
src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txtAdds the new API to the JIT interface thunk generator input.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.csAdds ILC implementation to resolve the new helper method handle.
src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.csAdds generated unmanaged callback plumbing for the new API.
src/coreclr/tools/aot/jitinterface/jitinterface_generated.hUpdates NativeAOT jitinterface wrapper with the new callback.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.csEnsures R2R token availability for the new helper methods.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds runtime stack-state fields + dispatcher logic to call awaiter continuation callbacks.
src/coreclr/jit/wellknownargs.hIntroduces a new well-known arg kind for the async awaiter pseudo-arg.
src/coreclr/jit/importercalls.cppAdds importer rewrite to swap struct awaiter calls to the new helper and attach symbolic continuation-member offset.
src/coreclr/jit/ICorJitInfo_wrapper_generated.hppAdds wrapper method for the new JIT↔EE query.
src/coreclr/jit/ICorJitInfo_names_generated.hAdds the new API name for logging/tracing.
src/coreclr/jit/gtstructs.hExtends GenTreeVal struct mapping to include GT_CONTINUATION_MEMBER_OFFSET.
src/coreclr/jit/gtlist.hAdds the GT_CONTINUATION_MEMBER_OFFSET node kind and documentation.
src/coreclr/jit/gentree.cppUpdates pseudo-arg handling and debug printing for the new node.
src/coreclr/jit/compiler.hAdds compiler state for tracking continuation members and the importer helper prototype.
src/coreclr/jit/async.hAdds continuation-member abstractions and extends layout builder to allocate member offsets.
src/coreclr/jit/async.cppImplements continuation-member tracking, reserves storage in continuation layouts, and bashes symbolic offsets to constants.
src/coreclr/inc/jiteeversionguid.hBumps JIT↔EE version GUID due to interface change.
src/coreclr/inc/icorjitinfoimpl_generated.hAdds the new virtual method to the generated ICorJitInfo impl surface.
src/coreclr/inc/corinfo.hAdds the new ICorStaticInfo virtual method for helper resolution.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings July 24, 2026 19:57

CopilotAI 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 33 out of 33 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 13:41

CopilotAI 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 73 out of 73 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 15:31
Comment threadsrc/coreclr/jit/morph.cpp

@AndyAyersMSAndyAyersMS left a comment

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.

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?
  • Could the embedded awaiters hold onto GC refs longer than before?

@jtschusterjtschuster left a comment

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.

Just one question about test coverage, otherwise crossgen2 changes lgtm.

CopilotAI review requested due to automatic review settings August 4, 2026 10:46

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

Suppressed comments (1)

src/tests/async/custom-struct-awaiters/custom-struct-awaiters.cs:19

  • This xUnit test blocks on an async method via Run().Wait(). xUnit supports async test methods directly, and blocking waits can introduce deadlocks when a synchronization context is present (or hide hangs as AggregateException). Prefer returning Task from the [Fact] instead of blocking.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings August 4, 2026 10:55

CopilotAI 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 74 out of 74 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:117

  • Same as the safe path: this reads TAwaiter from raw continuation data via Unsafe.As, which can generate misaligned struct loads if the awaiter type has alignment requirements beyond pointer-size. Prefer Unsafe.ReadUnaligned<TAwaiter> here as well.
    src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:60
  • Unsafe.As<byte, TAwaiter> assumes the source address is suitably aligned for TAwaiter. Continuation member offsets are only guaranteed to be pointer-aligned, so on architectures with stricter alignment requirements this can result in misaligned struct loads. Use Unsafe.ReadUnaligned<TAwaiter> to safely read the awaiter from raw object data.
    src/coreclr/jit/async.cpp:2475
  • Stores into the continuation member don't currently propagate GTF_IND_UNALIGNED when the awaiter type's alignment exceeds the object/pointer alignment. Other continuation stores compute indirFlags based on required vs heap alignment; this path should do the same to avoid generating aligned stores to potentially unaligned offsets (especially for SIMD-containing structs).
 GenTreeFieldList* fieldList = awaiter->AsFieldList();
for (GenTreeFieldList::Use& use : fieldList->Uses())
{
if (!use.GetNode()->IsInvariant() && !use.GetNode()->OperIs(GT_LCL_VAR))
{
LIR::Use lirUse(LIR::AsRange(callBlock), &use.NodeRef(), fieldList);
lirUse.ReplaceWithLclVar(m_compiler);

src/coreclr/jit/async.cpp:2506

  • This struct store uses GTF_IND_NONFAULTING but omits GTF_IND_UNALIGNED when the awaiter layout has stricter alignment than the continuation/object can guarantee. Mirror the existing pattern used for locals/return stores by computing indirFlags and passing it to gtNewStoreValueNode.
 GenTree* continuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF);
unsigned offset = OFFSETOF__CORINFO_Continuation__data + layout.ContinuationMemberOffsets[memberIndex];
GenTree* offsetNode = m_compiler->gtNewIconNode((ssize_t)offset, TYP_I_IMPL);
GenTree* address = m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, continuation, offsetNode);
GenTree* store = m_compiler->gtNewStoreValueNode(awaiterLayout, address, awaiter, GTF_IND_NONFAULTING);
LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_compiler, store));

CopilotAI review requested due to automatic review settings August 4, 2026 11:23

CopilotAI 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 76 out of 76 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/coreclr/jit/async.h:7

  • async.h defines non-inline types (ContinuationMemberType, ContinuationMember, etc.) but still has no include guard / #pragma once. Now that more translation units include this header (e.g., gentree.cpp, importercalls.cpp), an accidental double-include in any TU would become a hard compile error (redefinition). Adding a conventional include guard would make this robust.
enum class ContinuationMemberType
{
CustomAwaiterOfLayout,
};

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?

  • Could the embedded awaiters hold onto GC refs longer than before?

I think it is unlikely that we grow the continuation size by a lot here since the awaiter storage is shared across await sites and is keyed on the awaiter layout. I don't think there will be that many different awaiter layouts in the same method, even with lots of different await sites.

For the GC question: the C# emitted code for the helper that is getting optimized looks like this:

TAwaiterawaiter=somethingAwaitable.GetAwaiter();if(!awaiter.IsCompleted){AsyncHelpers.UnsafeAwaitAwaiter(awaiter);}TResult=awaiter.GetResult();

So the GC refs would already be retained by this.

It also means that there is actually already the necessary state to reconstruct the awaiter stored in the continuation, so we are being a bit wasteful not reusing that. But that is quite complicated and error prone to do:

  1. With promotion and optimizations we cannot really guarantee that the state in the continuation is an exact TAwaiter, and I don't see a non-error prone way to guarantee that
  2. Without 1 I think we need something like Investigate generating resumption stubs as funclet-like code in runtime async functions #121013 where the JIT generates some funclet, but even if we could generate such funclets I also don't see how we would easily represent this.

So I went with this fix for .NET 11, mostly from worrying about runtime async boxing in cases where async1 wouldn't. I would like to eventually implement 2, but it is definitely going to take some work.

@VSadovVSadov left a comment

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.

LGTM!

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Failure in run was #131330

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in run was #131330

@jakobbotsch
jakobbotsch merged commit 174a8af into dotnet:mainAug 7, 2026
160 of 163 checks passed
@jakobbotsch
jakobbotsch deleted the fix-119842-2 branch August 7, 2026 01:56
jakobbotsch added a commit to jakobbotsch/runtime that referenced this pull request Aug 7, 2026
Upstream landed the reviewed version of the custom awaiter work this branch had
prototyped in f448161 (dotnet#131342), so the branch's copy is dropped in favor of
it everywhere the two collided:
* getAwaitAwaiterInContinuationCall now takes a CORINFO_RESOLVED_TOKEN rather
than a CORINFO_SIG_INFO, through corinfo.h, the generated thunks, the superpmi
shims and both JIT and VM sides.
* StoreAsyncAwaiter uses upstream's version, which handles under-aligned
awaiters by passing GTF_IND_UNALIGNED.
* The custom awaiter layout allocation uses ClassLayout::GetAlignmentRequirement.
* The YieldAwaiter special case this branch had added to
UnsafeAwaiterOnCompletedFromContinuation is dropped; upstream keeps that
optimization only on the boxing path in UnsafeAwaitAwaiter.
The continuation member abstraction stays as this branch has it, since async
inlining needs members that are not custom awaiters: ContinuationMember keeps
the inlined frame member types, GetStorageType and the inline depth keying, and
the layout keeps grouping a frame's members so they are allocated together.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
@dotnet-milestone-botdotnet-milestone-botBot modified the milestones: 11.0.0, 11.0-rc1Aug 7, 2026
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
This optimizes calls to
`AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter)` with struct
awaiters to instead call a new function
`AsyncHelpers.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 later `UnsafeOnCompleted` call
can then be done without any boxing by extracting it from the
continuation.
This introduces a new `GT_CONTINUATION_MEMBER_OFFSET` which 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.
There is a new `AsyncAwaiter` pseudo arg passed to
`UnsafeAwaitAwaiterInContinuation`, and expanded in the suspension path
by the async transformation to be stored in the continuation at the
right offset.
I have a couple of use cases for `GT_CONTINUATION_MEMBER_OFFSET` in
mind, so I have made the mechanism to represent the type of member
easily expandable (particularly inlining needs this as well).
This PR also removes configurable continuation reuse. This is now
unconditionally enabled, to simplify the expansion of
`GT_CONTINUATION_MEMBER_OFFSET` nodes.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIPriority:1Work that is critical for the release, but we could probably ship withoutruntime-async

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runtime async should avoid boxing custom awaiters on suspension

7 participants

@jakobbotsch@VSadov@AndyAyersMS@jtschuster@am11@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Remove boxing for awaited custom awaiters - #131342

Merged
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2
Aug 7, 2026
Merged

Remove boxing for awaited custom awaiters#131342
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jul 24, 2026

Copy link
Copy Markdown
Member

This optimizes calls to AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter) with struct awaiters to instead call a new function AsyncHelpers.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 later UnsafeOnCompleted call can then be done without any boxing by extracting it from the continuation.

This introduces a new GT_CONTINUATION_MEMBER_OFFSET which 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.
There is a new AsyncAwaiter pseudo arg passed to UnsafeAwaitAwaiterInContinuation, and expanded in the suspension path by the async transformation to be stored in the continuation at the right offset.

I have a couple of use cases for GT_CONTINUATION_MEMBER_OFFSET in mind, so I have made the mechanism to represent the type of member easily expandable (particularly inlining needs this as well).

This PR also removes configurable continuation reuse. This is now unconditionally enabled, to simplify the expansion of GT_CONTINUATION_MEMBER_OFFSET nodes.

Fix#119842

Microbenchmark

About 10% improvement, and more importantly, avoids the box that async1 also avoids to gain parity.

usingSystem;usingSystem.Diagnostics;usingSystem.Runtime.CompilerServices;usingSystem.Threading;usingSystem.Threading.Tasks;publicstaticclassProgram{staticActions_continuation;staticlongs_value;publicstaticvoidMain(){for(inti=0;i<10;i++){for(intj=0;j<100;j++){Taskt=Foo(100);while(!t.IsCompleted)s_continuation();}Thread.Sleep(100);}for(inti=0;i<50;i++){Taskt=Foo(10_000_000);while(!t.IsCompleted)s_continuation();}}privatestaticasyncTaskFoo(intn){s_value=0;Stopwatchtimer=Stopwatch.StartNew();for(inti=0;i<n;i++){awaitnewAwaiter(i);}if(n>100)Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);Trace.Assert(s_value==((long)n*(n-1))/2);}privatestructAwaiter:ICriticalNotifyCompletion{publicintX;publicAwaiter(intx)=>X=x;publicboolIsCompleted=>false;publicAwaiterGetAwaiter()=>this;publicvoidGetResult(){}publicvoidOnCompleted(Actioncontinuation){}publicvoidUnsafeOnCompleted(Actioncontinuation){s_value+=X;s_continuation=continuation;}}}
-Took 323 ms-Took 318 ms-Took 320 ms-Took 323 ms-Took 322 ms-Took 318 ms-Took 320 ms-Took 318 ms-Took 317 ms-Took 319 ms+Took 290 ms+Took 291 ms+Took 292 ms+Took 288 ms+Took 292 ms+Took 291 ms+Took 290 ms+Took 290 ms+Took 287 ms+Took 292 ms

CopilotAI review requested due to automatic review settings July 24, 2026 19:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 24, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI 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 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/UnsafeOnCompleted by reading the awaiter out of the continuation.
  • Add GT_CONTINUATION_MEMBER_OFFSET plus 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
FileDescription
src/tests/async/struct/struct.csAdds a regression test for safe and unsafe custom struct awaiters under runtime async.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.csAdds new private awaiter-in-continuation helpers and extraction-based OnCompleted/UnsafeOnCompleted paths.
src/coreclr/vm/jitinterface.hAdds VM declaration for the new JIT↔EE query to resolve the awaiter-in-continuation helper.
src/coreclr/vm/jitinterface.cppImplements helper resolution + runtime lookup computation for the new awaiter-in-continuation call.
src/coreclr/vm/corelib.hAdds CoreLibBinder method IDs for the new AsyncHelpers helper methods.
src/coreclr/tools/superpmi/superpmi/icorjitinfo.cppPlumbs the new ICorJitInfo method through SuperPMI.
src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cppUpdates generated shim to forward the new ICorJitInfo entrypoint.
src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cppUpdates generated counter shim to track/forward the new call.
src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cppUpdates collector shim to record/replay the new query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.hAdds recording/replay declarations and a new packet kind for the query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cppImplements record/dump/replay for the new query.
src/coreclr/tools/superpmi/superpmi-shared/lwmlist.hAdds LWM map entry for the query.
src/coreclr/tools/superpmi/superpmi-shared/agnostic.hAdds agnostic key struct for recording the query inputs.
src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txtAdds the new API to the JIT interface thunk generator input.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.csAdds ILC implementation to resolve the new helper method handle.
src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.csAdds generated unmanaged callback plumbing for the new API.
src/coreclr/tools/aot/jitinterface/jitinterface_generated.hUpdates NativeAOT jitinterface wrapper with the new callback.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.csEnsures R2R token availability for the new helper methods.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds runtime stack-state fields + dispatcher logic to call awaiter continuation callbacks.
src/coreclr/jit/wellknownargs.hIntroduces a new well-known arg kind for the async awaiter pseudo-arg.
src/coreclr/jit/importercalls.cppAdds importer rewrite to swap struct awaiter calls to the new helper and attach symbolic continuation-member offset.
src/coreclr/jit/ICorJitInfo_wrapper_generated.hppAdds wrapper method for the new JIT↔EE query.
src/coreclr/jit/ICorJitInfo_names_generated.hAdds the new API name for logging/tracing.
src/coreclr/jit/gtstructs.hExtends GenTreeVal struct mapping to include GT_CONTINUATION_MEMBER_OFFSET.
src/coreclr/jit/gtlist.hAdds the GT_CONTINUATION_MEMBER_OFFSET node kind and documentation.
src/coreclr/jit/gentree.cppUpdates pseudo-arg handling and debug printing for the new node.
src/coreclr/jit/compiler.hAdds compiler state for tracking continuation members and the importer helper prototype.
src/coreclr/jit/async.hAdds continuation-member abstractions and extends layout builder to allocate member offsets.
src/coreclr/jit/async.cppImplements continuation-member tracking, reserves storage in continuation layouts, and bashes symbolic offsets to constants.
src/coreclr/inc/jiteeversionguid.hBumps JIT↔EE version GUID due to interface change.
src/coreclr/inc/icorjitinfoimpl_generated.hAdds the new virtual method to the generated ICorJitInfo impl surface.
src/coreclr/inc/corinfo.hAdds the new ICorStaticInfo virtual method for helper resolution.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings July 24, 2026 19:57

CopilotAI 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 33 out of 33 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 13:41

CopilotAI 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 73 out of 73 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 15:31
Comment threadsrc/coreclr/jit/morph.cpp

@AndyAyersMSAndyAyersMS left a comment

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.

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?
  • Could the embedded awaiters hold onto GC refs longer than before?

@jtschusterjtschuster left a comment

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.

Just one question about test coverage, otherwise crossgen2 changes lgtm.

CopilotAI review requested due to automatic review settings August 4, 2026 10:46

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

Suppressed comments (1)

src/tests/async/custom-struct-awaiters/custom-struct-awaiters.cs:19

  • This xUnit test blocks on an async method via Run().Wait(). xUnit supports async test methods directly, and blocking waits can introduce deadlocks when a synchronization context is present (or hide hangs as AggregateException). Prefer returning Task from the [Fact] instead of blocking.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings August 4, 2026 10:55

CopilotAI 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 74 out of 74 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:117

  • Same as the safe path: this reads TAwaiter from raw continuation data via Unsafe.As, which can generate misaligned struct loads if the awaiter type has alignment requirements beyond pointer-size. Prefer Unsafe.ReadUnaligned<TAwaiter> here as well.
    src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:60
  • Unsafe.As<byte, TAwaiter> assumes the source address is suitably aligned for TAwaiter. Continuation member offsets are only guaranteed to be pointer-aligned, so on architectures with stricter alignment requirements this can result in misaligned struct loads. Use Unsafe.ReadUnaligned<TAwaiter> to safely read the awaiter from raw object data.
    src/coreclr/jit/async.cpp:2475
  • Stores into the continuation member don't currently propagate GTF_IND_UNALIGNED when the awaiter type's alignment exceeds the object/pointer alignment. Other continuation stores compute indirFlags based on required vs heap alignment; this path should do the same to avoid generating aligned stores to potentially unaligned offsets (especially for SIMD-containing structs).
 GenTreeFieldList* fieldList = awaiter->AsFieldList();
for (GenTreeFieldList::Use& use : fieldList->Uses())
{
if (!use.GetNode()->IsInvariant() && !use.GetNode()->OperIs(GT_LCL_VAR))
{
LIR::Use lirUse(LIR::AsRange(callBlock), &use.NodeRef(), fieldList);
lirUse.ReplaceWithLclVar(m_compiler);

src/coreclr/jit/async.cpp:2506

  • This struct store uses GTF_IND_NONFAULTING but omits GTF_IND_UNALIGNED when the awaiter layout has stricter alignment than the continuation/object can guarantee. Mirror the existing pattern used for locals/return stores by computing indirFlags and passing it to gtNewStoreValueNode.
 GenTree* continuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF);
unsigned offset = OFFSETOF__CORINFO_Continuation__data + layout.ContinuationMemberOffsets[memberIndex];
GenTree* offsetNode = m_compiler->gtNewIconNode((ssize_t)offset, TYP_I_IMPL);
GenTree* address = m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, continuation, offsetNode);
GenTree* store = m_compiler->gtNewStoreValueNode(awaiterLayout, address, awaiter, GTF_IND_NONFAULTING);
LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_compiler, store));

CopilotAI review requested due to automatic review settings August 4, 2026 11:23

CopilotAI 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 76 out of 76 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/coreclr/jit/async.h:7

  • async.h defines non-inline types (ContinuationMemberType, ContinuationMember, etc.) but still has no include guard / #pragma once. Now that more translation units include this header (e.g., gentree.cpp, importercalls.cpp), an accidental double-include in any TU would become a hard compile error (redefinition). Adding a conventional include guard would make this robust.
enum class ContinuationMemberType
{
CustomAwaiterOfLayout,
};

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?

  • Could the embedded awaiters hold onto GC refs longer than before?

I think it is unlikely that we grow the continuation size by a lot here since the awaiter storage is shared across await sites and is keyed on the awaiter layout. I don't think there will be that many different awaiter layouts in the same method, even with lots of different await sites.

For the GC question: the C# emitted code for the helper that is getting optimized looks like this:

TAwaiterawaiter=somethingAwaitable.GetAwaiter();if(!awaiter.IsCompleted){AsyncHelpers.UnsafeAwaitAwaiter(awaiter);}TResult=awaiter.GetResult();

So the GC refs would already be retained by this.

It also means that there is actually already the necessary state to reconstruct the awaiter stored in the continuation, so we are being a bit wasteful not reusing that. But that is quite complicated and error prone to do:

  1. With promotion and optimizations we cannot really guarantee that the state in the continuation is an exact TAwaiter, and I don't see a non-error prone way to guarantee that
  2. Without 1 I think we need something like Investigate generating resumption stubs as funclet-like code in runtime async functions #121013 where the JIT generates some funclet, but even if we could generate such funclets I also don't see how we would easily represent this.

So I went with this fix for .NET 11, mostly from worrying about runtime async boxing in cases where async1 wouldn't. I would like to eventually implement 2, but it is definitely going to take some work.

@VSadovVSadov left a comment

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.

LGTM!

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Failure in run was #131330

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in run was #131330

@jakobbotsch
jakobbotsch merged commit 174a8af into dotnet:mainAug 7, 2026
160 of 163 checks passed
@jakobbotsch
jakobbotsch deleted the fix-119842-2 branch August 7, 2026 01:56
jakobbotsch added a commit to jakobbotsch/runtime that referenced this pull request Aug 7, 2026
Upstream landed the reviewed version of the custom awaiter work this branch had
prototyped in f448161 (dotnet#131342), so the branch's copy is dropped in favor of
it everywhere the two collided:
* getAwaitAwaiterInContinuationCall now takes a CORINFO_RESOLVED_TOKEN rather
than a CORINFO_SIG_INFO, through corinfo.h, the generated thunks, the superpmi
shims and both JIT and VM sides.
* StoreAsyncAwaiter uses upstream's version, which handles under-aligned
awaiters by passing GTF_IND_UNALIGNED.
* The custom awaiter layout allocation uses ClassLayout::GetAlignmentRequirement.
* The YieldAwaiter special case this branch had added to
UnsafeAwaiterOnCompletedFromContinuation is dropped; upstream keeps that
optimization only on the boxing path in UnsafeAwaitAwaiter.
The continuation member abstraction stays as this branch has it, since async
inlining needs members that are not custom awaiters: ContinuationMember keeps
the inlined frame member types, GetStorageType and the inline depth keying, and
the layout keeps grouping a frame's members so they are allocated together.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
@dotnet-milestone-botdotnet-milestone-botBot modified the milestones: 11.0.0, 11.0-rc1Aug 7, 2026
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
This optimizes calls to
`AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter)` with struct
awaiters to instead call a new function
`AsyncHelpers.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 later `UnsafeOnCompleted` call
can then be done without any boxing by extracting it from the
continuation.
This introduces a new `GT_CONTINUATION_MEMBER_OFFSET` which 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.
There is a new `AsyncAwaiter` pseudo arg passed to
`UnsafeAwaitAwaiterInContinuation`, and expanded in the suspension path
by the async transformation to be stored in the continuation at the
right offset.
I have a couple of use cases for `GT_CONTINUATION_MEMBER_OFFSET` in
mind, so I have made the mechanism to represent the type of member
easily expandable (particularly inlining needs this as well).
This PR also removes configurable continuation reuse. This is now
unconditionally enabled, to simplify the expansion of
`GT_CONTINUATION_MEMBER_OFFSET` nodes.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIPriority:1Work that is critical for the release, but we could probably ship withoutruntime-async

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runtime async should avoid boxing custom awaiters on suspension

7 participants

@jakobbotsch@VSadov@AndyAyersMS@jtschuster@am11@JulieLeeMSFT
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Remove boxing for awaited custom awaiters - #131342

Merged
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2
Aug 7, 2026
Merged

Remove boxing for awaited custom awaiters#131342
jakobbotsch merged 33 commits into
dotnet:mainfrom
jakobbotsch:fix-119842-2

Conversation

@jakobbotsch

@jakobbotschjakobbotsch commented Jul 24, 2026

Copy link
Copy Markdown
Member

This optimizes calls to AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter) with struct awaiters to instead call a new function AsyncHelpers.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 later UnsafeOnCompleted call can then be done without any boxing by extracting it from the continuation.

This introduces a new GT_CONTINUATION_MEMBER_OFFSET which 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.
There is a new AsyncAwaiter pseudo arg passed to UnsafeAwaitAwaiterInContinuation, and expanded in the suspension path by the async transformation to be stored in the continuation at the right offset.

I have a couple of use cases for GT_CONTINUATION_MEMBER_OFFSET in mind, so I have made the mechanism to represent the type of member easily expandable (particularly inlining needs this as well).

This PR also removes configurable continuation reuse. This is now unconditionally enabled, to simplify the expansion of GT_CONTINUATION_MEMBER_OFFSET nodes.

Fix#119842

Microbenchmark

About 10% improvement, and more importantly, avoids the box that async1 also avoids to gain parity.

usingSystem;usingSystem.Diagnostics;usingSystem.Runtime.CompilerServices;usingSystem.Threading;usingSystem.Threading.Tasks;publicstaticclassProgram{staticActions_continuation;staticlongs_value;publicstaticvoidMain(){for(inti=0;i<10;i++){for(intj=0;j<100;j++){Taskt=Foo(100);while(!t.IsCompleted)s_continuation();}Thread.Sleep(100);}for(inti=0;i<50;i++){Taskt=Foo(10_000_000);while(!t.IsCompleted)s_continuation();}}privatestaticasyncTaskFoo(intn){s_value=0;Stopwatchtimer=Stopwatch.StartNew();for(inti=0;i<n;i++){awaitnewAwaiter(i);}if(n>100)Console.WriteLine("Took {0} ms",timer.ElapsedMilliseconds);Trace.Assert(s_value==((long)n*(n-1))/2);}privatestructAwaiter:ICriticalNotifyCompletion{publicintX;publicAwaiter(intx)=>X=x;publicboolIsCompleted=>false;publicAwaiterGetAwaiter()=>this;publicvoidGetResult(){}publicvoidOnCompleted(Actioncontinuation){}publicvoidUnsafeOnCompleted(Actioncontinuation){s_value+=X;s_continuation=continuation;}}}
-Took 323 ms-Took 318 ms-Took 320 ms-Took 323 ms-Took 322 ms-Took 318 ms-Took 320 ms-Took 318 ms-Took 317 ms-Took 319 ms+Took 290 ms+Took 291 ms+Took 292 ms+Took 288 ms+Took 292 ms+Took 291 ms+Took 290 ms+Took 290 ms+Took 287 ms+Took 292 ms

CopilotAI review requested due to automatic review settings July 24, 2026 19:11
@github-actionsgithub-actionsBot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 24, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

CopilotAI 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 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/UnsafeOnCompleted by reading the awaiter out of the continuation.
  • Add GT_CONTINUATION_MEMBER_OFFSET plus 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
FileDescription
src/tests/async/struct/struct.csAdds a regression test for safe and unsafe custom struct awaiters under runtime async.
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.csAdds new private awaiter-in-continuation helpers and extraction-based OnCompleted/UnsafeOnCompleted paths.
src/coreclr/vm/jitinterface.hAdds VM declaration for the new JIT↔EE query to resolve the awaiter-in-continuation helper.
src/coreclr/vm/jitinterface.cppImplements helper resolution + runtime lookup computation for the new awaiter-in-continuation call.
src/coreclr/vm/corelib.hAdds CoreLibBinder method IDs for the new AsyncHelpers helper methods.
src/coreclr/tools/superpmi/superpmi/icorjitinfo.cppPlumbs the new ICorJitInfo method through SuperPMI.
src/coreclr/tools/superpmi/superpmi-shim-simple/icorjitinfo_generated.cppUpdates generated shim to forward the new ICorJitInfo entrypoint.
src/coreclr/tools/superpmi/superpmi-shim-counter/icorjitinfo_generated.cppUpdates generated counter shim to track/forward the new call.
src/coreclr/tools/superpmi/superpmi-shim-collector/icorjitinfo.cppUpdates collector shim to record/replay the new query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.hAdds recording/replay declarations and a new packet kind for the query.
src/coreclr/tools/superpmi/superpmi-shared/methodcontext.cppImplements record/dump/replay for the new query.
src/coreclr/tools/superpmi/superpmi-shared/lwmlist.hAdds LWM map entry for the query.
src/coreclr/tools/superpmi/superpmi-shared/agnostic.hAdds agnostic key struct for recording the query inputs.
src/coreclr/tools/Common/JitInterface/ThunkGenerator/ThunkInput.txtAdds the new API to the JIT interface thunk generator input.
src/coreclr/tools/Common/JitInterface/CorInfoImpl.csAdds ILC implementation to resolve the new helper method handle.
src/coreclr/tools/Common/JitInterface/CorInfoImpl_generated.csAdds generated unmanaged callback plumbing for the new API.
src/coreclr/tools/aot/jitinterface/jitinterface_generated.hUpdates NativeAOT jitinterface wrapper with the new callback.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/ReadyToRunCodegenCompilation.csEnsures R2R token availability for the new helper methods.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.csAdds runtime stack-state fields + dispatcher logic to call awaiter continuation callbacks.
src/coreclr/jit/wellknownargs.hIntroduces a new well-known arg kind for the async awaiter pseudo-arg.
src/coreclr/jit/importercalls.cppAdds importer rewrite to swap struct awaiter calls to the new helper and attach symbolic continuation-member offset.
src/coreclr/jit/ICorJitInfo_wrapper_generated.hppAdds wrapper method for the new JIT↔EE query.
src/coreclr/jit/ICorJitInfo_names_generated.hAdds the new API name for logging/tracing.
src/coreclr/jit/gtstructs.hExtends GenTreeVal struct mapping to include GT_CONTINUATION_MEMBER_OFFSET.
src/coreclr/jit/gtlist.hAdds the GT_CONTINUATION_MEMBER_OFFSET node kind and documentation.
src/coreclr/jit/gentree.cppUpdates pseudo-arg handling and debug printing for the new node.
src/coreclr/jit/compiler.hAdds compiler state for tracking continuation members and the importer helper prototype.
src/coreclr/jit/async.hAdds continuation-member abstractions and extends layout builder to allocate member offsets.
src/coreclr/jit/async.cppImplements continuation-member tracking, reserves storage in continuation layouts, and bashes symbolic offsets to constants.
src/coreclr/inc/jiteeversionguid.hBumps JIT↔EE version GUID due to interface change.
src/coreclr/inc/icorjitinfoimpl_generated.hAdds the new virtual method to the generated ICorJitInfo impl surface.
src/coreclr/inc/corinfo.hAdds the new ICorStaticInfo virtual method for helper resolution.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings July 24, 2026 19:57

CopilotAI 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 33 out of 33 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 13:41

CopilotAI 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 73 out of 73 changed files in this pull request and generated no new comments.

CopilotAI review requested due to automatic review settings July 27, 2026 15:31
Comment threadsrc/coreclr/jit/morph.cpp

@AndyAyersMSAndyAyersMS left a comment

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.

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?
  • Could the embedded awaiters hold onto GC refs longer than before?

@jtschusterjtschuster left a comment

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.

Just one question about test coverage, otherwise crossgen2 changes lgtm.

CopilotAI review requested due to automatic review settings August 4, 2026 10:46

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

Suppressed comments (1)

src/tests/async/custom-struct-awaiters/custom-struct-awaiters.cs:19

  • This xUnit test blocks on an async method via Run().Wait(). xUnit supports async test methods directly, and blocking waits can introduce deadlocks when a synchronization context is present (or hide hangs as AggregateException). Prefer returning Task from the [Fact] instead of blocking.

Comment threadsrc/coreclr/jit/importercalls.cpp
CopilotAI review requested due to automatic review settings August 4, 2026 10:55

CopilotAI 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 74 out of 74 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:117

  • Same as the safe path: this reads TAwaiter from raw continuation data via Unsafe.As, which can generate misaligned struct loads if the awaiter type has alignment requirements beyond pointer-size. Prefer Unsafe.ReadUnaligned<TAwaiter> here as well.
    src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs:60
  • Unsafe.As<byte, TAwaiter> assumes the source address is suitably aligned for TAwaiter. Continuation member offsets are only guaranteed to be pointer-aligned, so on architectures with stricter alignment requirements this can result in misaligned struct loads. Use Unsafe.ReadUnaligned<TAwaiter> to safely read the awaiter from raw object data.
    src/coreclr/jit/async.cpp:2475
  • Stores into the continuation member don't currently propagate GTF_IND_UNALIGNED when the awaiter type's alignment exceeds the object/pointer alignment. Other continuation stores compute indirFlags based on required vs heap alignment; this path should do the same to avoid generating aligned stores to potentially unaligned offsets (especially for SIMD-containing structs).
 GenTreeFieldList* fieldList = awaiter->AsFieldList();
for (GenTreeFieldList::Use& use : fieldList->Uses())
{
if (!use.GetNode()->IsInvariant() && !use.GetNode()->OperIs(GT_LCL_VAR))
{
LIR::Use lirUse(LIR::AsRange(callBlock), &use.NodeRef(), fieldList);
lirUse.ReplaceWithLclVar(m_compiler);

src/coreclr/jit/async.cpp:2506

  • This struct store uses GTF_IND_NONFAULTING but omits GTF_IND_UNALIGNED when the awaiter layout has stricter alignment than the continuation/object can guarantee. Mirror the existing pattern used for locals/return stores by computing indirFlags and passing it to gtNewStoreValueNode.
 GenTree* continuation = m_compiler->gtNewLclvNode(GetNewContinuationVar(), TYP_REF);
unsigned offset = OFFSETOF__CORINFO_Continuation__data + layout.ContinuationMemberOffsets[memberIndex];
GenTree* offsetNode = m_compiler->gtNewIconNode((ssize_t)offset, TYP_I_IMPL);
GenTree* address = m_compiler->gtNewOperNode(GT_ADD, TYP_BYREF, continuation, offsetNode);
GenTree* store = m_compiler->gtNewStoreValueNode(awaiterLayout, address, awaiter, GTF_IND_NONFAULTING);
LIR::AsRange(suspendBB).InsertAtEnd(LIR::SeqTree(m_compiler, store));

CopilotAI review requested due to automatic review settings August 4, 2026 11:23

CopilotAI 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 76 out of 76 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/coreclr/jit/async.h:7

  • async.h defines non-inline types (ContinuationMemberType, ContinuationMember, etc.) but still has no include guard / #pragma once. Now that more translation units include this header (e.g., gentree.cpp, importercalls.cpp), an accidental double-include in any TU would become a hard compile error (redefinition). Adding a conventional include guard would make this robust.
enum class ContinuationMemberType
{
CustomAwaiterOfLayout,
};

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

JIT changes LGTM with few nits.

Some questions:

  • Seems like we are now paying up-front for space that we might not ever use. Is there any limit or tradeoff we should consider? Maybe especially so for shared continuations?

  • Could the embedded awaiters hold onto GC refs longer than before?

I think it is unlikely that we grow the continuation size by a lot here since the awaiter storage is shared across await sites and is keyed on the awaiter layout. I don't think there will be that many different awaiter layouts in the same method, even with lots of different await sites.

For the GC question: the C# emitted code for the helper that is getting optimized looks like this:

TAwaiterawaiter=somethingAwaitable.GetAwaiter();if(!awaiter.IsCompleted){AsyncHelpers.UnsafeAwaitAwaiter(awaiter);}TResult=awaiter.GetResult();

So the GC refs would already be retained by this.

It also means that there is actually already the necessary state to reconstruct the awaiter stored in the continuation, so we are being a bit wasteful not reusing that. But that is quite complicated and error prone to do:

  1. With promotion and optimizations we cannot really guarantee that the state in the continuation is an exact TAwaiter, and I don't see a non-error prone way to guarantee that
  2. Without 1 I think we need something like Investigate generating resumption stubs as funclet-like code in runtime async functions #121013 where the JIT generates some funclet, but even if we could generate such funclets I also don't see how we would easily represent this.

So I went with this fix for .NET 11, mostly from worrying about runtime async boxing in cases where async1 wouldn't. I would like to eventually implement 2, but it is definitely going to take some work.

@VSadovVSadov left a comment

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.

LGTM!

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

Failure in run was #131330

@jakobbotsch

Copy link
Copy Markdown
MemberAuthor

/ba-g Failure in run was #131330

@jakobbotsch
jakobbotsch merged commit 174a8af into dotnet:mainAug 7, 2026
160 of 163 checks passed
@jakobbotsch
jakobbotsch deleted the fix-119842-2 branch August 7, 2026 01:56
jakobbotsch added a commit to jakobbotsch/runtime that referenced this pull request Aug 7, 2026
Upstream landed the reviewed version of the custom awaiter work this branch had
prototyped in f448161 (dotnet#131342), so the branch's copy is dropped in favor of
it everywhere the two collided:
* getAwaitAwaiterInContinuationCall now takes a CORINFO_RESOLVED_TOKEN rather
than a CORINFO_SIG_INFO, through corinfo.h, the generated thunks, the superpmi
shims and both JIT and VM sides.
* StoreAsyncAwaiter uses upstream's version, which handles under-aligned
awaiters by passing GTF_IND_UNALIGNED.
* The custom awaiter layout allocation uses ClassLayout::GetAlignmentRequirement.
* The YieldAwaiter special case this branch had added to
UnsafeAwaiterOnCompletedFromContinuation is dropped; upstream keeps that
optimization only on the boxing path in UnsafeAwaitAwaiter.
The continuation member abstraction stays as this branch has it, since async
inlining needs members that are not custom awaiters: ContinuationMember keeps
the inlined frame member types, GetStorageType and the inline depth keying, and
the layout keeps grouping a frame's members so they are allocated together.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed3e552-b907-4815-9683-56a7fed2cdf3
@dotnet-milestone-botdotnet-milestone-botBot modified the milestones: 11.0.0, 11.0-rc1Aug 7, 2026
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Aug 11, 2026
This optimizes calls to
`AsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>(TAwaiter)` with struct
awaiters to instead call a new function
`AsyncHelpers.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 later `UnsafeOnCompleted` call
can then be done without any boxing by extracting it from the
continuation.
This introduces a new `GT_CONTINUATION_MEMBER_OFFSET` which 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.
There is a new `AsyncAwaiter` pseudo arg passed to
`UnsafeAwaitAwaiterInContinuation`, and expanded in the suspension path
by the async transformation to be stored in the continuation at the
right offset.
I have a couple of use cases for `GT_CONTINUATION_MEMBER_OFFSET` in
mind, so I have made the mechanism to represent the type of member
easily expandable (particularly inlining needs this as well).
This PR also removes configurable continuation reuse. This is now
unconditionally enabled, to simplify the expansion of
`GT_CONTINUATION_MEMBER_OFFSET` nodes.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMIPriority:1Work that is critical for the release, but we could probably ship withoutruntime-async

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Runtime async should avoid boxing custom awaiters on suspension

7 participants

@jakobbotsch@VSadov@AndyAyersMS@jtschuster@am11@JulieLeeMSFT