Optimize async state machines #8683
Unanswered
timcassell
asked this question in
Language Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Motivation
Consider
ValueTask<TResult>
that stores theTResult _result
in the struct, so that synchronous value retrieval can occur without allocating on the heap.ValueTaskAwaiter<TResult>
has aValueTask<TResult>
field, so it can do the same thing.AsyncValueTaskMethodBuilder<TResult>
also stores aTResult _result
field to be able to construct a completedValueTask<TResult>
without allocating. Currently, async state machines store both the entire async method builder, and the entire awaiter, whether it completes synchronously or asynchronously. If theValueTask<TResult>
is actually pending, that's wasted space being stored in the state machine. When the async continuation occurs, the result is retrieved from the backing heap object, not from the result stored in the builder or the awaiter.The proposed optimization improvements come in 2 parts; awaiters and async method builders.
Optimized Awaiters
In order to prevent the result from being stored in the state machine which will never be used, the compiler may recognize a new pattern on awaiters:
When
awaiter.IsCompleted
returnsfalse
, the compiler may call the newawaiter.GetAsyncResultGetter()
and store the result in the state machine instead of the awaiter. When the state machine is continued, it calls the new static functionMyValueTaskAwaiter<T>.GetResult(asyncResultGetter)
, passing in the stored getter.Drawbacks
None
Optimized Async Method Builders
We can prevent the async method builder from being stored in the state machine altogether, and pass it into
MoveNext
instead.New interface:
Notice how the new interface no longer contains a
SetStateMachine
method thatIAsyncStateMachine
has. That's because it's no longer necessary. Modern async method builders store the generic state machine directly without boxing.New builder shape:
The compiler generates 2 state machines for this builder instead of 1. The first state machine is passed to
CreateAndStart
, and it controls whether to callGetCompletedTask
,GetTaskFromException
, orGetPendingTask
+Await(Unsafe)OnCompleted
. The second state machine, which is passed toGetPendingTask
, is almost exactly the same as existing state machines, and it controls the async state, passing awaiters toAwait(Unsafe)OnCompleted
and callingSetException
orSetResult
when it's complete.Drawbacks
AsyncMethodBuilderAttribute
does not allow multiple.One option to get around this is a new attribute to tell the compiler to use the new async method builder type, which older compilers will ignore. Or possibly the existing attribute could be extended to accept 2 types, as long as older compilers can still read it as if it only has 1 type.
A second option is the compiler can infer the new builder type if the old builder type contains a
CreateAndStart
function:An alternative solution would be to just store the new builder type in the state machine to avoid the new interface, but it's less optimal.
Benchmarks
Getting async results for
object
andint
:Getting async results for
Matrix3x2
andMatrix4x4
:You can clearly see that the larger the type we are retrieving, the larger the gains are.
Benchmark code
Additional Notes
I am aware that the runtime is working on adding runtime-async feature. That feature is only supporting
Task
,Task<T>
,ValueTask
, andValueTask<T>
. Custom task-like types will not gain from that feature (at least not for now, don't know if there will be future plans to expand it). Additionally, the new runtime-async feature will interop with existing awaiters, so the awaiter improvements here will still be an improvement for the new feature, even if the async method builders won't be touched.Beta Was this translation helpful? Give feedback.
All reactions