-
Notifications
You must be signed in to change notification settings - Fork 4.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Try to reduce cost of Async #101605
Try to reduce cost of Async #101605
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -157,6 +157,8 @@ private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>( | |
{ | ||
ExecutionContext? currentContext = ExecutionContext.Capture(); | ||
|
||
IAsyncStateMachineBox result; | ||
|
||
// Check first for the most common case: not the first yield in an async method. | ||
// In this case, the first yield will have already "boxed" the state machine in | ||
// a strongly-typed manner into an AsyncStateMachineBox. It will already contain | ||
|
@@ -168,9 +170,8 @@ private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>( | |
{ | ||
stronglyTypedBox.Context = currentContext; | ||
} | ||
return stronglyTypedBox; | ||
result = stronglyTypedBox; | ||
} | ||
|
||
// The least common case: we have a weakly-typed boxed. This results if the debugger | ||
// or some other use of reflection accesses a property like ObjectIdForDebugger or a | ||
// method like SetNotificationForWaitCompletion prior to the first await happening. In | ||
|
@@ -180,7 +181,7 @@ private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>( | |
// result in a boxing allocation when storing the TStateMachine if it's a struct, but | ||
// this only happens in active debugging scenarios where such performance impact doesn't | ||
// matter. | ||
if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox) | ||
else if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox) | ||
{ | ||
// If this is the first await, we won't yet have a state machine, so store it. | ||
if (weaklyTypedBox.StateMachine == null) | ||
|
@@ -192,52 +193,55 @@ private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>( | |
// Update the context. This only happens with a debugger, so no need to spend | ||
// extra IL checking for equality before doing the assignment. | ||
weaklyTypedBox.Context = currentContext; | ||
return weaklyTypedBox; | ||
result = weaklyTypedBox; | ||
} | ||
|
||
// Alert a listening debugger that we can't make forward progress unless it slips threads. | ||
// If we don't do this, and a method that uses "await foo;" is invoked through funceval, | ||
// we could end up hooking up a callback to push forward the async method's state machine, | ||
// the debugger would then abort the funceval after it takes too long, and then continuing | ||
// execution could result in another callback being hooked up. At that point we have | ||
// multiple callbacks registered to push the state machine, which could result in bad behavior. | ||
Debugger.NotifyOfCrossThreadDependency(); | ||
|
||
// At this point, taskField should really be null, in which case we want to create the box. | ||
// However, in a variety of debugger-related (erroneous) situations, it might be non-null, | ||
// e.g. if the Task property is examined in a Watch window, forcing it to be lazily-initialized | ||
// as a Task<TResult> rather than as an AsyncStateMachineBox. The worst that happens in such | ||
// cases is we lose the ability to properly step in the debugger, as the debugger uses that | ||
// object's identity to track this specific builder/state machine. As such, we proceed to | ||
// overwrite whatever's there anyway, even if it's non-null. | ||
else | ||
{ | ||
// Alert a listening debugger that we can't make forward progress unless it slips threads. | ||
// If we don't do this, and a method that uses "await foo;" is invoked through funceval, | ||
// we could end up hooking up a callback to push forward the async method's state machine, | ||
// the debugger would then abort the funceval after it takes too long, and then continuing | ||
// execution could result in another callback being hooked up. At that point we have | ||
// multiple callbacks registered to push the state machine, which could result in bad behavior. | ||
Debugger.NotifyOfCrossThreadDependency(); | ||
|
||
// At this point, taskField should really be null, in which case we want to create the box. | ||
// However, in a variety of debugger-related (erroneous) situations, it might be non-null, | ||
// e.g. if the Task property is examined in a Watch window, forcing it to be lazily-initialized | ||
// as a Task<TResult> rather than as an AsyncStateMachineBox. The worst that happens in such | ||
// cases is we lose the ability to properly step in the debugger, as the debugger uses that | ||
// object's identity to track this specific builder/state machine. As such, we proceed to | ||
// overwrite whatever's there anyway, even if it's non-null. | ||
#if NATIVEAOT | ||
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because | ||
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes | ||
// per each async method in NativeAOT binaries without adding much value. Avoid | ||
// generating this extra code until a better solution is implemented. | ||
var box = new AsyncStateMachineBox<TStateMachine>(); | ||
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because | ||
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be out of scope for this PR, but how about using an unspecialized There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't know much about this - if it's only used by managed debuggers, it would not be useful for native AOT anyway. It's ifdeffed out so I'm not concerned about it. |
||
// per each async method in NativeAOT binaries without adding much value. Avoid | ||
// generating this extra code until a better solution is implemented. | ||
var box = new AsyncStateMachineBox<TStateMachine>(); | ||
#else | ||
AsyncStateMachineBox<TStateMachine> box = AsyncMethodBuilderCore.TrackAsyncMethodCompletion ? | ||
CreateDebugFinalizableAsyncStateMachineBox<TStateMachine>() : | ||
new AsyncStateMachineBox<TStateMachine>(); | ||
AsyncStateMachineBox<TStateMachine> box = AsyncMethodBuilderCore.TrackAsyncMethodCompletion ? | ||
CreateDebugFinalizableAsyncStateMachineBox<TStateMachine>() : | ||
new AsyncStateMachineBox<TStateMachine>(); | ||
#endif | ||
taskField = box; // important: this must be done before storing stateMachine into box.StateMachine! | ||
box.StateMachine = stateMachine; | ||
box.Context = currentContext; | ||
taskField = box; // important: this must be done before storing stateMachine into box.StateMachine! | ||
box.StateMachine = stateMachine; | ||
box.Context = currentContext; | ||
|
||
// Log the creation of the state machine box object / task for this async method. | ||
if (TplEventSource.Log.IsEnabled()) | ||
{ | ||
TplEventSource.Log.TraceOperationBegin(box.Id, "Async: " + stateMachine.GetType().Name, 0); | ||
} | ||
// Log the creation of the state machine box object / task for this async method. | ||
if (TplEventSource.Log.IsEnabled()) | ||
{ | ||
AsyncMethodBuilderCore.LogTraceOperationBegin(box, stateMachine.GetType()); | ||
} | ||
|
||
// And if async debugging is enabled, track the task. | ||
if (Threading.Tasks.Task.s_asyncDebuggingEnabled) | ||
{ | ||
Threading.Tasks.Task.AddToActiveTasks(box); | ||
// And if async debugging is enabled, track the task. | ||
if (Threading.Tasks.Task.s_asyncDebuggingEnabled) | ||
{ | ||
Threading.Tasks.Task.AddToActiveTasks(box); | ||
} | ||
result = box; | ||
} | ||
|
||
return box; | ||
return result; | ||
} | ||
|
||
#if !NATIVEAOT | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're trying to squeeze water from a stone, this whole block should be extremely cold (only used in debugging scenarios), so if you can come up with a good way to outline it all to a separate helper, that'd be reasonable. Not sure if that'd help much, though, given that stateMachine is generic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it would likely make things worse because it's still the same code, but with extra unwinding/GC tracking data structures because we have a new method.