Add low-cost instrumented version of RuntimeAsyncTask::DispatchContinuations reclaiming lost performance.#126091
Conversation
Add instrumentation probes to RuntimeAsyncTask DispatchContinuations loop. This is an extremely hot loop, in the centre of dispatching async continuations. dotnet#123727 introduced a regression of ~7% when adding additional instrumentation for debugger/tpl into the loop. This commit uses generic value type specialization to setup an interface for the probes that JIT can use to create two versions of codegen for this hot method, most of the probes will be transformed to noop when profiling/debugging is disabled, introduce minimal overhead to critical hot code path. Dispatch loop checks on entry if instrumentation is enabled, if so it will switch to instrumented version of the function. It also checks on each completion of a continuation if instrumentation flags changed and if that is the case it will again switch to the instrumented version. In total it performs small set of instructions to "upgrade" the method on entry, and also a fast check against a static on each loop to support late attach scenarios. This change will make sure the dispatch continuations loop is protected from future performance regressions when more instrumentation gets added.
|
Tagging subscribers to this area: @dotnet/area-system-runtime |
There was a problem hiding this comment.
Pull request overview
This PR refactors runtime-async continuation dispatch to support a low-overhead “uninstrumented” fast path while still enabling Debugger/TPL (and future profiler) instrumentation via a separate, JIT-specialized codegen path, with updated flag plumbing and added tests to validate behavior and cleanup.
Changes:
- Introduces a generic, instrumentation-specialized
DispatchContinuations<TRuntimeAsyncTaskInstrumentation>()path and centralized runtime-async instrumentation flag management. - Refactors
Task’s runtime-async timestamp bookkeeping APIs to better support continuation chains and exception/unwind cleanup. - Adds/expands RuntimeAsync tests for timestamp cleanup, debugger detach behavior, continuation timestamp visibility, and TPL EventSource events; wires TPL EventSource enable/disable to update instrumentation flags.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/libraries/System.Runtime/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs | Adds coverage for runtime-async instrumentation behavior (timestamps, detach, unwind/cancel, and TPL events). |
| src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/TplEventSource.cs | Updates runtime-async instrumentation flags when TPL EventSource commands change enabled keywords. |
| src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs | Refactors runtime-async timestamp dictionaries and adds helpers for chain timestamp propagation and cleanup. |
| src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs | Adds the generic instrumentation abstraction and splits dispatch/finalize into specialized uninstrumented vs instrumented implementations. |
...time/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs
Outdated
Show resolved
Hide resolved
...time/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
Outdated
Show resolved
Hide resolved
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
Show resolved
Hide resolved
…s/System.Runtime.CompilerServices/RuntimeAsyncTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…s/Task.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…s/System.Runtime.CompilerServices/RuntimeAsyncTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
...time/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs
Show resolved
Hide resolved
...time/tests/System.Threading.Tasks.Tests/System.Runtime.CompilerServices/RuntimeAsyncTests.cs
Show resolved
Hide resolved
src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs
Outdated
Show resolved
Hide resolved
…s/Task.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
Show resolved
Hide resolved
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs
Show resolved
Hide resolved
|
Most failures are due to test |
#123727 introduced a regression of ~7% adding additional instrumentation for Debugger/TPL into
RuntimeAsyncTask::DispatchContinuations. Going forward, there will be even more instrumentation needed when implementing async profiling and that would increase the overhead even more, so we need a way to isolate the instrumented vs none instrumented version of this method and regain some of the lost performance.This PR introduces
DispatchContinuations<TRuntimeAsyncTaskInstrumentation>() where TRuntimeAsyncTaskInstrumentation : struct, IRuntimeAsyncTaskInstrumentationusing generic value type specialization using an interface for the instrumentation locations needed in the method creating two different versions of codegen. When instrumentation is disabled, most of the instrumentations are empty with minimal overhead on the execution of the method.I took this path to get away from duplicating the complete
DispatchContinuationsmethod into a regular and instrumented version, reducing the maintenance of ~100 lines of duplicated high performing unsafe code. It can be argued that duplicating theDispatchContinuationsis a small price to pay giving a little clearer implementation. If that is something we all agree on and accept, I'm happy to pursue that path as well, but wanted to start with the zero cost abstraction, no duplication path.To be able to "upgrade" from none instrumented to instrumented version of
DispatchContinuations, there are checks at method entry and after each completed continuation detecting if method should switch to instrumented version. Both checks are small and fast, but the check on method entry needs to do one more compare detecting if debugger flag has been toggled. The post continuation completion check is just a reload of a flag in a static variable and checked if it's not 0.This change will make sure the
RuntimeAsyncTask::DispatchContinuationsis protected from future performance regressions when more instrumentation gets added into the method. Since it shares the majority of the implementation of the loop itself, there is no code duplication. Another approach will be to just duplicate the method, but that would lead to maintaining two copies of the method.Running the same benchmark, #123727 (comment) now shows the following numbers on old vs new implementation:
S.P.C)RuntimeAsyncTask::DispatchContinuations)Measurements done on Windows x64.
S.P.Cis 56 KB smaller with this PR, so the extra instrumented codegen version ofRuntimeAsyncTask::DispatchContinuationsis not included inR2Rimage. The methods triggering the generic instantiation of the instrumented method have been explicitly markedBypassReadyToRun.JIT Size is 347 bytes smaller on the default none instrumented version of
RuntimeAsyncTask::DispatchContinuations, all previous instrumentation has been moved out into the instrumentation interface, completely eliminated in default method.Benchmark shows that this PR recover most of the performance previously lost in #123727.
Code paths triggering the use of instrumented specialization,
DispatchContinuations<EnableRuntimeAsyncTaskInstrumentation>are protected by aIsSupportedflag. On JIT this is alwaystrueand will be folded away, but onNative AOTit will use feature flags (Debugger.IsSupported || EventSource.IsSupported), that arefalseby default, meaning that none of the instrumented versions ofRuntimeAsyncTask::DispatchContinuationswill be included, and together with the JIT size savings toRuntimeAsyncTask::DispatchContinuations, Native AOT apps are expected to be slightly smaller.Most changes in this PR are around setting up the interface used as instrumentation points and extract out existing instrumentation into the instrumentation implementation of the interface. PR also optimize some of the debugger instrumentations previously implemented reducing locking in scenarios where continuation chains are handled.
PR adds a number of new tests validating that the current debugger and TPL instrumentation is still working.
PR also adds preparation for async profiler instrumentation in the RuntimeAsyncTaskInstrumentation type.