Skip to content

Commit ab6aa47

Browse files
authored
Use cached continuation in Await as well when used with ValueTaskSource (#128399)
Same idea as in #127973, but applied when the actual Await is used (not a thunk). With the original repro that I was looking at, merging of #127973 helped to reduce Gen0 GCs from ~ 15/sec to ~ 12/sec. Alocations went down but not completely, because of code like the following piece in the `DoReceive()` loop and possibly in other places. This pattern still churns continuations: https://github.com/dotnet/aspnetcore/blob/c0c2230799a3f876aa673a66bc008bf9b803acac/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs#L173-L182 ```cs var flushTask = Input.FlushAsync(); var paused = !flushTask.IsCompleted; if (paused) { SocketsLog.ConnectionPause(_logger, this); } var result = await flushTask; ``` Because `flushTask` is not a method, the `await` does not go through a thunk which could reuse the continuation. We call the actual `Await` in this case and it suspends/allocates. The change in this PR uses the same approach as in the thunk, but applied to the Await, when awaiting a ValueTaskSource. With this change we get to ~ 2 Gen0/sec in the repro - on par with net10.
1 parent d7173ab commit ab6aa47

3 files changed

Lines changed: 131 additions & 20 deletions

File tree

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.CoreCLR.cs

Lines changed: 99 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,38 @@ private static unsafe void TransparentAwaitValueTask(ValueTask valueTask)
380380

381381
[BypassReadyToRun]
382382
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Async)]
383-
private static unsafe void TransparentAwaitValueTaskOfT<T>(ValueTask<T?> valueTask)
383+
private static unsafe void AwaitValueTaskSource(object source, short token)
384+
{
385+
ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState;
386+
Continuation? sentinelContinuation = state.SentinelContinuation ??= new Continuation();
387+
388+
ValueTaskContinuation? vtsCont = state.CachedValueTaskContinuation;
389+
if (vtsCont != null)
390+
{
391+
state.CachedValueTaskContinuation = null;
392+
}
393+
else
394+
{
395+
vtsCont = new ValueTaskContinuation();
396+
}
397+
398+
Debug.Assert(source != null);
399+
vtsCont.Initialize(source, token);
400+
401+
// We only need to capture flags.
402+
// If needed, VTS will use the scheduling context captured in the "state".
403+
CaptureContinuationContextFlags(ref vtsCont.Flags, state.CurrentThread!);
404+
405+
sentinelContinuation.Next = vtsCont;
406+
state.StackState->ValueTaskContinuation = vtsCont;
407+
408+
state.CaptureContexts();
409+
AsyncSuspend(vtsCont);
410+
}
411+
412+
[BypassReadyToRun]
413+
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Async)]
414+
private static unsafe void TransparentAwaitValueTaskOfT<T>(ValueTask<T> valueTask)
384415
{
385416
ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState;
386417
Continuation? sentinelContinuation = state.SentinelContinuation ??= new Continuation();
@@ -405,6 +436,37 @@ private static unsafe void TransparentAwaitValueTaskOfT<T>(ValueTask<T?> valueTa
405436
AsyncSuspend(vtsCont);
406437
}
407438

439+
[BypassReadyToRun]
440+
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Async)]
441+
private static unsafe void AwaitValueTaskSourceOfT<T>(object source, short token)
442+
{
443+
ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState;
444+
Continuation? sentinelContinuation = state.SentinelContinuation ??= new Continuation();
445+
446+
ValueTaskContinuation? vtsCont = state.CachedValueTaskContinuation;
447+
if (vtsCont != null)
448+
{
449+
state.CachedValueTaskContinuation = null;
450+
}
451+
else
452+
{
453+
vtsCont = new ValueTaskContinuation();
454+
}
455+
456+
Debug.Assert(source != null);
457+
vtsCont.Initialize<T>(source, token);
458+
459+
// We only need to capture flags.
460+
// If needed, VTS will use the scheduling context captured in the "state".
461+
CaptureContinuationContextFlags(ref vtsCont.Flags, state.CurrentThread!);
462+
463+
sentinelContinuation.Next = vtsCont;
464+
state.StackState->ValueTaskContinuation = vtsCont;
465+
466+
state.CaptureContexts();
467+
AsyncSuspend(vtsCont);
468+
}
469+
408470
/// <summary>
409471
/// Used by internal thunks that implement awaiting on Task.
410472
/// </summary>
@@ -493,23 +555,25 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state)
493555

