Optimize await Task.Yield() in runtime async#130170
Conversation
`YieldAwaiter` is already optimized for async1 by virtue of implementing the internal `IStateMachineBoxAwareAwaiter` interface. This allows `await Task.Yield()` to skip allocating internal thread pool objects for async1. Since `await Task.Yield()` is frequently used by micro benchmarks this makes it hard to make apples-to-apples comparisons. To fix that this this PR implements the same optimization for runtime async. Also remove leftover and unused AsyncHelpers.TaskContinuation.cs; it was replaced by RuntimeAsyncTaskContinuation.
|
Tagging subscribers to this area: @steveisok, @dotnet/area-system-reflection |
There was a problem hiding this comment.
Pull request overview
This PR adds a runtime-async fast path for await Task.Yield() that avoids per-await allocations by swapping the boxed YieldAwaiter with a singleton ICriticalNotifyCompletion implementation, aligning runtime-async behavior more closely with the existing async1 optimization. It also removes an unused legacy continuation helper.
Changes:
- Special-case
YieldAwaitable.YieldAwaiterinAsyncHelpers.UnsafeAwaitAwaiter<TAwaiter>to use a singleton notifier instead of boxing the awaiter. - Introduce
RuntimeAsyncYielderto schedule the runtime-async continuation ontoSynchronizationContext/TaskScheduler/ thread pool without allocating extra thread-pool objects. - Remove the unused
AsyncHelpers.TaskContinuation.csand wire the new file into CoreCLR + NativeAOT CoreLib builds.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.cs | Adds a Task.Yield()-specific fast path for runtime async by using RuntimeAsyncYielder.Instance. |
| src/libraries/System.Private.CoreLib/src/System/Delegate.cs | Adds an internal helper to cheaply fetch the target of a single-cast instance delegate (used by the new yielder). |
| src/coreclr/System.Private.CoreLib/System.Private.CoreLib.csproj | Includes the new AsyncHelpers.RuntimeAsyncYielder.cs source file in CoreCLR CoreLib builds. |
| src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.TaskContinuation.cs | Deletes an unused legacy runtime-async task continuation helper. |
| src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.RuntimeAsyncYielder.cs | Implements the singleton yielder used to schedule runtime-async continuations for Task.Yield(). |
| src/coreclr/nativeaot/System.Private.CoreLib/src/System/Delegate.cs | Adds the same internal delegate-target helper for NativeAOT’s Delegate implementation. |
| src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj | Includes the new yielder file for NativeAOT CoreLib builds. |
| continuationTarget.GetType().IsGenericType && | ||
| continuationTarget.GetType().GetGenericTypeDefinition() == typeof(RuntimeAsyncTask<>)); |
There was a problem hiding this comment.
| continuationTarget.GetType().IsGenericType && | |
| continuationTarget.GetType().GetGenericTypeDefinition() == typeof(RuntimeAsyncTask<>)); | |
| continuationTarget.GetType().HasSameMetadataDefinitionAs(typeof(RuntimeAsyncTask<>))); |
There was a problem hiding this comment.
IsGenericType and GetGenericTypeDefinition are jit intrinsics that will be folded to constants in this case. Using HasSameMetadataDefinitionAs here will be much more slower.
https://godbolt.org/z/eMv5Ka9d6
UPDATE: I see its obj.GetType() instead of typeof(T), then this might be less relevant as we don't fold such cases.
YieldAwaiteris already optimized for async1 by virtue of implementing the internalIStateMachineBoxAwareAwaiterinterface. This allowsawait Task.Yield()to skip allocating internal thread pool objects for async1. Sinceawait Task.Yield()is frequently used by micro benchmarks this makes it hard to make apples-to-apples comparisons. To fix that this PR implements the same optimization for runtime async.Also remove leftover and unused AsyncHelpers.TaskContinuation.cs; it was replaced by RuntimeAsyncTaskContinuation.
Micro benchmark with warmup:
Async1 time for comparison:
Not surprisingly they are equal now, this benchmark basically measures thread pool performance more so than anything about async performance.