Skip to content

Commit

Permalink
Try to reduce cost of Async (dotnet#101605)
Browse files Browse the repository at this point in the history
Contributes to dotnet#79204.

I don't know if this will be considered mergeable, but I wanted to at least try something. For apps that use async a lot (like the Stage2 app we use for Goldilocks), the async infrastructure can cost 10% of the entire executable.

Shuffling a couple things in `GetStateMachineBox` I was able to get 0.23% saving. It's miniscule. In general async is death by a thousand papercuts so I don't see a silver bullet.
  • Loading branch information
MichalStrehovsky authored and michaelgsharp committed May 8, 2024
1 parent ef08e98 commit f5fd4af
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ internal static string GetAsyncStateMachineDescription(IAsyncStateMachine stateM
return sb.ToString();
}

[MethodImpl(MethodImplOptions.NoInlining)]
internal static void LogTraceOperationBegin(Task t, Type stateMachineType)
{
TplEventSource.Log.TraceOperationBegin(t.Id, "Async: " + stateMachineType.Name, 0);
}

internal static Action CreateContinuationWrapper(Action continuation, Action<Action, Task> invokeAction, Task innerTask) =>
new ContinuationWrapper(continuation, invokeAction, innerTask).Invoke;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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
// 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
Expand Down

0 comments on commit f5fd4af

Please sign in to comment.