494556
// Head continuation should be the result of async call to AwaitAwaiter or UnsafeAwaitAwaiter.
495557
// These never have special continuation context handling.
558+
// Except for the scenario with ValueTaskContinuation that wraps ValueTaskSource
559+
// which can capture continuation context flags.
496560
const ContinuationFlags continueFlags =
497561
ContinuationFlags.ContinueOnCapturedSynchronizationContext |
498562
ContinuationFlags.ContinueOnThreadPool |
499563
ContinuationFlags.ContinueOnCapturedTaskScheduler;
500564

501-
Debug.Assert((headContinuation.Flags & continueFlags) == 0);
502-
503565
SetContinuationState(headContinuation);
504566

505567
try
506568
{
507569
if (stackState->CriticalNotifier is { } critNotifier)
508570
{
571+
Debug.Assert((headContinuation.Flags & continueFlags) == 0);
509572
critNotifier.UnsafeOnCompleted(GetContinuationAction());
510573
}
511574
else if (stackState->TaskNotifier is { } taskNotifier)
512575
{
576+
Debug.Assert((headContinuation.Flags & continueFlags) == 0);
513577
// Runtime async callable wrapper for task returning
514578
// method. This implements the context transparent
515579
// forwarding and makes these wrappers minimal cost.
@@ -525,6 +589,7 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state)
525589
Debug.Assert(source != null);
526590
if (source is Task t)
527591
{
592+
Debug.Assert((headContinuation.Flags & continueFlags) == 0);
528593
if (!t.TryAddCompletionAction(this))
529594
{
530595
ThreadPool.UnsafeQueueUserWorkItemInternal(this, preferLocal: true);
@@ -541,17 +606,18 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state)
541606
// the continuation chain builds from the innermost frame out and at the time when the
542607
// notifier is created we do not know yet if the caller wants to continue on a context.
543608

544-
// Skip to a nontransparent/user continuation. Such continuaton must exist.
609+
// Skip to a nontransparent/user continuation. Such continuation must exist.
545610
// Since we see a VTS notifier, something was directly or indirectly
546-
// awaiting an async thunk for a ValueTask-returning method.
547-
// That can only happen in nontransparent/user code.
548-
Continuation nextUserContinuation = valueTaskSourceCont.Next!;
549-
while ((nextUserContinuation.Flags & continueFlags) == 0 && nextUserContinuation.Next != null)
611+
// awaiting either an async thunk for a ValueTask-returning method or
612+
// the direct AsyncHelpers.Await(ValueTask/ValueTask<T>) path.
613+
// In either case, that can only happen in nontransparent/user code.
614+
Continuation contWithContinueFlags = valueTaskSourceCont;
615+
while ((contWithContinueFlags.Flags & continueFlags) == 0 && contWithContinueFlags.Next != null)
550616
{
551-
nextUserContinuation = nextUserContinuation.Next;
617+
contWithContinueFlags = contWithContinueFlags.Next;
552618
}
553619

554-
ContinuationFlags continuationFlags = nextUserContinuation.Flags;
620+
ContinuationFlags continuationFlags = contWithContinueFlags.Flags;
555621
const ContinuationFlags continueOnContextFlags =
556622
ContinuationFlags.ContinueOnCapturedSynchronizationContext |
557623
ContinuationFlags.ContinueOnCapturedTaskScheduler;
@@ -564,7 +630,7 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state)
564630
}
565631

566632
// Clear continuation flags, so that continuation runs transparently
567-
nextUserContinuation.Flags &= ~continueFlags;
633+
contWithContinueFlags.Flags &= ~continueFlags;
568634

569635
valueTaskSourceCont.OnCompletedValueTaskSource(
570636
source,
@@ -576,6 +642,7 @@ internal unsafe bool HandleSuspended(ref RuntimeAsyncAwaitState state)
576642
}
577643
else
578644
{
645+
Debug.Assert((headContinuation.Flags & continueFlags) == 0);
579646
Debug.Assert(stackState->Notifier != null);
580647
stackState->Notifier!.OnCompleted(GetContinuationAction());
581648
}
@@ -1117,7 +1184,7 @@ private static void RestoreContextsOnSuspension(bool resumed, ExecutionContext?
11171184
}
11181185
}
11191186

