Recognize Task<->ValueTask adaptions in async versions #130081
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 |
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. |
…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.
AndyAyersMS
left a comment
There was a problem hiding this comment.
JIT changes look ok.
How robust are all these patterns? Is missing one just a lost optimization?
The interpreter pattern match machinery looks appealing, though I suppose in the JIT we also need to bind on certain bits of info along the way and/ or do further valuation so maybe it can't be this simple?
Yes, it just results in losing the direct runtime async call, meaning we materialize a Task object and await it via It would be interesting to do this optimization on JIT IR too by pushing runtime/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/Pipe.cs Lines 753 to 771 in 9d70f01 Conceptually, this gets transformed to ValueTask<ReadResult> result;
lock (SyncObj)
{
_readerAwaitable.BeginOperation(token, s_signalReaderAwaitable, this);
// If the awaitable is already complete then return the value result directly
if (_readerAwaitable.IsCompleted)
{
GetReadResult(out ReadResult readResult);
result = new ValueTask<ReadResult>(readResult);
}
else
{
// Otherwise it's async
result = new ValueTask<ReadResult>(_reader, token: 0);
}
}
return AsyncHelpers.TransparentAwait(result);in the async version of this method. It would have better performance if written as: ReadResult syncResult = default;
IValueTaskSource<ReadResult> asyncResult = null;
lock (SyncObj)
{
_readerAwaitable.BeginOperation(token, s_signalReaderAwaitable, this);
// If the awaitable is already complete then return the value result directly
if (_readerAwaitable.IsCompleted)
{
GetReadResult(out syncResult);
goto ReturnSync;
}
else
{
// Otherwise it's async
asyncResult = _reader;
goto ReturnAsync;
}
}
ReturnSync:
return new ValueTask(syncResult);
ReturnAsync:
return new ValueTask(_reader, token: 0);since we know how to fold Ideally we would do something like this automatically. In this case there is actually intervening side effect (exiting the lock), but I would expect there to be cases too where we could switch task-returning calls to runtime async calls if we did this.
I like it for the simple things, but in this particular case we need to match |
|
|
Valid IL can run out of bytes in these checks because the bytes we are matching on span only to the next basic block start, not until the end of the IL.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
|
/azp run runtime-nativeaot-outerloop |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
Last nativeaot-outerloop failure is the one being fixed in #130618 |
ValueTask is better than Task in two scenarios (and they are exactly the scenarios when VT does not wrap Task):
The "completed result" scenario is naturally matched quite well in runtime async. The "async result generator" scenario is done via the same |
|
/ba-g Unknown failure is the one fixed in #130618 |
Just to be clear: I think @VSadov is talking about the scenarios that led to |
…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: