Skip to content

Recognize Task<->ValueTask adaptions in async versions #130081

Merged
jakobbotsch merged 15 commits into
dotnet:mainfrom
jakobbotsch:async-versions-adapt-tasks
Jul 14, 2026
Merged

Recognize Task<->ValueTask adaptions in async versions #130081
jakobbotsch merged 15 commits into
dotnet:mainfrom
jakobbotsch:async-versions-adapt-tasks

Conversation

@jakobbotsch

@jakobbotsch jakobbotsch commented Jul 1, 2026

Copy link
Copy Markdown
Member

Support looking through return new ValueTask(TaskReturningFunction()) and return ValueTaskReturningFunction().AsTask() in async versions. These can be transformed into async calls.

This is a common pattern in the BCL, e.g.:

return ReceiveAsync(buffer, socketFlags, fromNetworkStream, default).AsTask();

return new ValueTask(WriteAsyncCore(buffer, cancellationToken));

Example:

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static ValueTask TestTaskToValueTask()
    {
        return new ValueTask(TestTask());
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static async Task TestTask()
    {
        await Task.Yield();
    }

Diff:

 G_M37697_IG01:
-       push     rbp
-       sub      rsp, 80
-       lea      rbp, [rsp+0x50]
-       vxorps   xmm4, xmm4, xmm4
-       vmovdqu  ymmword ptr [rbp-0x30], ymm4
-       vmovdqa  xmmword ptr [rbp-0x10], xmm4
-       mov      gword ptr [rbp+0x10], rcx
-						;; size=28 bbWeight=1 PerfScore 5.08
+       sub      rsp, 40
+						;; size=4 bbWeight=1 PerfScore 0.25
 G_M37697_IG02:
-       vxorps   xmm0, xmm0, xmm0
-       vmovdqu  xmmword ptr [rbp-0x10], xmm0
-       call     [AsyncMicro.Program:TestTask():System.Threading.Tasks.Task]
-       mov      gword ptr [rbp-0x18], rax
-       mov      rdx, gword ptr [rbp-0x18]
-       lea      rcx, [rbp-0x10]
-       call     [System.Threading.Tasks.ValueTask:.ctor(System.Threading.Tasks.Task):this]
-						;; size=33 bbWeight=1 PerfScore 9.83
-G_M37697_IG03:
-       vmovdqu  xmm0, xmmword ptr [rbp-0x10]
-       vmovdqu  xmmword ptr [rbp-0x28], xmm0
-						;; size=10 bbWeight=1 PerfScore 2.00
-G_M37697_IG04:
-       lea      rdx, [rbp-0x28]
        xor      rcx, rcx
-       call     [System.Runtime.CompilerServices.AsyncHelpers:TransparentAwaitWithResult(System.Threading.Tasks.ValueTask)]
-       mov      gword ptr [rbp-0x30], rcx
-       cmp      gword ptr [rbp-0x30], 0
-       jne      SHORT G_M37697_IG06
+       call     [AsyncMicro.Program:TestTask()]
+       test     rcx, rcx
+       jne      SHORT G_M37697_IG04
        xor      ecx, ecx
-						;; size=25 bbWeight=1 PerfScore 7.00
-G_M37697_IG05:
-       add      rsp, 80
-       pop      rbp
+						;; size=15 bbWeight=1 PerfScore 4.75
+G_M37697_IG03:
+       add      rsp, 40
        ret      
-						;; size=6 bbWeight=1 PerfScore 1.75
-G_M37697_IG06:
-       mov      rax, gword ptr [rbp-0x30]
-       mov      rcx, rax
-						;; size=7 bbWeight=0 PerfScore 0.00
-G_M37697_IG07:
-       add      rsp, 80
-       pop      rbp
+						;; size=5 bbWeight=1 PerfScore 1.25
+G_M37697_IG04:
+       add      rsp, 40
        ret      
-						;; size=6 bbWeight=0 PerfScore 0.00
+						;; size=5 bbWeight=0 PerfScore 0.00
 
-; Total bytes of code 115, prolog size 24, PerfScore 25.67, instruction count 31, allocated bytes for code 115 (MethodHash=6db76cbe) for method AsyncMicro.Program:TestTaskToValueTask() (Tier0)
+; Total bytes of code 29, prolog size 4, PerfScore 6.25, instruction count 10, allocated bytes for code 29 (MethodHash=6db76cbe) for method AsyncMicro.Program:TestTaskToValueTask() (FullOpts)
 ; ============================================================
 

Support looking through `return new ValueTask(TaskReturningFunction())`
and `return ValueTaskReturningFunction().AsTask()` in async versions.
These can be transformed into async calls.
Copilot AI review requested due to automatic review settings July 1, 2026 14:05
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 1, 2026
@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.

Comment thread src/coreclr/jit/importercalls.cpp

Copilot AI 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 / async-version” recognition so the JIT can see through common TaskValueTask 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 and AsTask() methods as [Intrinsic] so the JIT can pattern-match them.
  • Teach the JIT importer to recognize async-version tail patterns involving ValueTask adapters 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.

Comment thread src/coreclr/jit/importer.cpp
Copilot AI review requested due to automatic review settings July 1, 2026 14:25

Copilot AI 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 12 out of 13 changed files in this pull request and generated 1 comment.

Comment thread src/coreclr/jit/importer.cpp
Comment thread src/coreclr/jit/importer.cpp Outdated
jakobbotsch added a commit that referenced this pull request Jul 7, 2026
…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.
Copilot AI review requested due to automatic review settings July 7, 2026 13:02

Copilot AI 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 13 out of 14 changed files in this pull request and generated 2 comments.

Comment thread src/coreclr/jit/importer.cpp
Comment thread src/coreclr/jit/importer.cpp Outdated

@AndyAyersMS AndyAyersMS 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 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?

@jakobbotsch

jakobbotsch commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

How robust are all these patterns? Is missing one just a lost optimization?

Yes, it just results in losing the direct runtime async call, meaning we materialize a Task object and await it via AsyncHelpers.TransparentAwait. That has the same behavior as the direct runtime async call.

It would be interesting to do this optimization on JIT IR too by pushing AsyncHelpers.Await/TransparentAwait up through phis/defs (as side effects allow) and then folding that when possible. For example, I came across this example the other day:

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 result;

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 AsyncHelpers.TransparentAwait(new ValueTask(syncResult)) => syncResult.

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.

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?

I like it for the simple things, but in this particular case we need to match stloc+ldloca both of which have multiple encodings, and that ends up being quite messy. I think a bit more flexibility would be needed to make this really convenient to use for complex patterns. I think as it is I would be more inclined to abstract the JIT's pattern matching code for these cases and then use it from interpreter, JIT and NativeAOT.

@jakobbotsch

jakobbotsch commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

Looks like I have some NativeAOT test failures to look at. Fixed in 7ccc165

@BrzVlad BrzVlad 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.

Interpreter LGTM

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.
Copilot AI review requested due to automatic review settings July 13, 2026 14:23

Copilot AI 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 16 out of 17 changed files in this pull request and generated 3 comments.

Comment thread src/coreclr/jit/importer.cpp
Comment thread src/coreclr/interpreter/compiler.cpp
Comment thread src/tests/async/async-versions-task-adapters/async-versions-task-adapters.cs Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 13, 2026 14:39

Copilot AI 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 16 out of 17 changed files in this pull request and generated 1 comment.

@jakobbotsch

Copy link
Copy Markdown
Member Author

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@jakobbotsch

Copy link
Copy Markdown
Member Author

Last nativeaot-outerloop failure is the one being fixed in #130618

@VSadov

VSadov commented Jul 14, 2026

Copy link
Copy Markdown
Member

General question. Is there a reason to use ValueTask in the first place now that we have runtime async ? Doesn't runtime async make the usefullness of ValueTask obsolete by avoiding task allocation for the non-blocking path ?

ValueTask is better than Task in two scenarios (and they are exactly the scenarios when VT does not wrap Task):

  • when VT wraps a completed result.
    Yes Task can do that too and common completed Tasks can even be cached, but VT can wrap any T result with no allocations.
  • wrap a ValueTaskSource in a way that can be versioned and re-armed.
    A Task, once complete, is complete and cannot be rearmed. VT just wraps a tuple {source, version}, so you can get numerous versioned results from the same source, some complete, some not yet - all with no allocations.

The "completed result" scenario is naturally matched quite well in runtime async.

The "async result generator" scenario is done via the same {source, version} abstraction. We just had to make runtime async familiar with existing ValueTaskSource APIs. Not sure how much different this scenario would be if we had rt async from the beginning.

Comment thread src/coreclr/interpreter/compiler.cpp

@VSadov VSadov 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
Member Author

/ba-g Unknown failure is the one fixed in #130618

@jakobbotsch
jakobbotsch merged commit e824476 into dotnet:main Jul 14, 2026
195 of 203 checks passed
@jakobbotsch
jakobbotsch deleted the async-versions-adapt-tasks branch July 14, 2026 14:46
@jakobbotsch

Copy link
Copy Markdown
Member Author

ValueTask is better than Task in two scenarios (and they are exactly the scenarios when VT does not wrap Task):

* when VT wraps a completed result.
  Yes Task can do that too and common completed Tasks can even be cached, but VT can wrap any T result with no allocations.

* wrap a ValueTaskSource in a way that can be versioned and re-armed.
  A Task, once complete, is complete and cannot be rearmed. VT just wraps a tuple `{source, version}`, so you can get numerous versioned results from the same source, some complete, some not yet - all with no allocations.

The "completed result" scenario is naturally matched quite well in runtime async.

Just to be clear: I think @VSadov is talking about the scenarios that led to ValueTask's existence, not the state of things with runtime async. For strictly runtime async calls there is no difference between ValueTask and Task. They will result in the same codegen under-the-hood for both synchronously finishing and asynchronously finishing work. There would only be a difference if you explicitly materialize a Task or ValueTask from a runtime async method (i.e. if you don't await it immediately).

eiriktsarpalis pushed a commit that referenced this pull request Jul 15, 2026
…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.
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview7 milestone Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI runtime-async

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants