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

Conversation

VSadov
Copy link
Member

@VSadov VSadov commented Jan 17, 2025

Not for merging, just taking a look how RuntimeHelpers.Await could be implemented.

The code actually runs. The implementation is suboptimal because we always suspend.
We do not have to, but it would require implementing ReturnAsync2 intrinsic, the value-returning counterpart of SuspendAsync2.
It seems doable, but needs a bit more JIT expertise.

Even with always supending the perf is not too bad - roughly 30% worse on the varying-yield benchmark.
Although this benchmark suspends a lot, thus is not impacted as much by unconditional suspend.

For the real implementation we would definitely need ReturnAsync2

@VSadov VSadov added the * NO MERGE * The PR is not ready for merge yet (see discussion for detailed reasons) label Jan 17, 2025
@@ -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.

// // 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.

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.

}

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.

@VSadov VSadov requested a review from jakobbotsch January 17, 2025 01:29
@VSadov
Copy link
Member Author

VSadov commented Jan 18, 2025

switched to state machine version of Await.

It was a bit more involved than I expected. The main issue is that for the "magic" methods you can't figure their "async-ness" by only looking at the signature, which is kind of ByDesign. I think we may have to keep the special methods as Intrinsics.

Explicit async is also a bit viral - once you declare something as async machine you'd need to call with async call convention and vice versa, lest you get into obscure crashes from dereferencing continuations that are not actually passed by the caller or calee and depend on what junk there was in the corresponding register.
(thus UnsafeAwaitAwaiterFromRuntimeAsync had to be moved onto new plan as well)

I like the new approach more, though. It seems more generally useful/scalable in case we need more "magic" helpers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
* NO MERGE * The PR is not ready for merge yet (see discussion for detailed reasons)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants