Skip to content
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

Prototyping T RuntimeHelpers.Await<T>(Task<T>) #2941

Open
wants to merge 2 commits into
base: feature/async2-experiment
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/coreclr/jit/importercalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,8 @@ var_types Compiler::impImportCall(OPCODE opcode,
// calls in JIT generated state machines only.
if (compIsAsync2() &&
((ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_AwaitAwaiterFromRuntimeAsync) ||
(ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync)))
(ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync) ||
(ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_Await)))
{
assert((call != nullptr) && call->OperIs(GT_CALL));
call->AsCall()->gtIsAsyncCall = true;
Expand Down Expand Up @@ -3376,7 +3377,8 @@ GenTree* Compiler::impIntrinsic(CORINFO_CLASS_HANDLE clsHnd,
}

if ((ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_AwaitAwaiterFromRuntimeAsync) ||
(ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync))
(ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync) ||
(ni == NI_System_Runtime_CompilerServices_RuntimeHelpers_Await))
{
// These are marked intrinsics simply to mark the call node as async,
// which the caller will do. Make sure we keep pIntrinsicName assigned
Expand Down Expand Up @@ -10840,6 +10842,11 @@ NamedIntrinsic Compiler::lookupNamedIntrinsic(CORINFO_METHOD_HANDLE method)
result =
NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync;
}
else if (strcmp(methodName, "Await") == 0)
{
result =
NI_System_Runtime_CompilerServices_RuntimeHelpers_Await;
}
else if (strcmp(methodName, "SuspendAsync2") == 0)
{
result = NI_System_Runtime_CompilerServices_RuntimeHelpers_SuspendAsync2;
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/namedintrinsiclist.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ enum NamedIntrinsic : unsigned short
NI_System_Runtime_CompilerServices_RuntimeHelpers_GetMethodTable,
NI_System_Runtime_CompilerServices_RuntimeHelpers_AwaitAwaiterFromRuntimeAsync,
NI_System_Runtime_CompilerServices_RuntimeHelpers_UnsafeAwaitAwaiterFromRuntimeAsync,
NI_System_Runtime_CompilerServices_RuntimeHelpers_Await,
NI_System_Runtime_CompilerServices_RuntimeHelpers_SuspendAsync2,
NI_System_Runtime_CompilerServices_RuntimeHelpers_get_RuntimeAsyncViaJitGeneratedStateMachines,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,12 @@
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.Threading.Lock.#ctor(System.Boolean)</Target>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.Runtime.CompilerServices.RuntimeHelpers.Await``1(System.Threading.Tasks.Task{``0})</Target>
<Left>ref/net10.0/System.Private.CoreLib.dll</Left>
<Right>lib/net10.0/System.Private.CoreLib.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:System.Runtime.CompilerServices.RuntimeHelpers.AwaitAwaiterFromRuntimeAsync``1(``0)</Target>
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/corelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,7 @@ DEFINE_METHOD(RUNTIME_HELPERS, FINALIZE_VALUETASK_RETURNING_THUNK, Finalize
DEFINE_METHOD(RUNTIME_HELPERS, FINALIZE_VALUETASK_RETURNING_THUNK_1, FinalizeValueTaskReturningThunk, GM_Continuation_RetValueTaskOfT)
DEFINE_METHOD(RUNTIME_HELPERS, UNSAFE_AWAIT_AWAITER_FROM_RUNTIME_ASYNC_1, UnsafeAwaitAwaiterFromRuntimeAsync, GM_T_RetVoid)
DEFINE_METHOD(RUNTIME_HELPERS, AWAIT_AWAITER_FROM_RUNTIME_ASYNC_1, AwaitAwaiterFromRuntimeAsync, GM_T_RetVoid)
DEFINE_METHOD(RUNTIME_HELPERS, AWAIT_1, Await, GM_TaskOfT_RetT)

DEFINE_CLASS(SPAN_HELPERS, System, SpanHelpers)
DEFINE_METHOD(SPAN_HELPERS, MEMSET, Fill, SM_RefByte_Byte_UIntPtr_RetVoid)
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/metasig.h
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ DEFINE_METASIG_T(IM(RetValueTask, _, g(VALUETASK)))

DEFINE_METASIG_T(GM(Exception_RetTaskOfT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, C(EXCEPTION), GI(C(TASK_1), 1, M(0))))
DEFINE_METASIG_T(GM(T_RetTaskOfT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, M(0), GI(C(TASK_1), 1, M(0))))
DEFINE_METASIG_T(GM(TaskOfT_RetT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, GI(C(TASK_1), 1, M(0)), M(0)))
DEFINE_METASIG_T(GM(Exception_RetValueTaskOfT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, C(EXCEPTION), GI(g(VALUETASK_1), 1, M(0))))
DEFINE_METASIG_T(GM(T_RetValueTaskOfT, IMAGE_CEE_CS_CALLCONV_DEFAULT, 1, M(0), GI(g(VALUETASK_1), 1, M(0))))

Expand Down
4 changes: 3 additions & 1 deletion src/coreclr/vm/threadsuspend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4915,7 +4915,9 @@ bool IsSpecialCaseAsyncRet(MethodDesc* pMD)
// causing loading to happen? Also, can we just mark them as async2 in SPC,
// or force them to be fully interruptible?
LPCUTF8 name = pMD->GetName();
return strcmp(name, "UnsafeAwaitAwaiterFromRuntimeAsync") == 0 || strcmp(name, "AwaitAwaiterFromRuntimeAsync") == 0;
return strcmp(name, "UnsafeAwaitAwaiterFromRuntimeAsync") == 0 ||
strcmp(name, "AwaitAwaiterFromRuntimeAsync") == 0 ||
strcmp(name, "Await") == 0;
}

static bool GetReturnAddressHijackInfo(EECodeInfo *pCodeInfo, ReturnKind *pReturnKind, bool* hasAsyncRet)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,75 @@ public static void UnsafeAwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter
SuspendAsync2(sentinelContinuation);
return;
}

Copy link
Member Author

@VSadov VSadov Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The interesting part. The rest of code changes are mechanical - to make the helper known as special method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a good reason to not write this function in terms of UnsafeAwaitAwaiterFromRuntimeAsync. This looks more complicated than necessary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a good reason to not write this function in terms of UnsafeAwaitAwaiterFromRuntimeAsync. This looks more complicated than necessary.

Possibly. I started on that path, but was running into asserts (something about conditional BB not ending with conditional jump,...).

I was not sure if that was something that I did wrong or issues with SuspendAsync2 in unusual context.

This way worked though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we will need an explicit SuspendAsync2 once we switch to use UnsafeAwaitAwaiter... with everything hooked up. The JIT should create the state machine itself at that point.

// TODO: should this be called "AwaitFromRuntimeAsync" ? (i.e. same as above, but no "Awaiter")
//
// Marked intrinsic since this needs to be
// recognizes as an async2 call.
[Intrinsic]
[BypassReadyToRun]
[MethodImpl(MethodImplOptions.NoInlining)]
public static unsafe T Await<T>(Task<T> task)
{
// TODO: handle complete tasks more efficiently.
//if (!task.IsCompleted)
//{
ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState;
Continuation? sentinelContinuation = state.SentinelContinuation;
if (sentinelContinuation == null)
state.SentinelContinuation = sentinelContinuation = new Continuation();

Continuation myContinuation = new Continuation();
myContinuation.GCData = new object[] { task };
myContinuation.Resume = &AwaitHelper<T>.Resume;

state.Notifier = task.GetAwaiter();
sentinelContinuation.Next = myContinuation;

// RETURN {default(T), myContinuation}
//
SuspendAsync2(myContinuation);

// unreachable
return task.ResultOnSuccess;
//}
//else
//{
// // RETURN {task.Result, null}
// //
// T result = task.Result;
// ReturnAsync2(Unsafe.AsPointer(ref result));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just guessing how ReturnAsync2 could look.

If generic intrisic is ok, it could also be just ReturnAsync2<T>(result)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need an intrinsic, this can just be a normal return.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a regular return guarantee that the continuation return is null?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would need the VM to tell the JIT this function has runtime-async calling convention. That's probably something we'd want anyway. It might also be the only thing we need to allow using UnsafeAwaitAwaiterFromRuntimeAsync in its implementation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will try. We do not do that for UnsafeAwaitAwaiterFromRuntimeAsync, but that may work just because it is a simpler method.

//
// // unreachable
// return result;
//}
}

