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 all commits
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
17 changes: 12 additions & 5 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 @@ -913,7 +914,7 @@ var_types Compiler::impImportCall(OPCODE opcode,
impPopCallArgs(sig, call->AsCall());

// Extra args
if ((instParam != nullptr) || sig->isAsyncCall() || (varArgsCookie != nullptr))
if ((instParam != nullptr) || call->AsCall()->IsAsync2() || (varArgsCookie != nullptr))
{
if (Target::g_tgtArgOrder == Target::ARG_ORDER_R2L)
{
Expand All @@ -923,7 +924,7 @@ var_types Compiler::impImportCall(OPCODE opcode,
.WellKnown(WellKnownArg::VarArgsCookie));
}

if (sig->isAsyncCall())
if (call->AsCall()->IsAsync2())
{
call->AsCall()->gtArgs.PushFront(this, NewCallArg::Primitive(gtNewNull(), TYP_REF)
.WellKnown(WellKnownArg::AsyncContinuation));
Expand All @@ -943,7 +944,7 @@ var_types Compiler::impImportCall(OPCODE opcode,
NewCallArg::Primitive(instParam).WellKnown(WellKnownArg::InstParam));
}

if (sig->isAsyncCall())
if (call->AsCall()->IsAsync2())
{
call->AsCall()->gtArgs.PushBack(this, NewCallArg::Primitive(gtNewNull(), TYP_REF)
.WellKnown(WellKnownArg::AsyncContinuation));
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
18 changes: 13 additions & 5 deletions src/coreclr/vm/method.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,12 @@ enum class AsyncMethodKind
AsyncImplHelper,

// Synthetic Async2 method that forwards to the NotAsync Task-returning method
AsyncThunkHelper
AsyncThunkHelper,

// Actual IL method that is explicitly declared as Async2 and thus compiled into a state machine.
// Such methods do not get Async thunks and can only be called from another Async2 method using Async2 call convention.
// This is used in a few infrastructure methods like `Await`
AsyncImplExplicit,
};

struct AsyncMethodData
Expand Down Expand Up @@ -1829,10 +1834,13 @@ class MethodDesc
// CONSIDER: We probably need a better name for the concept, but it is hard to beat shortness of "async2"
inline bool IsAsync2Method() const
{
// Right now the only Async2 methods that exist are synthetic helpers.
// It is possible to declare an Async2 method directly in IL/Metadata,
// but we do not have a scenario for that.
return IsAsyncHelperMethod();
LIMITED_METHOD_DAC_CONTRACT;
if (!HasAsyncMethodData())
return false;
auto asyncKind = GetAddrOfAsyncMethodData()->kind;
return asyncKind == AsyncMethodKind::AsyncThunkHelper ||
asyncKind == AsyncMethodKind::AsyncImplHelper ||
asyncKind == AsyncMethodKind::AsyncImplExplicit;
}

inline bool IsStructMethodOperatingOnCopy()
Expand Down
13 changes: 12 additions & 1 deletion src/coreclr/vm/methodtablebuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3605,7 +3605,18 @@ MethodTableBuilder::EnumerateClassMethods()
else
{
_ASSERTE(IsAsyncTaskMethodNormal(asyncMethodType));
pNewMethod->SetAsyncMethodKind(AsyncMethodKind::NotAsync);

if (IsMiAsync(dwImplFlags))
{
// TODO: VS must validate that only a few special methods can do this.
// the possibility is useful, but should not become a general
// feature by accident.
pNewMethod->SetAsyncMethodKind(AsyncMethodKind::AsyncImplExplicit);
}
else
{
pNewMethod->SetAsyncMethodKind(AsyncMethodKind::NotAsync);
}
}

pDeclaredMethod = pNewMethod;
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 @@ -178,8 +178,8 @@ public static unsafe ReadOnlySpan<T> CreateSpan<T>(RuntimeFieldHandle fldHandle)

#if !NATIVEAOT
[Intrinsic]
[MethodImpl(MethodImplOptions.NoInlining)]
[BypassReadyToRun]
[MethodImpl(MethodImplOptions.NoInlining | (MethodImplOptions)0x0400)] // NoInlining | Async
public static void AwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : INotifyCompletion
{
ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState;
Expand All @@ -196,7 +196,7 @@ public static void AwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) wher
// recognizes as an async2 call.
[Intrinsic]
[BypassReadyToRun]
[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(MethodImplOptions.NoInlining | (MethodImplOptions)0x0400)] // NoInlining | Async
public static void UnsafeAwaitAwaiterFromRuntimeAsync<TAwaiter>(TAwaiter awaiter) where TAwaiter : ICriticalNotifyCompletion
{
ref RuntimeAsyncAwaitState state = ref t_runtimeAsyncAwaitState;
Expand All @@ -208,6 +208,25 @@ 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 | (MethodImplOptions)0x0400)] // NoInlining | Async
public static T Await<T>(Task<T> task)
{
TaskAwaiter<T> awaiter = task.GetAwaiter();
if (!awaiter.IsCompleted)
{
UnsafeAwaitAwaiterFromRuntimeAsync(awaiter);
}

return awaiter.GetResult();
}

#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