Skip to content

Commit 507abdf

Browse files
Try to reduce cost of Async (#101605)
Contributes to #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.
1 parent 71f8fb6 commit 507abdf

File tree

2 files changed

+51
-41
lines changed

2 files changed

+51
-41
lines changed

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs

+6
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ internal static string GetAsyncStateMachineDescription(IAsyncStateMachine stateM
102102
return sb.ToString();
103103
}
104104

105+
[MethodImpl(MethodImplOptions.NoInlining)]
106+
internal static void LogTraceOperationBegin(Task t, Type stateMachineType)
107+
{
108+
TplEventSource.Log.TraceOperationBegin(t.Id, "Async: " + stateMachineType.Name, 0);
109+
}
110+
105111
internal static Action CreateContinuationWrapper(Action continuation, Action<Action, Task> invokeAction, Task innerTask) =>
106112
new ContinuationWrapper(continuation, invokeAction, innerTask).Invoke;
107113

src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncTaskMethodBuilderT.cs

+45-41
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
157157
{
158158
ExecutionContext? currentContext = ExecutionContext.Capture();
159159

160+
IAsyncStateMachineBox result;
161+
160162
// Check first for the most common case: not the first yield in an async method.
161163
// In this case, the first yield will have already "boxed" the state machine in
162164
// a strongly-typed manner into an AsyncStateMachineBox. It will already contain
@@ -168,9 +170,8 @@ private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
168170
{
169171
stronglyTypedBox.Context = currentContext;
170172
}
171-
return stronglyTypedBox;
173+
result = stronglyTypedBox;
172174
}
173-
174175
// The least common case: we have a weakly-typed boxed. This results if the debugger
175176
// or some other use of reflection accesses a property like ObjectIdForDebugger or a
176177
// method like SetNotificationForWaitCompletion prior to the first await happening. In
@@ -180,7 +181,7 @@ private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
180181
// result in a boxing allocation when storing the TStateMachine if it's a struct, but
181182
// this only happens in active debugging scenarios where such performance impact doesn't
182183
// matter.
183-
if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
184+
else if (taskField is AsyncStateMachineBox<IAsyncStateMachine> weaklyTypedBox)
184185
{
185186
// If this is the first await, we won't yet have a state machine, so store it.
186187
if (weaklyTypedBox.StateMachine == null)
@@ -192,52 +193,55 @@ private static IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
192193
// Update the context. This only happens with a debugger, so no need to spend
193194
// extra IL checking for equality before doing the assignment.
194195
weaklyTypedBox.Context = currentContext;
195-
return weaklyTypedBox;
196+
result = weaklyTypedBox;
196197
}
197-
198-
// Alert a listening debugger that we can't make forward progress unless it slips threads.
199-
// If we don't do this, and a method that uses "await foo;" is invoked through funceval,
200-
// we could end up hooking up a callback to push forward the async method's state machine,
201-
// the debugger would then abort the funceval after it takes too long, and then continuing
202-
// execution could result in another callback being hooked up. At that point we have
203-
// multiple callbacks registered to push the state machine, which could result in bad behavior.
204-
Debugger.NotifyOfCrossThreadDependency();
205-
206-
// At this point, taskField should really be null, in which case we want to create the box.
207-
// However, in a variety of debugger-related (erroneous) situations, it might be non-null,
208-
// e.g. if the Task property is examined in a Watch window, forcing it to be lazily-initialized
209-
// as a Task<TResult> rather than as an AsyncStateMachineBox. The worst that happens in such
210-
// cases is we lose the ability to properly step in the debugger, as the debugger uses that
211-
// object's identity to track this specific builder/state machine. As such, we proceed to
212-
// overwrite whatever's there anyway, even if it's non-null.
198+
else
199+
{
200+
// Alert a listening debugger that we can't make forward progress unless it slips threads.
201+
// If we don't do this, and a method that uses "await foo;" is invoked through funceval,
202+
// we could end up hooking up a callback to push forward the async method's state machine,
203+
// the debugger would then abort the funceval after it takes too long, and then continuing
204+
// execution could result in another callback being hooked up. At that point we have
205+
// multiple callbacks registered to push the state machine, which could result in bad behavior.
206+
Debugger.NotifyOfCrossThreadDependency();
207+
208+
// At this point, taskField should really be null, in which case we want to create the box.
209+
// However, in a variety of debugger-related (erroneous) situations, it might be non-null,
210+
// e.g. if the Task property is examined in a Watch window, forcing it to be lazily-initialized
211+
// as a Task<TResult> rather than as an AsyncStateMachineBox. The worst that happens in such
212+
// cases is we lose the ability to properly step in the debugger, as the debugger uses that
213+
// object's identity to track this specific builder/state machine. As such, we proceed to
214+
// overwrite whatever's there anyway, even if it's non-null.
213215
#if NATIVEAOT
214-
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
215-
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes
216-
// per each async method in NativeAOT binaries without adding much value. Avoid
217-
// generating this extra code until a better solution is implemented.
218-
var box = new AsyncStateMachineBox<TStateMachine>();
216+
// DebugFinalizableAsyncStateMachineBox looks like a small type, but it actually is not because
217+
// it will have a copy of all the slots from its parent. It will add another hundred(s) bytes
218+
// per each async method in NativeAOT binaries without adding much value. Avoid
219+
// generating this extra code until a better solution is implemented.
220+
var box = new AsyncStateMachineBox<TStateMachine>();
219221
#else
220-
AsyncStateMachineBox<TStateMachine> box = AsyncMethodBuilderCore.TrackAsyncMethodCompletion ?
221-
CreateDebugFinalizableAsyncStateMachineBox<TStateMachine>() :
222-
new AsyncStateMachineBox<TStateMachine>();
222+
AsyncStateMachineBox<TStateMachine> box = AsyncMethodBuilderCore.TrackAsyncMethodCompletion ?
223+
CreateDebugFinalizableAsyncStateMachineBox<TStateMachine>() :
224+
new AsyncStateMachineBox<TStateMachine>();
223225
#endif
224-
taskField = box; // important: this must be done before storing stateMachine into box.StateMachine!
225-
box.StateMachine = stateMachine;
226-
box.Context = currentContext;
226+
taskField = box; // important: this must be done before storing stateMachine into box.StateMachine!
227+
box.StateMachine = stateMachine;
228+
box.Context = currentContext;
227229

228-
// Log the creation of the state machine box object / task for this async method.
229-
if (TplEventSource.Log.IsEnabled())
230-
{
231-
TplEventSource.Log.TraceOperationBegin(box.Id, "Async: " + stateMachine.GetType().Name, 0);
232-
}
230+
// Log the creation of the state machine box object / task for this async method.
231+
if (TplEventSource.Log.IsEnabled())
232+
{
233+
AsyncMethodBuilderCore.LogTraceOperationBegin(box, stateMachine.GetType());
234+
}
233235

234-
// And if async debugging is enabled, track the task.
235-
if (Threading.Tasks.Task.s_asyncDebuggingEnabled)
236-
{
237-
Threading.Tasks.Task.AddToActiveTasks(box);
236+
// And if async debugging is enabled, track the task.
237+
if (Threading.Tasks.Task.s_asyncDebuggingEnabled)
238+
{
239+
Threading.Tasks.Task.AddToActiveTasks(box);
240+
}
241+
result = box;
238242
}
239243

240-
return box;
244+
return result;
241245
}
242246

243247
#if !NATIVEAOT

0 commit comments

Comments
 (0)