Chuanqi Xu b32aa72afc Recommit [C++20] [Coroutines] Mark await_suspend as noinline if the awaiter is not empty
The original patch is incorrect since it marks too many calls to be
noinline. It shows that it is bad to do analysis in the frontend again.
This patch tries to mark the await_suspend function as noinlne only.

---

Close https://github.com/llvm/llvm-project/issues/56301
Close https://github.com/llvm/llvm-project/issues/64151
Close https://github.com/llvm/llvm-project/issues/65018

See the summary and the discussion of
https://reviews.llvm.org/D157070
to get the full context.

As @rjmccall pointed out, the key point of the root cause is that
currently we didn't implement the semantics for '@llvm.coro.save'
well ("after the await-ready returns false, the coroutine is considered
to be suspended ") well.
Since the semantics implies that we (the compiler) shouldn't write
the spills into the coroutine frame in the await_suspend. But now it is
possible due to some combinations of the optimizations so the semantics are
broken. And the inlining is the root optimization of such optimizations.
So in this patch, we tried to add the `noinline` attribute to the
await_suspend function.

This looks slightly problematic since the users are able to call the
await_suspend function standalone. This is limited by the
implementation. On the one hand, we don't want the workaround solution
(See the proposed solution later) to be too complex. On the other hand,
it is rare to call await_suspend standalone. Also it is not semantically
incorrect to do so since the inlining is not part of the C++ standard.

Also as an optimization, we don't add the `noinline` attribute to
the await_suspend function if the awaiter is an empty class. This should be
correct since the programmers can't access the local variables in
await_suspend if the awaiter is empty. I think this is necessary for
the performance since it is pretty common.

The long term solution is:

    call @llvm.coro.await_suspend(ptr %awaiter, ptr %handle,
                                  ptr @awaitSuspendFn)

Then it is much easier to perform the safety analysis in the middle
end. If it is safe to inline the call to awaitSuspend, we can replace it
in the CoroEarly pass. Otherwise we could replace it in the CoroSplit
pass.

Reviewed By: rjmccall

Differential Revision: https://reviews.llvm.org/D157833
2023-08-28 17:07:30 +08:00

86 lines
2.7 KiB
C++

// An end-to-end test to make sure things get processed correctly.
// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -emit-llvm -o - %s -O3 | \
// RUN: FileCheck %s
#include "Inputs/coroutine.h"
struct SomeAwaitable {
// Resume the supplied handle once the awaitable becomes ready,
// returning a handle that should be resumed now for the sake of symmetric transfer.
// If the awaitable is already ready, return an empty handle without doing anything.
//
// Defined in another translation unit. Note that this may contain
// code that synchronizees with another thread.
std::coroutine_handle<> Register(std::coroutine_handle<>);
};
// Defined in another translation unit.
void DidntSuspend();
struct Awaiter {
SomeAwaitable&& awaitable;
bool suspended;
bool await_ready() { return false; }
std::coroutine_handle<> await_suspend(const std::coroutine_handle<> h) {
// Assume we will suspend unless proven otherwise below. We must do
// this *before* calling Register, since we may be destroyed by another
// thread asynchronously as soon as we have registered.
suspended = true;
// Attempt to hand off responsibility for resuming/destroying the coroutine.
const auto to_resume = awaitable.Register(h);
if (!to_resume) {
// The awaitable is already ready. In this case we know that Register didn't
// hand off responsibility for the coroutine. So record the fact that we didn't
// actually suspend, and tell the compiler to resume us inline.
suspended = false;
return h;
}
// Resume whatever Register wants us to resume.
return to_resume;
}
void await_resume() {
// If we didn't suspend, make note of that fact.
if (!suspended) {
DidntSuspend();
}
}
};
struct MyTask{
struct promise_type {
MyTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception();
Awaiter await_transform(SomeAwaitable&& awaitable) {
return Awaiter{static_cast<SomeAwaitable&&>(awaitable)};
}
};
};
MyTask FooBar() {
co_await SomeAwaitable();
}
// CHECK-LABEL: @_Z6FooBarv
// CHECK: %[[to_resume:.*]] = {{.*}}call ptr @_ZN13SomeAwaitable8RegisterESt16coroutine_handleIvE
// CHECK-NEXT: %[[to_bool:.*]] = icmp eq ptr %[[to_resume]], null
// CHECK-NEXT: br i1 %[[to_bool]], label %[[then:.*]], label %[[else:.*]]
// CHECK: [[then]]:
// We only access the coroutine frame conditionally as the sources did.
// CHECK: store i8 0,
// CHECK-NEXT: br label %[[else]]
// CHECK: [[else]]:
// No more access to the coroutine frame until suspended.
// CHECK-NOT: store
// CHECK: }