Refactor non-async Task-returning methods to be amenable to runtime async variant optimization - #129737
Refactor non-async Task-returning methods to be amenable to runtime async variant optimization#129737jakobbotsch wants to merge 4 commits into
Conversation
…timization When compiling async variants of non-async task-returning calls the JIT/interpreter will optimize tail calls to call runtime async variants directly. Make various places amenable to this optimization.
|
Tagging subscribers to this area: @dotnet/area-meta |
There was a problem hiding this comment.
Pull request overview
This PR rewrites a large set of non-async methods that return Task / ValueTask (and related members) from conditional-expression / expression-bodied forms into explicit if/return blocks, with the intent of making the resulting IL patterns more amenable to runtime “async variant” tail-call optimizations.
Changes:
- Replaces many
?:expressions and expression-bodied members with structuredifblocks across libraries (Tasks, IO, Channels, Http, Sockets, Xml, etc.). - Keeps observable behavior intended to be equivalent (same returned tasks/value-tasks and exception/cancellation outcomes), but changes emitted IL shape.
- Applies the same transformation pattern in multiple hot-path types (e.g.,
Task,ValueTask,Stream, SocketsHttpHandler code paths).
Reviewed changes
Copilot reviewed 86 out of 86 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Threading.Tasks.Dataflow/src/Blocks/ActionBlock.cs | Expands Completion getter into explicit branching. |
| src/libraries/System.Threading.Tasks.Dataflow/src/Base/DataflowBlock.cs | Refactors OutputAvailableAsync early-exit checks into if blocks. |
| src/libraries/System.Threading.Channels/src/System/Threading/Channels/UnboundedPriorityChannel.cs | Expands ValueTask-returning conditionals in reader/writer paths. |
| src/libraries/System.Threading.Channels/src/System/Threading/Channels/UnboundedChannel.cs | Same conditional-to-if refactors for unbounded channel paths. |
| src/libraries/System.Threading.Channels/src/System/Threading/Channels/SingleConsumerUnboundedChannel.cs | Same refactor pattern for single-consumer unbounded channel. |
| src/libraries/System.Threading.Channels/src/System/Threading/Channels/RendezvousChannel.cs | Expands done-writing checks into if blocks. |
| src/libraries/System.Threading.Channels/src/System/Threading/Channels/ChannelWriter.cs | Refactors cancellation / TryWrite fast paths into explicit branching. |
| src/libraries/System.Threading.Channels/src/System/Threading/Channels/BoundedChannel.cs | Expands done-writing checks into explicit branching. |
| src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptoStream.cs | Refactors FlushAsync and DisposeAsync dispatch checks into if blocks. |
| src/libraries/System.Private.Xml/src/System/Xml/Core/XmlUtf8RawTextWriterAsync.cs | Expands RawTextAsync conditional return. |
| src/libraries/System.Private.Xml/src/System/Xml/Core/XmlReaderAsync.cs | Expands SkipAsync conditional return. |
| src/libraries/System.Private.Xml/src/System/Xml/Core/XmlEncodedRawTextWriterAsync.cs | Expands RawTextAsync conditional return. |
| src/libraries/System.Private.Xml/src/System/Xml/AsyncHelper.cs | Expands several task-continuation helpers from ternaries into if blocks. |
| src/libraries/System.Private.Xml.Linq/src/System/Xml/Linq/XText.cs | Expands conditional selection of writer async method. |
| src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlBaseWriter.cs | Expands FlushElementAsync / FlushBase64Async conditional returns. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs | Refactors AsTask / Preserve to explicit branching and locals. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TaskExtensions.cs | Refactors Unwrap fast paths to explicit branching. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs | Refactors Delay, WhenAll, WhenAny fast paths into if blocks. |
| src/libraries/System.Private.CoreLib/src/System/Threading/SemaphoreSlim.cs | Refactors waiter selection conditional return into if blocks. |
| src/libraries/System.Private.CoreLib/src/System/Threading/CancellationTokenRegistration.cs | Expands DisposeAsync conditional return into explicit branching. |
| src/libraries/System.Private.CoreLib/src/System/Text/TranscodingStream.cs | Refactors DisposeAsync fast path branching (and introduces a small style nit). |
| src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs | Refactors Task getter and box creation conditional into if blocks. |
| src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilder.cs | Refactors Task getter to explicit branching. |
| src/libraries/System.Private.CoreLib/src/System/IO/TextWriter.cs | Expands several async overloads from conditional expressions into if blocks. |
| src/libraries/System.Private.CoreLib/src/System/IO/TextReader.cs | Expands cancellation fast paths into if blocks. |
| src/libraries/System.Private.CoreLib/src/System/IO/StringReader.cs | Expands cancellation fast paths into if blocks for multiple overrides. |
| src/libraries/System.Private.CoreLib/src/System/IO/StreamWriter.cs | Refactors DisposeAsync type-check dispatch into explicit branching. |
| src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs | Expands cancellation fast paths in NullStreamReader overrides. |
| src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs | Expands cancellation fast paths in several Stream and NullStream overrides. |
| src/libraries/System.Private.CoreLib/src/System/IO/Strategies/BufferedFileStreamStrategy.cs | Expands sync-completed vs async ValueTask conversion logic and cancellation return. |
| src/libraries/System.Private.CoreLib/src/System/IO/File.cs | Expands cancellation/branching in several async file helpers. |
| src/libraries/System.Private.CoreLib/src/System/IO/BufferedStream.cs | Expands one conditional return in async read path. |
| src/libraries/System.Private.CoreLib/src/System/CodeDom/Compiler/IndentedTextWriter.cs | Expands FlushAsync(CancellationToken) conditional dispatch. |
| src/libraries/System.Net.WebSockets/src/System/Net/WebSockets/WebSocketStream.cs | Expands cancellation fast path in FlushAsync. |
| src/libraries/System.Net.WebSockets/src/System/Net/WebSockets/WebSocket.cs | Expands buffer-array fast path selection in SendAsync. |
| src/libraries/System.Net.WebSockets/src/System/Net/WebSockets/ManagedWebSocket.cs | Expands lock-acquired vs fallback send path selection. |
| src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.Tasks.cs | Expands error->ValueTask result mapping into explicit branching. |
| src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs | Expands async vs sync adapter dispatch in authentication path. |
| src/libraries/System.Net.Requests/src/System/Net/RequestBufferingStream.cs | Expands cancellation fast path in FlushAsync. |
| src/libraries/System.Net.Ping/src/System/Net/NetworkInformation/Ping.Unix.cs | Expands raw-socket vs utility dispatch into if block. |
| src/libraries/System.Net.NameResolution/src/System/Net/NameResolutionPal.Windows.cs | Expands Task property selection into explicit branching. |
| src/libraries/System.Net.NameResolution/src/System/Net/Dns.cs | Expands several justAddresses branches into explicit if blocks. |
| src/libraries/System.Net.HttpListener/src/System/Net/Managed/HttpResponseStream.Managed.cs | Expands ignore-errors vs direct write dispatch. |
| src/libraries/System.Net.Http/src/System/Net/Http/StringContent.cs | Expands type-check dispatch for SerializeToStreamAsync. |
| src/libraries/System.Net.Http/src/System/Net/Http/StreamContent.cs | Expands type-check dispatch for SerializeToStreamAsync. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs | Expands handler-exists vs create-and-send selection. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RawConnectionStream.cs | Expands completed vs pending ValueTask handling. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpContentWriteStream.cs | Expands FlushAsync connection-null branch (contains a correctness issue). |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs | Expands a few internal conditional returns in read/fill helpers. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Stream.cs | Expands disposed/canceled fast paths for CopyToAsync. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionWaiter.cs | Expands telemetry vs non-telemetry wait selection. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs | Expands cancellation/connection-null fast paths and trailer fill selection. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.NtAuth.cs | Expands proxy-auth dispatch. |
| src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/AuthenticationHelper.cs | Expands proxy-auth dispatch. |
| src/libraries/System.Net.Http/src/System/Net/Http/MultipartFormDataContent.cs | Expands type-check dispatch for SerializeToStreamAsync. |
| src/libraries/System.Net.Http/src/System/Net/Http/MultipartContent.cs | Expands type-check dispatch for serialize/read-stream helpers. |
| src/libraries/System.Net.Http/src/System/Net/Http/Metrics/MetricsHandler.cs | Expands async vs sync send selection. |
| src/libraries/System.Net.Http/src/System/Net/Http/HttpBaseStream.cs | Expands cancellation “nop” helper into if block. |
| src/libraries/System.Net.Http/src/System/Net/Http/FormUrlEncodedContent.cs | Expands type-check dispatch for SerializeToStreamAsync. |
| src/libraries/System.Net.Http/src/System/Net/Http/EmptyReadStream.cs | Expands cancellation fast path in ReadAsync. |
| src/libraries/System.Net.Http/src/System/Net/Http/EmptyContent.cs | Expands cancellation fast paths for serialize and read-stream creation. |
| src/libraries/System.Net.Http/src/System/Net/Http/DiagnosticsHandler.cs | Expands async vs sync send selection. |
| src/libraries/System.Net.Http/src/System/Net/Http/ByteArrayContent.cs | Expands type-check dispatch for SerializeToStreamAsync. |
| src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpResponseStream.cs | Expands cancellation fast path in FlushAsync. |
| src/libraries/System.Net.Http.WinHttpHandler/src/System/Net/Http/WinHttpRequestStream.cs | Expands cancellation fast path and chunked/manual dispatch. |
| src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/JsonContent.cs | Expands transcoding vs UTF8 serialize dispatch. |
| src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpContentJsonExtensions.cs | Expands encoding/transcoding selection in content stream helper. |
| src/libraries/System.Net.Http.Json/src/System/Net/Http/Json/HttpClientJsonExtensions.cs | Expands headers-read vs buffered response selection. |
| src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Windows.cs | Expands async vs async-over-sync branching for reads/writes. |
| src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.Windows.cs | Expands cancellation and async vs sync wait dispatch. |
| src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.Unix.cs | Expands cancellation fast path in wait helper. |
| src/libraries/System.IO.IsolatedStorage/src/System/IO/IsolatedStorage/IsolatedStorageFileStream.cs | Expands type-check and null-field branching in DisposeAsync. |
| src/libraries/System.IO.Compression/src/System/IO/Compression/Zstandard/ZstandardStream.Compress.cs | Expands cancellation fast path in WriteAsync. |
| src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCustomStreams.cs | Expands empty-buffer and null-stream fast paths. |
| src/libraries/System.IO.Compression/src/System/IO/Compression/ZipArchiveEntry.cs | Expands empty-buffer fast path in write stream. |
| src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs | Expands several fast-path conditionals and type-check dispose dispatch. |
| src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateManaged/DeflateManagedStream.cs | Expands cancellation fast path in FlushAsync. |
| src/libraries/System.IO.Compression.Brotli/src/System/IO/Compression/enc/BrotliStream.Compress.cs | Expands cancellation fast path in WriteAsync(ReadOnlyMemory<byte>) and flush logic. |
| src/libraries/System.Formats.Tar/src/System/Formats/Tar/GnuSparseStream.cs | Expands cancellation fast path in FlushAsync. |
| src/libraries/System.Diagnostics.Process/src/System/Diagnostics/AsyncStreamReader.cs | Expands null-coalescing EOF task into explicit branching. |
| src/libraries/System.Data.Common/src/System/Data/Common/DbDataReader.cs | Expands boolean-returning async wrappers into explicit branching. |
| src/libraries/System.Console/src/System/IO/SyncTextReader.cs | Expands cancellation fast paths into if blocks. |
| src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCache.cs | Replaces small switch expressions with explicit branching for Remove APIs. |
| src/libraries/Common/src/System/Threading/Tasks/TaskCompletionSourceWithCancellation.cs | Expands async vs sync wait selection. |
| src/libraries/Common/src/System/IO/SubReadStream.cs | Expands cancellation fast path in FlushAsync. |
| src/libraries/Common/src/System/IO/ReadOnlyMemoryStream.cs | Expands cancellation fast paths into if blocks. |
| _innerStream = null!; | ||
|
|
||
| return (_leaveOpen) | ||
| ? default /* no work to do */ | ||
| : innerStream.DisposeAsync(); | ||
| if ((_leaveOpen)) | ||
| { | ||
| return default; | ||
| } | ||
| return innerStream.DisposeAsync(); |
| private static Task Delay(uint millisecondsDelay, TimeProvider timeProvider, CancellationToken cancellationToken) | ||
| { | ||
| if (cancellationToken.IsCancellationRequested) | ||
| { | ||
| return FromCanceled(cancellationToken); | ||
| } | ||
| if (millisecondsDelay == 0) | ||
| { | ||
| return CompletedTask; | ||
| } | ||
| if (cancellationToken.CanBeCanceled) | ||
| { | ||
| return new DelayPromiseWithCancellation(millisecondsDelay, timeProvider, cancellationToken); | ||
| } | ||
| return new DelayPromise(millisecondsDelay, timeProvider); | ||
| } |
|
I know this is just a draft so not being critical, but isn't this back to "everyone needs to change their code to make good use of runtime async"? |
We cannot do anything about users using tasks as values. In those cases we need to fall back to materializing the task object that the user is working with. It is what async1 is doing every time. As you can see in this PR many of these cases use ternaries or switch expressions that are task-valued. I expect our guidance is going to be to make async code async and to use For the BCL we are in a situation where it is infeasible for us to blanket convert non-async methods to async. Both because in many cases we want the code to remain performant with async1, but also because there are some behavioral differences around argument validation and async contexts. We have introduced a mechanism that allows optimizing a large fraction of these non-async methods when they happen to be used by a runtime async caller. This PR handles some remaining cases that treat tasks as values but where it is not really necessary. In the future we may introduce a more explicit mechanism to describe what we have done through an optimization in .NET 11, but we are still not yet sure what that is going to look like. #115771 had some suggestions for what it may look like. |
|
While I'm sympathetic, lot's of people write libraries that will need to work efficiently with both, just like the BCL? I'm not sure what the recommendations will be in those circumstances? |
|
We are still not sure what the exact guidance will be, but note that there is nothing wrong with materializing the task object, you will just lose a little bit of runtime async optimization opportunity, falling back to async1-like overhead. There is nothing we can do to avoid creating the task object when the user is using the task object. Currently the JIT and interpreter avoid creating the task object when returned directly from a call if we are in a runtime async context. It is what this PR makes the code amenable to. It is certainly possible for users to put their own code in this shape. Whether or not we will officially document this optimization or not as part of guidance is less clear to me. |
|
Also note that we may end up with more optimizations here -- even optimizing the cases in this PR automatically -- but if we do so it will be a tier1 JIT optimization only. That's probably good enough for most users while for many of these framework methods we want this allocation-avoiding optimization to kick in deterministically. |
|
After looking a little closer at this it looks like Roslyn already optimizes |
When compiling async variants of non-async task-returning calls the JIT/interpreter will optimize tail calls to call runtime async variants directly. Make various places amenable to this optimization.
As a side note: the simple way for users to ensure these optimizations are kicking in is by making their code async and introducing
await. For various reasons that is not straightforwardly possibly in framework sources, so I view this as a next-best solution. It would be possible to recognize some of these C#/IL patterns in the JIT and interpreter, but not easily and not consistently in tier0/tier1.