Recognize Task<->ValueTask adaptions in async versions #130081
Recognize Task<->ValueTask adaptions in async versions #130081jakobbotsch wants to merge 9 commits into
Conversation
Support looking through `return new ValueTask(TaskReturningFunction())` and `return ValueTaskReturningFunction().AsTask()` in async versions. These can be transformed into async calls.
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
| // We cannot inline if the callee returns valueTask.AsTask() in an | ||
| // async version. We need to preserve the continuation in this case to | ||
| // be able to mark it with CORINFO_CONTINUATION_VALUETASK_ADAPTED_TO_TASK. | ||
| if ((prefixFlags & PREFIX_IS_ADAPTED_FROM_VALUETASK) != 0) | ||
| { | ||
| compInlineResult->NoteFatal(InlineObservation::CALLEE_AWAIT); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Not very satisfied with this. The main problem is that return valueTask.AsTask() comes with additional ceremony around handling IValueTaskSource because the returned Task does not have the exact same semantics as ValueTask for that case. In particular the Task adapter always passes ValueTaskSourceOnCompletedFlags.None to the IValueTaskSource. We need to teach our IValueTaskSource handling about this special case, and that requires introducing the new flag, but then we end up not able to inline once we see this case.
One solution may be to have the JIT insert an extra Continuation into the chain when this flag is present if we inlined.
In any case I think this optimization even if it means we disallow inlining will still be beneficial.
There was a problem hiding this comment.
Pull request overview
This PR extends CoreCLR “runtime-async / async-version” recognition so the JIT can see through common Task↔ValueTask adapters (new ValueTask(task) and valueTask.AsTask()) and treat them as direct async calls, reducing wrapper overhead. It also adds tests intended to cover these adapter patterns.
Changes:
- Mark
ValueTask/ValueTask<T>Task-based constructors andAsTask()methods as[Intrinsic]so the JIT can pattern-match them. - Teach the JIT importer to recognize async-version tail patterns involving
ValueTaskadapters and propagate a new “adapted” flag into async-call lowering and continuation flags. - Add/adjust async tests/projects for adapter scenarios and align the IL project’s assembly naming.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/tests/async/async-versions/async-versions.ilproj | Adds an IL SDK project for the IL-authored async-version tests; removes injected C# helper attribute source. |
| src/tests/async/async-versions/async-versions.il | Renames the IL assembly to match the project name (async-versions). |
| src/tests/async/async-versions-task-adapters/async-versions-task-adapters.csproj | New test project to validate Task↔ValueTask adapter behavior. |
| src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs | New xUnit tests covering new ValueTask(Task), ValueTask.AsTask(), and ValueTask<Task<int>> overload binding behavior. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs | Adds [Intrinsic] to relevant constructors/AsTask() overloads to enable JIT recognition. |
| src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs | Adds a new continuation flag bit and shifts bitfield layout constants. |
| src/coreclr/jit/namedintrinsiclist.h | Adds new NamedIntrinsic IDs for ValueTask/ValueTask<T> ctor and AsTask. |
| src/coreclr/jit/importercalls.cpp | Blocks certain tail-await optimizations/inlining for adapted patterns; wires new intrinsic lookups. |
| src/coreclr/jit/importer.cpp | Factors stloc/ldloca matching; adds async-version tail-call pattern matching for AsTask() and newobj ValueTask(..). |
| src/coreclr/jit/gentree.h | Adds AsyncCallInfo.IsValueTaskAsTask marker. |
| src/coreclr/jit/compiler.h | Adds PREFIX_IS_ADAPTED_FROM_VALUETASK prefix flag. |
| src/coreclr/jit/async.cpp | Sets the new continuation flag based on IsValueTaskAsTask. |
| src/coreclr/inc/corinfo.h | Adds CORINFO_CONTINUATION_VALUETASK_ADAPTED_TO_TASK and updates bitfield layout constants. |
| if (info.compRetType != TYP_VOID) | ||
| { | ||
| assert((sig.sigInst.classInstCount == 1) && (sig.sigInst.methInstCount == 0)); | ||
| CORINFO_CLASS_HANDLE paramClass = info.compCompHnd->getArgClass(&sig, sig.args); | ||
| if (paramClass == sig.sigInst.classInst[0]) | ||
| { | ||
| // This is "class ValueTask<T> { ValueTask(T value) }" overload | ||
| // which is not what we are looking for. That one gets folded | ||
| // by impFoldAwaitedTopOfStack. | ||
| return false; | ||
| } | ||
| } |
| nextOpcode++; | ||
|
|
||
| // Quick check for ret before we resolve the token | ||
| if (nextOpcode + sizeof(mdToken) >= codeEndp || (*(nextOpcode + sizeof(mdToken)) != CEE_RET)) |
There was a problem hiding this comment.
In impMatchAsyncVersionTailCall (importer.cpp), the bounds checks at lines 6055 and 6089 both use nextOpcode + sizeof(mdToken) >= codeEndp before peeking the byte at nextOpcode + sizeof(mdToken). Since a valid read only requires that index to be strictly less than codeEndp, the >= rejects the case where nextOpcode + sizeof(mdToken) == codeEndp - 1 — i.e. when the trailing CEE_RET is the very last byte of the method.
Is that intentional (over-conservative but safe), or an off-by-one that silently misses legitimate tail-await matches at end-of-method? A method ending in ... call ValueTask.AsTask(); ret with the ret as the final byte seems like it should match but wouldn't. If it's deliberate, a one-line comment noting why would help; otherwise > looks like the correct comparison here. A targeted test with the pattern placed at the exact end of the IL stream would pin down the behavior.
🤖 This comment was generated with the assistance of AI.
There was a problem hiding this comment.
I think it looks right. If nextOpcode + sizeof(mdToken) >= codeEndp then we short circuit and return false to avoid an out of bounds in the next half of the expression.
…frastructure `NonVersionable` (#129894) We do not need to save/restore contexts in synchronously finishing task-returning thunks since that already happens in the JIT generated code for async functions. Also make the types and methods used in the thunks `NonVersionable` for more efficient handling by crossgen2. Bump R2R version, and at the same time take the opportunity to rename various internal functions: - Rename `FinalizeTaskReturningThunk` and `FinalizeValueTaskReturningThunk` to `CreateRuntimeAsyncTask` and `CreateRuntimeAsyncValueTask` respectively - All await helpers that do not check for completion and that always suspend immediately have been renamed to `Suspend` and `TransparentSuspend` - `TransparentAwaitWithResult` overloads have been renamed to `TransparentAwait` With the R2R version bump, also free up a flag bit in continuation flags to be used for #130081.
Support looking through
return new ValueTask(TaskReturningFunction())andreturn ValueTaskReturningFunction().AsTask()in async versions. These can be transformed into async calls.This is a common pattern in the BCL, e.g.:
runtime/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.Tasks.cs
Line 268 in 06362e8
runtime/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Unix.cs
Line 176 in 5ee89e4
Example:
Diff: