Skip to content
Merged
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
7 changes: 7 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,13 @@ Bug Fixes in This Version
- Fix a hang on valid C code passing a function type as an argument to
``typeof`` to form a function declaration.
(`#64713 <https://github.com/llvm/llvm-project/issues/64713>_`)
- 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>`_)
The current solution may bring performance regressions if the awaiters have
non-static data members. See
`#64945 <https://github.com/llvm/llvm-project/issues/64945>`_ for details.

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 @@ -5467,6 +5467,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
Loading