[Runtime Async] Reuse ValueTaskSourceNotifier instance#126622
[Runtime Async] Reuse ValueTaskSourceNotifier instance#126622VSadov wants to merge 3 commits intodotnet:mainfrom
Conversation
|
Tagging subscribers to this area: @agocke |
There was a problem hiding this comment.
Pull request overview
Reduces per-await allocations in runtime-async by reusing a per-thread ValueTaskSource notifier instead of allocating a new wrapper object each time a ValueTask suspends on an IValueTaskSource.
Changes:
- Replace per-call
ValueTaskSourceNotifierallocations with a[ThreadStatic]cachedValueTaskSourceNotifierinstance. - Introduce a reusable notifier that stores the source, token, and an
OnCompleteddispatcher delegate. - Update CoreCLR runtime-async suspension state to store the concrete
ValueTaskSourceNotifiertype (instead of a removed interface).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs | Adds reusable ValueTaskSourceNotifier and updates AsTaskOrNotifier to return a cached notifier for IValueTaskSource-backed ValueTasks. |
| src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs | Updates runtime-async suspension bookkeeping to reference/cast the new ValueTaskSourceNotifier type. |
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/ValueTask.cs
Outdated
Show resolved
Hide resolved
| GetTaskForValueTaskSource(Unsafe.As<IValueTaskSource>(obj)); | ||
| } | ||
|
|
||
| // A class to not have a static initializer directly on ValueTask |
There was a problem hiding this comment.
Roslyn will take care of the caching and other details for you if you just use lambda.
Are we doing the manual caching to improve the perf characteristics in some way? What is the perf characteristics that the manual caching is improving?
There was a problem hiding this comment.
I think this is where I'd need some advice. I'd like to not pay too much for this cache.
I have two concerns:
-
ValueTask<T>is a very common type and will be instantiated over all kind of T. But the number of ValueTask sources in the process could be very few. VTS is typically used to interact with things that actually can source asynchrony (like sockets, pipes, ManualResetEventSlim ...).
I'd like to be sure that we do not initialize this lambda as soon as we see another instantiation ofValueTask<T>.
I assume placing the lambda instance directly onValueTask<T>might not be a good idea. -
I'd like the
static readonlyto be a JIT-time constant, if possible. I am not sure what Roslyn produces would fit the requirements. Assuming that what I want is possible at all.. - I am not sure if reference-typed readonly statics can actually be JIT-constants in a generic class. The requirements for that were changing over time.
I just wanted to be more explicit of how this is initialized in case it needs to be changed or moved around to fit the criteria.
| { | ||
| internal static readonly Action<object, Action<object?>, object?, short, ValueTaskSourceOnCompletedFlags> s_action = | ||
| static (object obj, Action<object?> continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => | ||
| Unsafe.As<IValueTaskSource>(obj).OnCompleted(continuation, state, token, flags); |
There was a problem hiding this comment.
Given that we depend on Unsafe.As, would it be even better to use a function pointer instead of a delegate?
There was a problem hiding this comment.
I did not realize that we actually could use a function pointer here. Can we get a pointer to an interfce method?
There was a problem hiding this comment.
The annoying part is that all this deal with the lambda is because IValueTaskSource<T> does not derive from IValueTaskSource. It could have inherited OnCompleted, since OnCompleted does not need to know T.
Then the notifier could just be a tuple of {source, token}.
The lambda/indirection is just a way to capture T without making the notifier a generic type.
I wonder if with a pointer we could avoid specializing the notifier to T somehow?
OnCompleted is likely in the same slot in both interfaces (and all instantiations), although not sure if it would be safe to rely on that.
There was a problem hiding this comment.
I have a branch where I started on #119842. It should allow solving this without requiring the extra ValueTaskSourceNotifier or IValueTaskSourceNotifier, via function pointers to instantiations that get the ValueTask from the continuation. You can see the idea here for normal awaiters, but the same approach should work for ValueTask:
There is some cost to create instantiations even here... Ideally with the ability to create lightweight OnCompleted-funclets (a la #121013) we wouldn't even need that.
There was a problem hiding this comment.
Can we get a pointer to an interfce method?
That's complicated. I would get a pointer to a (specialized) static method that calls the interface method you need.
There was a problem hiding this comment.
I have a branch where I started on #119842. It should allow solving this without requiring the extra
ValueTaskSourceNotifierorIValueTaskSourceNotifier, via function pointers to instantiations that get theValueTaskfrom the continuation. You can see the idea here for normal awaiters, but the same approach should work forValueTask:There is some cost to create instantiations even here... Ideally with the ability to create lightweight OnCompleted-funclets (a la #121013) we wouldn't even need that.
I think ValueTaskSource will still require some special casing. By default UnsafeOnCompleted on ValueTask/Awaiter would pass ValueTaskSourceOnCompletedFlags.UseSchedulingContext to the source. In our case that could be incorrect as the context at the time of IsCompleted (the actual scheduling context) could be different from the context at the time we call UnsafeOnCompleted - after popping the stack. We may need to compute this flag from the head continuation, similarly to how we do that right now, and pass the computed flag to the OnCompleted on the source.
That does not prevent the proposed mechanism from working, just that VTS will require some special handling.
The head continuation indeed contains all the information that we store in ValueTaskSourceNotifier, we just do not have a good way to extract it.
Once the suggested mechanism works, ValueTaskSourceNotifier would become dead code and could be deleted.
There was a problem hiding this comment.
Can we get a pointer to an interfce method?
That's complicated. I would get a pointer to a (specialized) static method that calls the interface method you need.
Thanks! That does look much simpler.
This avoids allocation on every time we need to suspend on a
ValueTaskSource.