1120-
private static void CaptureContinuationContext(ref object continuationContext, ref ContinuationFlags flags)
1187+
private static void CaptureContinuationContext(ref object? continuationContext, ref ContinuationFlags flags)
11211188
{
11221189
SynchronizationContext? syncCtx = Thread.CurrentThreadAssumedInitialized._synchronizationContext;
11231190
if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))
@@ -1138,6 +1205,26 @@ private static void CaptureContinuationContext(ref object continuationContext, r
11381205
flags |= ContinuationFlags.ContinueOnThreadPool;
11391206
}
11401207

1208+
// Same as above, but only captures flags
1209+
private static void CaptureContinuationContextFlags(ref ContinuationFlags flags, Thread currentThread)
1210+
{
1211+
SynchronizationContext? syncCtx = currentThread._synchronizationContext;
1212+
if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))
1213+
{
1214+
flags |= ContinuationFlags.ContinueOnCapturedSynchronizationContext;
1215+
return;
1216+
}
1217+
1218+
TaskScheduler? sched = TaskScheduler.InternalCurrent;
1219+
if (sched != null && sched != TaskScheduler.Default)
1220+
{
1221+
flags |= ContinuationFlags.ContinueOnCapturedTaskScheduler;
1222+
return;
1223+
}
1224+
1225+
flags |= ContinuationFlags.ContinueOnThreadPool;
1226+
}
1227+
11411228
// Finish suspension in the common case of a custom await or for a ConfigureAwait(false) task await:
11421229
// - Capture current ExecutionContext into the continuation
11431230
// - Restore ExecutionContext and SynchronizationContext to the current Thread object

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncHelpers.ValueTaskContinuation.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ private static class ValueTaskContinuationResume
105105
{
106106
var vtsCont = (ValueTaskContinuation)cont;
107107
vtsCont.Next = null;
108+
109+
const ContinuationFlags continueFlags =
110+
ContinuationFlags.ContinueOnCapturedSynchronizationContext |
111+
ContinuationFlags.ContinueOnThreadPool |
112+
ContinuationFlags.ContinueOnCapturedTaskScheduler;
113+
114+
Debug.Assert((vtsCont.Flags & continueFlags) == 0);
115+
108116
t_runtimeAsyncAwaitState.CachedValueTaskContinuation = vtsCont;
109117

110118
vtsCont.GetResult(ref result);

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

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,21 @@ public static void Await(Task task)
104104
[StackTraceHidden]
105105
public static T Await<T>(ValueTask<T> task)
106106
{
107-
ValueTaskAwaiter<T> awaiter = task.GetAwaiter();
108-
if (!awaiter.IsCompleted)
107+
if (!task.IsCompleted)
109108
{
110-
UnsafeAwaitAwaiter(awaiter);
109+
if (task._obj is Task<T> t)
110+
{
111+
TailAwait();
112+
Await(t);
113+
}
114+
else
115+
{
116+
TailAwait();
117+
AwaitValueTaskSourceOfT<T>(task._obj!, task._token);
118+
}
111119
}
112120

113-
return awaiter.GetResult();
121+
return task.Result;
114122
}
115123

116124
/// <summary>
@@ -123,13 +131,21 @@ public static T Await<T>(ValueTask<T> task)
123131
[StackTraceHidden]
124132
public static void Await(ValueTask task)
125133
{
126-
ValueTaskAwaiter awaiter = task.GetAwaiter();
127-
if (!awaiter.IsCompleted)
134+
if (!task.IsCompleted)
128135
{
129-
UnsafeAwaitAwaiter(awaiter);
136+
if (task._obj is Task t)
137+
{
138+
TailAwait();
139+
Await(t);
140+
}
141+
else
142+
{
143+
TailAwait();
144+
AwaitValueTaskSource(task._obj!, task._token);
145+
}
130146
}
131147

132-
awaiter.GetResult();
148+
task.ThrowIfCompletedUnsuccessfully();
133149
}
134150

135151
/// <summary>

0 commit comments

Comments
 (0)