internal static class AwaitHelper<T>
{
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resuming the continuation simply pushes the result into the caller continuation. (or throws, if faulted)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if UnsafeAwaitAwaiterFromRuntimeAsync needs to call GetResult() upon resuming as well? (in case it faulted)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also GetAwaiter().GetResult() seems more appropriate than .Result. This is just a rough prototype anyways.

Copy link
Member

@jakobbotsch jakobbotsch Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if UnsafeAwaitAwaiterFromRuntimeAsync needs to call GetResult() upon resuming as well? (in case it faulted)

The caller is responsible for calling awaiter.GetResult() (UnsafeAwaitAwaiterFromRuntimeAsync has no way to bind to the awaiter anyway).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caller is responsible for calling awaiter.GetResult()

Ah, right, in this case we have code in the caller.

public static Continuation? Resume(Continuation continuation)
{
Task<T> task = (Task<T>)continuation.GCData![0]!;
Continuation next = continuation.Next!;

if (IsReferenceOrContainsReferences<T>())
{
next.GCData![0] = task.Result;
}
else
{
int retIndex =
(next.Flags & CorInfoContinuationFlags.CORINFO_CONTINUATION_OSR_IL_OFFSET_IN_DATA) != 0 ?
4 :
0;

// TODO: WriteUnaligned?
Unsafe.As<byte, T>(ref next.Data![retIndex]) = task.Result;
}

return null;
}
}
#endif
}
}
1 change: 1 addition & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13708,6 +13708,7 @@ public static void RunModuleConstructor(System.ModuleHandle module) { }
public delegate void TryCode(object? userData);
public static void UnsafeAwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion { }
public static void AwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : INotifyCompletion { }
public static T Await<T>(System.Threading.Tasks.Task<T> task) { throw null; }
}
public sealed partial class RuntimeWrappedException : System.Exception
{
Expand Down
9 changes: 9 additions & 0 deletions src/tests/async/varying-yields-await.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.IL">
<PropertyGroup>
<Optimize>True</Optimize>
<DefineConstants>AWAIT;$(DefineConstants)</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="varying-yields.cs" />
</ItemGroup>
</Project>
21 changes: 19 additions & 2 deletions src/tests/async/varying-yields.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
//#define ASYNC1_TASK
//#define ASYNC1_VALUETASK

#pragma warning disable 4014, 1998

using System;
using System.Collections.Generic;
using System.Diagnostics;
Expand Down Expand Up @@ -91,9 +93,19 @@ async2 Task<long>
double liveState3 = _yieldProbability;

if (depth == 0)
#if AWAIT
return RuntimeHelpers.Await(Loop());
#else
return await Loop();
#endif

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Await in a benchmark/test.

long result =
#if AWAIT
RuntimeHelpers.Await(Run(depth - 1));
#else
await Run(depth - 1);
#endif

long result = await Run(depth - 1);
Sink = (int)liveState1 + (int)liveState2 + (int)(1 / liveState3) + depth;
return result;
}
Expand All @@ -117,7 +129,12 @@ async2 Task<long>
{
for (int i = 0; i < 20; i++)
{
numIters += await DoYields();
numIters +=
#if AWAIT
RuntimeHelpers.Await(DoYields());
#else
await DoYields();
#endif
}
}

Expand Down