JIT: Use a "resumed?" indicator variable in runtime async instead of lvaAsyncContinuationArg != nullptr - #128152
Conversation
We will need this to enable inlining.
|
cc @AndyAyersMS, this is the PR with the indicator variables we discussed |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
There was a problem hiding this comment.
Pull request overview
Replaces the implicit "have we resumed?" check (lvaAsyncContinuationArg != nullptr) with an explicit boolean indicator local (lvaResumedIndicator). The root motivation is to enable async inlining, where the existing check is not valid for inlinees. Two new pseudo-args (AsyncResumedUse / AsyncResumedDef) are threaded through every async call so each callsite can read the indicator and the resumption path can write 1 back into it; the JIT machinery (lcl morph, address-of escape analysis, fgInline, local def visitors, diagnostic checks) is updated to recognize the new defined-via-address local.
Changes:
- Introduce
lvaResumedIndicator(TYP_UBYTE) and initialize it in the prolog / OSR entry / loops, replacing the inline-special-casedcontinuation != nullrecipe at all uses. - Add
WellKnownArg::AsyncResumedUse/AsyncResumedDefplusgtCallGetDefinedAsyncResumedLclAddr, and propagate them through arg setup, IR sequencing, escape analysis, local-def visiting, and diagnostic checks. - In
AsyncTransformation, store1to the indicator on the resumption path, add common-def head-merging into the resumption switch, and add a shared "resumed" temp for shared finish-context handling.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/jit/async.h | Declares new shared resumed var, helper signatures, and updated parameter lists for finish-context BB construction. |
| src/coreclr/jit/async.cpp | Core change: introduces indicator local, initializes it, threads new well-known args through calls, writes 1 on resumption, and implements common-def head merging. |
| src/coreclr/jit/compiler.h / compiler.cpp | Adds lvaResumedIndicator field and includes it in the OSR-local sanity check. |
| src/coreclr/jit/compiler.hpp | Extends VisitLocalDefs / VisitLocalDefNodes to also visit the async-resumed def on calls. |
| src/coreclr/jit/gentree.h / gentree.cpp | Adds the two new WellKnownArg values, their names, and gtCallGetDefinedAsyncResumedLclAddr. |
| src/coreclr/jit/morph.cpp | Wires the new well-known arg names into getWellKnownArgName. |
| src/coreclr/jit/importer.cpp / fginline.cpp | Skips the new pseudo-args in inline arg-info construction and prepend-statements. |
| src/coreclr/jit/lclmorph.cpp | Generalizes the callsite address-escape logic to recognize the resumed def (size 1) alongside retbuf, and sequences the def address. |
| src/coreclr/jit/lclvars.cpp | Minor cleanup: uses INDEBUG(...) macro for SetDefinedViaAddress. |
| src/coreclr/jit/fgdiagnostic.cpp | Extends linked-locals debug check to also push/expect the async-resumed def. |
Invariant nodes and LCL_VARs do not need to be lifted across async calls.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
src/coreclr/jit/async.cpp:3618
CreateSharedFinishContextHandlingBBcreates the shared "resumed" local asTYP_UBYTEand also loads it asTYP_UBYTE. This value is later substituted into an already-morphed helper call whose placeholder arg was anint, so passing a 1-byte typed node here can lead to arg type / ABI mismatches. Load the local asTYP_INT(keeping the underlying local asTYP_UBYTEif desired) to match the rest of the async resumed flow, which consistently usesTYP_INTnodes.
GenTree* resumed;
if (invariantResumed == nullptr)
{
if (m_sharedFinishContextHandlingResumedVar == BAD_VAR_NUM)
{
m_sharedFinishContextHandlingResumedVar =
m_compiler->lvaGrabTemp(false DEBUGARG("'resumed' for shared finish context handling"));
m_compiler->lvaGetDesc(m_sharedFinishContextHandlingResumedVar)->lvType = TYP_UBYTE;
}
resumed = m_compiler->gtNewLclVarNode(m_sharedFinishContextHandlingResumedVar, TYP_UBYTE);
}
Consolidate the duplicated non-null assert in FinishContextHandlingAndSuspensionWithHelper into a single assert covering all three well-known args. Update the stale comment in InsertFinishContextHandlingCall that still described the resumed placeholder as being replaced by a 'continuation != null' computation; it is now replaced by the already-computed resumed value. No functional change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d86cad55-abc0-42d6-8369-bc6bc619326f
Value numbering can fold the AsyncResumedUse argument to a constant when the resumed indicator is provably zero at the call, for example when two awaits sit on mutually exclusive paths. IsReusableSuspension only accepted locals, so it bailed out with 'argument is too complex' and gave up on merging the suspension points. That produced a duplicated CORINFO_HELP_ALLOC_CONTINUATION sequence, an extra resumption block and an extra readonly data slot. In smoke_tests.nativeaot this showed up as +59 bytes (17.15%) on Generics+TestAsyncGVMScenarios:AsyncGvm1[System.__Canon] and +57 bytes (16.52%) on RunAsync. Accept invariant nodes as well, matching the predicate already used for invariantResumed in CreateResumptionsAndSuspensions. Invariant nodes are constants, LCL_ADDR or FTN_ADDR, so comparing them and removing them from the call is safe. Both methods now improve by 15 bytes relative to the base instead of regressing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d86cad55-abc0-42d6-8369-bc6bc619326f
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
src/coreclr/jit/gentree.cpp:21662
gtCallGetDefinedAsyncResumedLclAddrassumes the AsyncResumedDef argument node is always a rawGT_LCL_ADDR. HoweverCallArg::GetNode()can be aGT_PUTARG_*after lowering (same reasongtCallGetDefinedRetBufLclAddrunwrapsPUTARG_*and callsgtSkipReloadOrCopy). This can trigger theassert(node->OperIs(GT_LCL_ADDR) ...)in debug builds and mis-identify the def when the argument is wrapped.
Consider unwrapping GT_PUTARG_REG/GT_PUTARG_STK and skipping reload/copy, mirroring the retbuf helper.
src/coreclr/jit/compiler.hpp:4630
VisitLocalDefscomputesisEntirefor the AsyncResumedDef call definition vialvaLclExactSize(...) == 1. That ignores the def's offset and doesn't use the sameIsEntireAccess(...)logic as the retbuf call definition andLocalAddressVisitor(which setsGTF_VAR_USEASGbased onIsEntireAccess).
To keep the call-def contract consistent (esp. if the resumed indicator ever becomes a field/offset def), compute isEntire using IsEntireAccess(lclNum, offs, ValueSize(1)).
GenTreeLclVarCommon* asyncResumedLclAddr = comp->gtCallGetDefinedAsyncResumedLclAddr(call);
if (asyncResumedLclAddr != nullptr)
{
bool isEntire = comp->lvaLclExactSize(asyncResumedLclAddr->GetLclNum()) == 1;
RETURN_IF_ABORT(
visitor(LocalDef(asyncResumedLclAddr, isEntire, asyncResumedLclAddr->GetLclOffs(), ValueSize(1))));
}
|
Azure Pipelines: Successfully started running 5 pipeline(s). 11 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
cc @dotnet/jit-contrib PTAL @AndyAyersMS Diffs. TP regressions are a bit larger than I'd like. Looking at the detailed TP diffs it looks like we no longer inline a bunch of the visitor functions passed into |
AndyAyersMS
left a comment
There was a problem hiding this comment.
This is more complicated than I would have imagined.
I think it's more or less the cost of making the dependencies of the async transformation visible in the IR. Before this PR the uses of this state just weren't visible -- e.g. on suspension we need to query information about "have we resumed before?". We did that by checking if there is a non-null continuation argument, which is not correct once inlining is in the picture. And then we furthermore need to model the fact that async resumptions conditionally define that state. If we expanded the async transformation earlier it would be simpler since we could just introduce the IR there. Now that we model this in IR I think it does subsume some of the previous stuff. The async transformation does |
Conflicts in the runtime async transformation with the new "resumed?" indicator variable from dotnet#128152: - async.h: combined the new AsyncResumedUse/AsyncResumedDef parameters and FindAndRemoveCommonAsyncResumedDef with the ContinuationLayout return value and continuation member offset output of CreateResumptionsAndSuspensions. - async.cpp: call FindAndRemoveCommonAsyncResumedDef before creating resumptions and suspensions and pass the result to CreateResumptionSwitch, and add AsyncAwaiter to the well known argument lists used when validating and removing arguments for reused continuations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 757a9e4c-44f3-4255-86f6-065ebb823ed8
We will need this to enable inlining, since
lvaAsyncContinuationArg != nullptris not a correct way to compute this value for inlinees.Diffs are pretty mixed and not generally improvements since we have to generate stores to this local now. One possibility is that we use
lvaAsyncContinuationArg != nullfor the root function and only use indicator locals in inlinees instead. This would also make things simpler for OSR.