Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 0a3aaa0

Browse files
committed
Avoid async method delegate allocation
Previously when a task-returning async method would yield for the first time, there would be four allocations: the task, the state machine object boxed to the heap, a context "runner" object, and a delegate that points to the boxed state machine's MoveNext method. A recent PR changed this to avoid the separate box object and the runner, but that still left the task and the delegate. This PR avoids the delegate as well in a common case. For async methods that only ever await Task/Task`1, that aren't using a custom sync context/scheduler, and for which tracing isn't enabled, we know the inner workings of both the builder and the awaiter and can thus bypass the awaiter's pattern APIs; instead of creating the delegate that gets passed to the awaiter and then stored in the wrapped task's continuation slot/list, we can instead just store the boxed state machine directly in the slot/list.
1 parent 522c309 commit 0a3aaa0

File tree

5 files changed

+245
-28
lines changed

5 files changed

+245
-28
lines changed

src/mscorlib/shared/System/Runtime/CompilerServices/ConfiguredValueTaskAwaitable.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public struct ConfiguredValueTaskAwaiter : ICriticalNotifyCompletion
3939
/// <summary>The value being awaited.</summary>
4040
private ValueTask<TResult> _value; // Methods are called on this; avoid making it readonly so as to avoid unnecessary copies
4141
/// <summary>The value to pass to ConfigureAwait.</summary>
42-
private readonly bool _continueOnCapturedContext;
42+
internal readonly bool _continueOnCapturedContext;
4343

4444
/// <summary>Initializes the awaiter.</summary>
4545
/// <param name="value">The value to be awaited.</param>
@@ -66,6 +66,9 @@ public void OnCompleted(Action continuation) =>
6666
/// <summary>Schedules the continuation action for the <see cref="ConfiguredValueTaskAwaitable{TResult}"/>.</summary>
6767
public void UnsafeOnCompleted(Action continuation) =>
6868
_value.AsTask().ConfigureAwait(_continueOnCapturedContext).GetAwaiter().UnsafeOnCompleted(continuation);
69+
70+
/// <summary>Gets the task underlying <see cref="_value"/>.</summary>
71+
internal Task<TResult> AsTask() => _value.AsTask();
6972
}
7073
}
7174
}

src/mscorlib/shared/System/Runtime/CompilerServices/ValueTaskAwaiter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,8 @@ public void OnCompleted(Action continuation) =>
3333
/// <summary>Schedules the continuation action for this ValueTask.</summary>
3434
public void UnsafeOnCompleted(Action continuation) =>
3535
_value.AsTask().ConfigureAwait(continueOnCapturedContext: true).GetAwaiter().UnsafeOnCompleted(continuation);
36+
37+
/// <summary>Gets the task underlying <see cref="_value"/>.</summary>
38+
internal Task<TResult> AsTask() => _value.AsTask();
3639
}
3740
}

src/mscorlib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs

Lines changed: 130 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ public void AwaitOnCompleted<TAwaiter, TStateMachine>(
365365
{
366366
try
367367
{
368-
awaiter.OnCompleted(GetMoveNextDelegate(ref stateMachine));
368+
awaiter.OnCompleted(GetStateMachineBox(ref stateMachine).MoveNextAction);
369369
}
370370
catch (Exception e)
371371
{
@@ -384,10 +384,98 @@ public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
384384
ref TAwaiter awaiter, ref TStateMachine stateMachine)
385385
where TAwaiter : ICriticalNotifyCompletion
386386
where TStateMachine : IAsyncStateMachine
387+
{
388+
IAsyncStateMachineBox box = GetStateMachineBox(ref stateMachine);
389+
390+
// TODO https://github.com/dotnet/coreclr/issues/12877:
391+
// Once the JIT is able to recognize "awaiter is ITaskAwaiter" and "awaiter is IConfiguredTaskAwaiter",
392+
// use those in order to a) consolidate a lot of this code, and b) handle all Task/Task<T> and not just
393+
// the few types special-cased here. For now, handle common {Configured}TaskAwaiter. Having the types
394+
// explicitly listed here allows the JIT to generate the best code for them; otherwise we'll fall through
395+
// to the later workaround.
396+
if (typeof(TAwaiter) == typeof(TaskAwaiter) ||
397+
typeof(TAwaiter) == typeof(TaskAwaiter<string>) ||
398+
typeof(TAwaiter) == typeof(TaskAwaiter<byte[]>) ||
399+
typeof(TAwaiter) == typeof(TaskAwaiter<int>) ||
400+
typeof(TAwaiter) == typeof(TaskAwaiter<long>) ||
401+
typeof(TAwaiter) == typeof(TaskAwaiter<byte>))
402+
{
403+
ref TaskAwaiter ta = ref Unsafe.As<TAwaiter, TaskAwaiter>(ref awaiter); // relies on TaskAwaiter/TaskAwaiter<T> having the same layout
404+
TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, continueOnCapturedContext: true);
405+
}
406+
else if (
407+
typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) ||
408+
typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable<string>.ConfiguredTaskAwaiter) ||
409+
typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable<byte[]>.ConfiguredTaskAwaiter) ||
410+
typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter) ||
411+
typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable<long>.ConfiguredTaskAwaiter) ||
412+
typeof(TAwaiter) == typeof(ConfiguredTaskAwaitable<byte>.ConfiguredTaskAwaiter))
413+
{
414+
ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ta = ref Unsafe.As<TAwaiter, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter>(ref awaiter);
415+
TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, ta.m_continueOnCapturedContext);
416+
}
417+
418+
// Handle common {Configured}ValueTaskAwaiter<T> types. Unfortunately these need to be special-cased
419+
// individually, as we don't have good way to extract the task from a ValueTaskAwaiter<T> when we don't
420+
// know what the T is; we could make ValueTaskAwaiter<T> implement an IValueTaskAwaiter interface, but
421+
// calling a GetTask method on that would end up boxing the awaiter. This hard-coded list here is
422+
// somewhat arbitrary and is based on types currently in use with ValueTask<T> in coreclr/corefx.
423+
else if (typeof(TAwaiter) == typeof(ValueTaskAwaiter<int>))
424+
{
425+
var vta = (ValueTaskAwaiter<int>)(object)awaiter;
426+
TaskAwaiter.UnsafeOnCompletedInternal(vta.AsTask(), box, continueOnCapturedContext: true);
427+
}
428+
else if (typeof(TAwaiter) == typeof(ConfiguredValueTaskAwaitable<int>.ConfiguredValueTaskAwaiter))
429+
{
430+
var vta = (ConfiguredValueTaskAwaitable<int>.ConfiguredValueTaskAwaiter)(object)awaiter;
431+
TaskAwaiter.UnsafeOnCompletedInternal(vta.AsTask(), box, vta._continueOnCapturedContext);
432+
}
433+
else if (typeof(TAwaiter) == typeof(ConfiguredValueTaskAwaitable<System.IO.Stream>.ConfiguredValueTaskAwaiter))
434+
{
435+
var vta = (ConfiguredValueTaskAwaitable<System.IO.Stream>.ConfiguredValueTaskAwaiter)(object)awaiter;
436+
TaskAwaiter.UnsafeOnCompletedInternal(vta.AsTask(), box, vta._continueOnCapturedContext);
437+
}
438+
else if (typeof(TAwaiter) == typeof(ConfiguredValueTaskAwaitable<ArraySegment<byte>>.ConfiguredValueTaskAwaiter))
439+
{
440+
var vta = (ConfiguredValueTaskAwaitable<ArraySegment<byte>>.ConfiguredValueTaskAwaiter)(object)awaiter;
441+
TaskAwaiter.UnsafeOnCompletedInternal(vta.AsTask(), box, vta._continueOnCapturedContext);
442+
}
443+
444+
// To catch all Task/Task<T> awaits, do the currently more expensive interface checks.
445+
// Eventually these and the above Task/Task<T> checks should be replaced by "is" checks,
446+
// once that's recognized and optimized by the JIT. We do these after all of the hardcoded
447+
// checks above so that they don't incur the costs of these checks.
448+
else if (InterfaceIsCheckWorkaround<TAwaiter>.IsITaskAwaiter)
449+
{
450+
ref TaskAwaiter ta = ref Unsafe.As<TAwaiter, TaskAwaiter>(ref awaiter);
451+
TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, continueOnCapturedContext: true);
452+
}
453+
else if (InterfaceIsCheckWorkaround<TAwaiter>.IsIConfiguredTaskAwaiter)
454+
{
455+
ref ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ta = ref Unsafe.As<TAwaiter, ConfiguredTaskAwaitable.ConfiguredTaskAwaiter>(ref awaiter);
456+
TaskAwaiter.UnsafeOnCompletedInternal(ta.m_task, box, ta.m_continueOnCapturedContext);
457+
}
458+
459+
// The awaiter isn't specially known. Fall back to doing a normal await.
460+
else
461+
{
462+
// TODO: https://github.com/dotnet/coreclr/issues/14177
463+
// Move the code back into this method once the JIT is able to
464+
// elide it successfully when one of the previous branches is hit.
465+
AwaitArbitraryAwaiterUnsafeOnCompleted(ref awaiter, box);
466+
}
467+
}
468+
469+
/// <summary>Schedules the specified state machine to be pushed forward when the specified awaiter completes.</summary>
470+
/// <typeparam name="TAwaiter">Specifies the type of the awaiter.</typeparam>
471+
/// <param name="awaiter">The awaiter.</param>
472+
/// <param name="box">The state machine box.</param>
473+
private static void AwaitArbitraryAwaiterUnsafeOnCompleted<TAwaiter>(ref TAwaiter awaiter, IAsyncStateMachineBox box)
474+
where TAwaiter : ICriticalNotifyCompletion
387475
{
388476
try
389477
{
390-
awaiter.UnsafeOnCompleted(GetMoveNextDelegate(ref stateMachine));
478+
awaiter.UnsafeOnCompleted(box.MoveNextAction);
391479
}
392480
catch (Exception e)
393481
{
@@ -399,7 +487,7 @@ public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
399487
/// <typeparam name="TStateMachine">Specifies the type of the async state machine.</typeparam>
400488
/// <param name="stateMachine">The state machine.</param>
401489
/// <returns>The "boxed" state machine.</returns>
402-
private Action GetMoveNextDelegate<TStateMachine>(
490+
private IAsyncStateMachineBox GetStateMachineBox<TStateMachine>(
403491
ref TStateMachine stateMachine)
404492
where TStateMachine : IAsyncStateMachine
405493
{
@@ -416,7 +504,7 @@ private Action GetMoveNextDelegate<TStateMachine>(
416504
{
417505
stronglyTypedBox.Context = currentContext;
418506
}
419-
return stronglyTypedBox.MoveNextAction;
507+
return stronglyTypedBox;
420508
}
421509

422510
// The least common case: we have a weakly-typed boxed. This results if the debugger
@@ -440,7 +528,7 @@ private Action GetMoveNextDelegate<TStateMachine>(
440528
// Update the context. This only happens with a debugger, so no need to spend
441529
// extra IL checking for equality before doing the assignment.
442530
weaklyTypedBox.Context = currentContext;
443-
return weaklyTypedBox.MoveNextAction;
531+
return weaklyTypedBox;
444532
}
445533

446534
// Alert a listening debugger that we can't make forward progress unless it slips threads.
@@ -462,34 +550,33 @@ private Action GetMoveNextDelegate<TStateMachine>(
462550
m_task = box; // important: this must be done before storing stateMachine into box.StateMachine!
463551
box.StateMachine = stateMachine;
464552
box.Context = currentContext;
465-
return box.MoveNextAction;
553+
return box;
466554
}
467555

468556
/// <summary>A strongly-typed box for Task-based async state machines.</summary>
469557
/// <typeparam name="TStateMachine">Specifies the type of the state machine.</typeparam>
470558
/// <typeparam name="TResult">Specifies the type of the Task's result.</typeparam>
471559
private sealed class AsyncStateMachineBox<TStateMachine> :
472-
Task<TResult>, IDebuggingAsyncStateMachineAccessor
560+
Task<TResult>, IAsyncStateMachineBox
473561
where TStateMachine : IAsyncStateMachine
474562
{
475563
/// <summary>Delegate used to invoke on an ExecutionContext when passed an instance of this box type.</summary>
476564
private static readonly ContextCallback s_callback = s => ((AsyncStateMachineBox<TStateMachine>)s).StateMachine.MoveNext();
477565

478566
/// <summary>A delegate to the <see cref="MoveNext"/> method.</summary>
479-
public readonly Action MoveNextAction;
567+
private Action _moveNextAction;
480568
/// <summary>The state machine itself.</summary>
481569
public TStateMachine StateMachine; // mutable struct; do not make this readonly
482570
/// <summary>Captured ExecutionContext with which to invoke <see cref="MoveNextAction"/>; may be null.</summary>
483571
public ExecutionContext Context;
484572

485-
public AsyncStateMachineBox()
486-
{
487-
var mn = new Action(MoveNext);
488-
MoveNextAction = AsyncCausalityTracer.LoggingOn ? AsyncMethodBuilderCore.OutputAsyncCausalityEvents(this, mn) : mn;
489-
}
573+
/// <summary>A delegate to the <see cref="MoveNext"/> method.</summary>
574+
public Action MoveNextAction =>
575+
_moveNextAction ??
576+
(_moveNextAction = AsyncCausalityTracer.LoggingOn ? AsyncMethodBuilderCore.OutputAsyncCausalityEvents(this, new Action(MoveNext)) : new Action(MoveNext));
490577

491-
/// <summary>Call MoveNext on <see cref="StateMachine"/>.</summary>
492-
private void MoveNext()
578+
/// <summary>Calls MoveNext on <see cref="StateMachine"/></summary>
579+
public void MoveNext()
493580
{
494581
if (Context == null)
495582
{
@@ -501,8 +588,19 @@ private void MoveNext()
501588
}
502589
}
503590

591+
/// <summary>
592+
/// Calls MoveNext on <see cref="StateMachine"/>. Implements ITaskCompletionAction.Invoke so
593+
/// that the state machine object may be queued directly as a continuation into a Task's
594+
/// continuation slot/list.
595+
/// </summary>
596+
/// <param name="completedTask">The completing task that caused this method to be invoked, if there was one.</param>
597+
void ITaskCompletionAction.Invoke(Task completedTask) => MoveNext();
598+
599+
/// <summary>Signals to Task's continuation logic that <see cref="Invoke"/> runs arbitrary user code via MoveNext.</summary>
600+
bool ITaskCompletionAction.InvokeMayRunArbitraryCode => true;
601+
504602
/// <summary>Gets the state machine as a boxed object. This should only be used for debugging purposes.</summary>
505-
IAsyncStateMachine IDebuggingAsyncStateMachineAccessor.GetStateMachineObject() => StateMachine; // likely boxes, only use for debugging
603+
IAsyncStateMachine IAsyncStateMachineBox.GetStateMachineObject() => StateMachine; // likely boxes, only use for debugging
506604
}
507605

508606
/// <summary>Gets the <see cref="System.Threading.Tasks.Task{TResult}"/> for this builder.</summary>
@@ -815,12 +913,24 @@ internal static Task<TResult> CreateCacheableTask<TResult>(TResult result) =>
815913
new Task<TResult>(false, result, (TaskCreationOptions)InternalTaskOptions.DoNotDispose, default(CancellationToken));
816914
}
817915

916+
/// <summary>Temporary workaround for https://github.com/dotnet/coreclr/issues/12877.</summary>
917+
internal static class InterfaceIsCheckWorkaround<TAwaiter>
918+
{
919+
internal static readonly bool IsITaskAwaiter = typeof(TAwaiter).GetInterface("ITaskAwaiter") != null;
920+
internal static readonly bool IsIConfiguredTaskAwaiter = typeof(TAwaiter).GetInterface("IConfiguredTaskAwaiter") != null;
921+
}
922+
818923
/// <summary>
819-
/// An interface implemented by <see cref="AsyncStateMachineBox{TStateMachine, TResult}"/> to allow access
820-
/// non-generically to state associated with a builder and state machine.
924+
/// An interface implemented by all <see cref="AsyncStateMachineBox{TStateMachine, TResult}"/> instances, regardless of generics.
821925
/// </summary>
822-
interface IDebuggingAsyncStateMachineAccessor
926+
interface IAsyncStateMachineBox : ITaskCompletionAction
823927
{
928+
/// <summary>
929+
/// Gets an action for moving forward the contained state machine.
930+
/// This will lazily-allocate the delegate as needed.
931+
/// </summary>
932+
Action MoveNextAction { get; }
933+
824934
/// <summary>Gets the state machine as a boxed object. This should only be used for debugging purposes.</summary>
825935
IAsyncStateMachine GetStateMachineObject();
826936
}
@@ -843,7 +953,7 @@ internal static Action TryGetStateMachineForDebugger(Action action) // debugger
843953
{
844954
object target = action.Target;
845955
return
846-
target is IDebuggingAsyncStateMachineAccessor sm ? sm.GetStateMachineObject().MoveNext :
956+
target is IAsyncStateMachineBox sm ? sm.GetStateMachineObject().MoveNext :
847957
target is ContinuationWrapper cw ? TryGetStateMachineForDebugger(cw._continuation) :
848958
action;
849959
}

0 commit comments

Comments
 (0)