Skip to content

Commit

Permalink
[C++20] [Coroutines] Mark await_suspend as noinline if the awaiter is…
Browse files Browse the repository at this point in the history
… not empty

Close #56301
Close #64151

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

Also as an optimization, we don't add the `noinline` attribute to the
await_suspend call 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.

Another potential optimization 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
  • Loading branch information
ChuanqiXu9 committed Aug 22, 2023
1 parent 7c4e8c6 commit c467245
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 0 deletions.
4 changes: 4 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ Bug Fixes in This Version
``abs`` builtins.
(`#45129 <https://github.com/llvm/llvm-project/issues/45129>`_,
`#45794 <https://github.com/llvm/llvm-project/issues/45794>`_)
- Fixed an issue where accesses to the local variables of a coroutine during
``await_suspend`` could be misoptimized, including accesses to the awaiter
object itself.
(`#56301 <https://github.com/llvm/llvm-project/issues/56301>`_)

Bug Fixes to Compiler Builtins
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
24 changes: 24 additions & 0 deletions clang/lib/CodeGen/CGCall.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5507,6 +5507,30 @@ RValue CodeGenFunction::EmitCall(const CGFunctionInfo &CallInfo,
Attrs.addFnAttribute(getLLVMContext(), llvm::Attribute::AlwaysInline);
}

// The await_suspend call performed by co_await is essentially asynchronous
// to the execution of the coroutine. Inlining it normally into an unsplit
// coroutine can cause miscompilation because the coroutine CFG misrepresents
// the true control flow of the program: things that happen in the
// await_suspend are not guaranteed to happen prior to the resumption of the
// coroutine, and things that happen after the resumption of the coroutine
// (including its exit and the potential deallocation of the coroutine frame)
// are not guaranteed to happen only after the end of await_suspend.
//
// The short-term solution to this problem is to mark the call as uninlinable.
// But we don't want to do this if the call is known to be trivial, which is
// very common.
//
// The long-term solution may introduce patterns like:
//
// 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.
if (inSuspendBlock() && mayCoroHandleEscape())
Attrs = Attrs.addFnAttribute(getLLVMContext(), llvm::Attribute::NoInline);

// Disable inlining inside SEH __try blocks.
if (isSEHTryScope()) {
Attrs = Attrs.addFnAttribute(getLLVMContext(), llvm::Attribute::NoInline);
Expand Down
33 changes: 33 additions & 0 deletions clang/lib/CodeGen/CGCoroutine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,36 @@ static bool memberCallExpressionCanThrow(const Expr *E) {
return true;
}

/// Return true when the coroutine handle may escape from the await-suspend
/// (`awaiter.await_suspend(std::coroutine_handle)` expression).
/// Return false only when the coroutine wouldn't escape in the await-suspend
/// for sure.
///
/// While it is always safe to return true, return falses can bring better
/// performances.
///
/// See https://github.com/llvm/llvm-project/issues/56301 and
/// https://reviews.llvm.org/D157070 for the example and the full discussion.
///
/// FIXME: It will be much better to perform such analysis in the middle end.
/// See the comments in `CodeGenFunction::EmitCall` for example.
static bool MayCoroHandleEscape(CoroutineSuspendExpr const &S) {
CXXRecordDecl *Awaiter =
S.getCommonExpr()->getType().getNonReferenceType()->getAsCXXRecordDecl();

// Return true conservatively if the awaiter type is not a record type.
if (!Awaiter)
return true;

// In case the awaiter type is empty, the suspend wouldn't leak the coroutine
// handle.
//
// TODO: We can improve this by looking into the implementation of
// await-suspend and see if the coroutine handle is passed to foreign
// functions.
return !Awaiter->field_empty();
}

// Emit suspend expression which roughly looks like:
//
// auto && x = CommonExpr();
Expand Down Expand Up @@ -199,8 +229,11 @@ static LValueOrRValue emitSuspendExpression(CodeGenFunction &CGF, CGCoroData &Co
auto *SaveCall = Builder.CreateCall(CoroSave, {NullPtr});

CGF.CurCoro.InSuspendBlock = true;
CGF.CurCoro.MayCoroHandleEscape = MayCoroHandleEscape(S);
auto *SuspendRet = CGF.EmitScalarExpr(S.getSuspendExpr());
CGF.CurCoro.InSuspendBlock = false;
CGF.CurCoro.MayCoroHandleEscape = false;

if (SuspendRet != nullptr && SuspendRet->getType()->isIntegerTy(1)) {
// Veto suspension if requested by bool returning await_suspend.
BasicBlock *RealSuspendBlock =
Expand Down
5 changes: 5 additions & 0 deletions clang/lib/CodeGen/CodeGenFunction.h
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ class CodeGenFunction : public CodeGenTypeCache {
struct CGCoroInfo {
std::unique_ptr<CGCoroData> Data;
bool InSuspendBlock = false;
bool MayCoroHandleEscape = false;
CGCoroInfo();
~CGCoroInfo();
};
Expand All @@ -347,6 +348,10 @@ class CodeGenFunction : public CodeGenTypeCache {
return isCoroutine() && CurCoro.InSuspendBlock;
}

bool mayCoroHandleEscape() const {
return isCoroutine() && CurCoro.MayCoroHandleEscape;
}

/// CurGD - The GlobalDecl for the current function being compiled.
GlobalDecl CurGD;

Expand Down
207 changes: 207 additions & 0 deletions clang/test/CodeGenCoroutines/coro-awaiter-noinline-suspend.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Tests that we can mark await-suspend as noinline correctly.
//
// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -emit-llvm -o - %s \
// RUN: -disable-llvm-passes | FileCheck %s

#include "Inputs/coroutine.h"

struct Task {
struct promise_type {
struct FinalAwaiter {
bool await_ready() const noexcept { return false; }
template <typename PromiseType>
std::coroutine_handle<> await_suspend(std::coroutine_handle<PromiseType> h) noexcept {
return h.promise().continuation;
}
void await_resume() noexcept {}
};

Task get_return_object() noexcept {
return std::coroutine_handle<promise_type>::from_promise(*this);
}

std::suspend_always initial_suspend() noexcept { return {}; }
FinalAwaiter final_suspend() noexcept { return {}; }
void unhandled_exception() noexcept {}
void return_void() noexcept {}

std::coroutine_handle<> continuation;
};

Task(std::coroutine_handle<promise_type> handle);
~Task();

private:
std::coroutine_handle<promise_type> handle;
};

struct StatefulAwaiter {
int value;
bool await_ready() const noexcept { return false; }
template <typename PromiseType>
void await_suspend(std::coroutine_handle<PromiseType> h) noexcept {}
void await_resume() noexcept {}
};

typedef std::suspend_always NoStateAwaiter;
using AnotherStatefulAwaiter = StatefulAwaiter;

template <class T>
struct TemplatedAwaiter {
T value;
bool await_ready() const noexcept { return false; }
template <typename PromiseType>
void await_suspend(std::coroutine_handle<PromiseType> h) noexcept {}
void await_resume() noexcept {}
};


class Awaitable {};
StatefulAwaiter operator co_await(Awaitable) {
return StatefulAwaiter{};
}

StatefulAwaiter GlobalAwaiter;
class Awaitable2 {};
StatefulAwaiter& operator co_await(Awaitable2) {
return GlobalAwaiter;
}

Task testing() {
co_await std::suspend_always{};
co_await StatefulAwaiter{};
co_await AnotherStatefulAwaiter{};

// Test lvalue case.
StatefulAwaiter awaiter;
co_await awaiter;

// The explicit call to await_suspend is not considered suspended.
awaiter.await_suspend(std::coroutine_handle<void>::from_address(nullptr));

co_await TemplatedAwaiter<int>{};
TemplatedAwaiter<int> TemplatedAwaiterInstace;
co_await TemplatedAwaiterInstace;

co_await Awaitable{};
co_await Awaitable2{};
}

// CHECK-LABEL: @_Z7testingv

// Check `co_await __promise__.initial_suspend();` Since it returns std::suspend_always,
// which is an empty class, we shouldn't generate optimization blocker for it.
// CHECK: call token @llvm.coro.save
// CHECK: call void @_ZNSt14suspend_always13await_suspendESt16coroutine_handleIvE{{.*}}#[[NORMAL_ATTR:[0-9]+]]

// Check the `co_await std::suspend_always{};` expression. We shouldn't emit the optimization
// blocker for it since it is an empty class.
// CHECK: call token @llvm.coro.save
// CHECK: call void @_ZNSt14suspend_always13await_suspendESt16coroutine_handleIvE{{.*}}#[[NORMAL_ATTR]]

// Check `co_await StatefulAwaiter{};`. We need to emit the optimization blocker since
// the awaiter is not empty.
// CHECK: call token @llvm.coro.save
// CHECK: call void @_ZN15StatefulAwaiter13await_suspendIN4Task12promise_typeEEEvSt16coroutine_handleIT_E{{.*}}#[[NOINLINE_ATTR:[0-9]+]]

// Check `co_await AnotherStatefulAwaiter{};` to make sure that we can handle TypedefTypes.
// CHECK: call token @llvm.coro.save
// CHECK: call void @_ZN15StatefulAwaiter13await_suspendIN4Task12promise_typeEEEvSt16coroutine_handleIT_E{{.*}}#[[NOINLINE_ATTR]]

// Check `co_await awaiter;` to make sure we can handle lvalue cases.
// CHECK: call token @llvm.coro.save
// CHECK: call void @_ZN15StatefulAwaiter13await_suspendIN4Task12promise_typeEEEvSt16coroutine_handleIT_E{{.*}}#[[NOINLINE_ATTR]]

// Check `awaiter.await_suspend(...)` to make sure the explicit call the await_suspend won't be marked as noinline
// CHECK: call void @_ZN15StatefulAwaiter13await_suspendIvEEvSt16coroutine_handleIT_E{{.*}}#[[NORMAL_ATTR]]

// Check `co_await TemplatedAwaiter<int>{};` to make sure we can handle specialized template
// type.
// CHECK: call token @llvm.coro.save
// CHECK: call void @_ZN16TemplatedAwaiterIiE13await_suspendIN4Task12promise_typeEEEvSt16coroutine_handleIT_E{{.*}}#[[NOINLINE_ATTR]]

// Check `co_await TemplatedAwaiterInstace;` to make sure we can handle the lvalue from
// specialized template type.
// CHECK: call token @llvm.coro.save
// CHECK: call void @_ZN16TemplatedAwaiterIiE13await_suspendIN4Task12promise_typeEEEvSt16coroutine_handleIT_E{{.*}}#[[NOINLINE_ATTR]]

// Check `co_await Awaitable{};` to make sure we can handle awaiter returned by
// `operator co_await`;
// CHECK: call token @llvm.coro.save
// CHECK: call void @_ZN15StatefulAwaiter13await_suspendIN4Task12promise_typeEEEvSt16coroutine_handleIT_E{{.*}}#[[NOINLINE_ATTR]]

// Check `co_await Awaitable2{};` to make sure we can handle awaiter returned by
// `operator co_await` which returns a reference;
// CHECK: call token @llvm.coro.save
// CHECK: call void @_ZN15StatefulAwaiter13await_suspendIN4Task12promise_typeEEEvSt16coroutine_handleIT_E{{.*}}#[[NOINLINE_ATTR]]

// Check `co_await __promise__.final_suspend();`. We don't emit an blocker here since it is
// empty.
// CHECK: call token @llvm.coro.save
// CHECK: call ptr @_ZN4Task12promise_type12FinalAwaiter13await_suspendIS0_EESt16coroutine_handleIvES3_IT_E{{.*}}#[[NORMAL_ATTR]]

struct AwaitTransformTask {
struct promise_type {
struct FinalAwaiter {
bool await_ready() const noexcept { return false; }
template <typename PromiseType>
std::coroutine_handle<> await_suspend(std::coroutine_handle<PromiseType> h) noexcept {
return h.promise().continuation;
}
void await_resume() noexcept {}
};

AwaitTransformTask get_return_object() noexcept {
return std::coroutine_handle<promise_type>::from_promise(*this);
}

std::suspend_always initial_suspend() noexcept { return {}; }
FinalAwaiter final_suspend() noexcept { return {}; }
void unhandled_exception() noexcept {}
void return_void() noexcept {}

template <typename Awaitable>
auto await_transform(Awaitable &&awaitable) {
return awaitable;
}

std::coroutine_handle<> continuation;
};

AwaitTransformTask(std::coroutine_handle<promise_type> handle);
~AwaitTransformTask();

private:
std::coroutine_handle<promise_type> handle;
};

struct awaitableWithGetAwaiter {
bool await_ready() const noexcept { return false; }
template <typename PromiseType>
void await_suspend(std::coroutine_handle<PromiseType> h) noexcept {}
void await_resume() noexcept {}
};

AwaitTransformTask testingWithAwaitTransform() {
co_await awaitableWithGetAwaiter{};
}

// CHECK-LABEL: @_Z25testingWithAwaitTransformv

// Init suspend
// CHECK: call token @llvm.coro.save
// CHECK-NOT: call void @llvm.coro.opt.blocker(
// CHECK: call void @_ZNSt14suspend_always13await_suspendESt16coroutine_handleIvE{{.*}}#[[NORMAL_ATTR]]

// Check `co_await awaitableWithGetAwaiter{};`.
// CHECK: call token @llvm.coro.save
// CHECK-NOT: call void @llvm.coro.opt.blocker(
// Check call void @_ZN23awaitableWithGetAwaiter13await_suspendIN18AwaitTransformTask12promise_typeEEEvSt16coroutine_handleIT_E{{.*}}#[[NORMAL_ATTR]]

// Final suspend
// CHECK: call token @llvm.coro.save
// CHECK-NOT: call void @llvm.coro.opt.blocker(
// CHECK: call ptr @_ZN18AwaitTransformTask12promise_type12FinalAwaiter13await_suspendIS0_EESt16coroutine_handleIvES3_IT_E{{.*}}#[[NORMAL_ATTR]]

// CHECK-NOT: attributes #[[NORMAL_ATTR]] = noinline
// CHECK: attributes #[[NOINLINE_ATTR]] = {{.*}}noinline

0 comments on commit c467245

Please sign in to comment.