Skip to content

Elide suspension points via [[clang::coro_await_suspend_destroy]] #152623

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ Removed Compiler Flags
Attribute Changes in Clang
--------------------------

- Introduced a new attribute ``[[clang::coro_await_suspend_destroy]]``. When
applied to a coroutine awaiter class, it causes suspensions into this awaiter
to use a new `await_suspend_destroy(Promise&)` method instead of the standard
`await_suspend(std::coroutine_handle<...>)`. The coroutine is then destroyed.
This improves code speed & size for "short-circuiting" coroutines.

Improvements to Clang's diagnostics
-----------------------------------
- Added a separate diagnostic group ``-Wfunction-effect-redeclarations``, for the more pedantic
Expand Down
8 changes: 8 additions & 0 deletions clang/include/clang/Basic/Attr.td
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,14 @@ def CoroAwaitElidableArgument : InheritableAttr {
let SimpleHandler = 1;
}

def CoroAwaitSuspendDestroy: InheritableAttr {
let Spellings = [Clang<"coro_await_suspend_destroy">];
let Subjects = SubjectList<[CXXRecord]>;
let LangOpts = [CPlusPlus];
let Documentation = [CoroAwaitSuspendDestroyDoc];
let SimpleHandler = 1;
}

// OSObject-based attributes.
def OSConsumed : InheritableParamAttr {
let Spellings = [Clang<"os_consumed">];
Expand Down
92 changes: 92 additions & 0 deletions clang/include/clang/Basic/AttrDocs.td
Original file line number Diff line number Diff line change
Expand Up @@ -9270,6 +9270,98 @@ Example:
}];
}

def CoroAwaitSuspendDestroyDoc : Documentation {
let Category = DocCatDecl;
let Content = [{

The ``[[clang::coro_await_suspend_destroy]]`` attribute may be applied to a C++
coroutine awaiter type. When this attribute is present, the awaiter must
implement ``void await_suspend_destroy(Promise&)``. If ``await_ready()``
returns ``false`` at a suspension point, ``await_suspend_destroy`` will be
called directly, bypassing the ``await_suspend(std::coroutine_handle<...>)``
method. The coroutine being suspended will then be immediately destroyed.

Logically, the new behavior is equivalent to this standard code:

.. code-block:: c++

void await_suspend_destroy(YourPromise&) { ... }
void await_suspend(auto handle) {
await_suspend_destroy(handle.promise());
handle.destroy();
}

This enables `await_suspend_destroy()` usage in portable awaiters — just add a
stub ``await_suspend()`` as above. Without ``coro_await_suspend_destroy``
support, the awaiter will behave nearly identically, with the only difference
being heap allocation instead of stack allocation for the coroutine frame.

Comment on lines +9295 to +9298
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coroutine's heap allocation gets elided as a result of coro_await_suspend_destroy, so you can't say this for certain here without knowing usages of other awaitables here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used this phrasing for brevity. I will rephrase it to be more precise.

This attribute exists to optimize short-circuiting coroutines—coroutines whose
suspend points are either (i) trivial (like ``std::suspend_never``), or (ii)
short-circuiting (like a ``co_await`` that can be expressed in regular control
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel better to define the concept short-circuiting coro more explicitly and clearly.

Copy link
Author

@snarkmaster snarkmaster Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next update, I will start with something like the below. I'm happy to linkify the various concepts if that's not frowned upon. Do you have any feedback?


A short-circuiting coroutine is one where every co_await or co_yield either immediately produces a value, or exits the coroutine. In other words, they use coroutine syntax to concisely branch out of a synchronous function. Here are analogies to other languages:

  • Rust has Result<T> and a ? operator to unpack it, while folly::result<T> is a C++ short-circuiting coroutine, where co_await acts just like ?.

  • Haskell has Maybe & Error monads. A short-circuiting co_await loosely corresponds to the monadic >>=, whereas a short-circuiting std::optional would be an exact analog of Maybe.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response requested

flow as):

.. code-block:: c++

T val;
if (awaiter.await_ready()) {
val = awaiter.await_resume();
} else {
awaiter.await_suspend();
return /* value representing the "execution short-circuited" outcome */;
}

The benefits of this attribute are:

- **Avoid heap allocations for coro frames**: Allocating short-circuiting
coros on the stack makes code more predictable under memory pressure.
Without this attribute, LLVM cannot elide heap allocation even when all
awaiters are short-circuiting.

- **Performance**: Significantly faster execution and smaller code size.

- **Build time**: Faster compilation due to less IR being generated.

Marking your ``await_suspend_destroy`` method as ``noexcept`` can sometimes
further improve optimization.
Comment on lines +9352 to +9353
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method is not a C++ term. The await_suspend_destroy is a non-static member function.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol sure, I'll change it, but ~everyone knows that method is short for "member function" :-P


Here is a toy example of a portable short-circuiting awaiter:

.. code-block:: c++

template <typename T>
struct [[clang::coro_await_suspend_destroy]] optional_awaitable {
std::optional<T> opt_;
bool await_ready() const noexcept { return opt_.has_value(); }
T await_resume() { return std::move(opt_).value(); }
void await_suspend_destroy(auto& promise) {
// Assume the return object of the outer coro defaults to "empty".
}
// Fallback for when `coro_await_suspend_destroy` is unavailable.
void await_suspend(auto handle) {
await_suspend_destroy(handle.promise());
handle.destroy();
}
};

If all suspension points use (i) trivial or (ii) short-circuiting awaiters,
then the coroutine optimizes more like a plain function, with 2 caveats:

- **Behavior:** The coroutine promise provides an implicit exception boundary
(as if wrapping the function in ``try {} catch { unhandled_exception(); }``).
This exception handling behavior is usually desirable in robust,
return-value-oriented programs that need short-circuiting coroutines.
Otherwise, the promise can always re-throw.

- **Speed:** As of 2025, there is still an optimization gap between a
realistic short-circuiting coro, and the equivalent (but much more verbose)
function. For a guesstimate, expect 4-5ns per call on x86. One idea for
improvement is to also elide trivial suspends like `std::suspend_never`, in
order to hit the `HasCoroSuspend` path in `CoroEarly.cpp`.

}];
}

def CountedByDocs : Documentation {
let Category = DocCatField;
let Content = [{
Expand Down
3 changes: 3 additions & 0 deletions clang/include/clang/Basic/DiagnosticSemaKinds.td
Original file line number Diff line number Diff line change
Expand Up @@ -12504,6 +12504,9 @@ def note_coroutine_promise_call_implicitly_required : Note<
def err_await_suspend_invalid_return_type : Error<
"return type of 'await_suspend' is required to be 'void' or 'bool' (have %0)"
>;
def err_await_suspend_destroy_invalid_return_type : Error<
"return type of 'await_suspend_destroy' is required to be 'void' (have %0)"
>;
def note_await_ready_no_bool_conversion : Note<
"return type of 'await_ready' is required to be contextually convertible to 'bool'"
>;
Expand Down
Loading