llvm / llvm/llvm-project

Coroutine elision not happening at default elision threshold anymore in llvm 23

Open
#214,151 2 comments 1 reaction 0 assignees View on GitHub
clang:codegen coroutines missed-optimization regression:23
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

It seems like branch frequency calculation has changed somewhere in llvm 23 which means that coroutine elision / HALO now no longer triggers or only very rarely.

I noticed this as part of https://github.com/llvm/llvm-project/issues/188230.

Originally the reproducer reproduced at "default" clang args (i.e.: only `-O2`). With the clang 23 RC it now also additionally requires a lowered value of `-mllvm -coro-elide-branch-ratio=X`. Alternatively PGO still seems to function fine and allow HALO to trigger.

We can see the same in our seastar coroutines microbenches and real code. Originally we would crash pretty quickly which was no longer the case and even now that it's fixed we don't see HALO getting active at all. Again the two workarounds from above are needed.

I have traced the issue back to https://github.com/llvm/llvm-project/pull/194281 / db001b920668d26523e15c95fa34f85e6b13456e. With that patch reverted HALO seems to happen as before.

Looks like the extra cleanup block screws with branch frequencies.

With the following reproducer we see:

```
stephan@rp:/build/llvm-project$ ./build/bin/clang++ -std=c++23 -O2 -o /tmp/repro /tmp/repro.cpp && /tmp/repro
after start: allocations=2 deallocations=0 finished=0
after resume: allocations=2 deallocations=2 finished=1 value=42
```

and with db001b920668d26523e15c95fa34f85e6b13456e reverted we get:

```
stephan@rp:/build/llvm-project$ ./build/bin/clang++ -std=c++23 -O2 -o /tmp/repro /tmp/repro.cpp && /tmp/repro
after start: allocations=1 deallocations=0 finished=0
after resume: allocations=1 deallocations=1 finished=1 value=42
```

``` cpp
#include
#include
#include
#include
#include

static int allocations;
static int deallocations;

struct gate {
std::coroutine_handle<> waiter = nullptr;

struct awaiter {
gate &g;

bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) noexcept { g.waiter = h; }
void await_resume() noexcept {}
};

awaiter operator co_await() noexcept { return {*this}; }

void open() noexcept {
if (waiter)
std::exchange(waiter, nullptr).resume();
}
};

struct [[clang::coro_await_elidable]] task {
struct promise_type {
std::coroutine_handle<> continuation = nullptr;

static void *operator new(std::size_t size) {
++allocations;
return ::operator new(size);
}

static void operator delete(void *ptr, std::size_t) noexcept {
++deallocations;
::operator delete(ptr);
}

task get_return_object() noexcept {
return {std::coroutine_handle::from_promise(*this)};
}

std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }

void return_void() noexcept {
if (continuation)
continuation.resume();
}

void unhandled_exception() noexcept { std::abort(); }
};

std::coroutine_handle handle;

bool await_ready() noexcept { return false; }

void await_suspend(std::coroutine_handle<> continuation) noexcept {
handle.promise().continuation = continuation;
}

void await_resume() noexcept {}
};

task callee(gate &g, int &value) {
co_await g;
value = 42;
}

[[gnu::noinline]] task caller(gate &g, int &value, bool &finished) {
co_await callee(g, value);
finished = true;
}

int main() {
gate g;
int value = 0;
bool finished = false;

[[maybe_unused]] task root = caller(g, value, finished);
std::printf("after start: allocations=%d deallocations=%d finished=%d\n",
allocations, deallocations, finished);

g.open();
std::printf("after resume: allocations=%d deallocations=%d finished=%d "
"value=%d\n",
allocations, deallocations, finished, value);

return value == 42 && finished ? 0 : 1;
}
```

Contributor guide

Open the contributing guide

Research direction

Start with the supplied C++ coroutine reproducer and run it with clang++ -std=c++23 -O2, then compare behavior with commit db001b920668d26523e15c95fa34f85e6b13456e reverted. Read PR 194281 and investigate the extra cleanup block's branch-frequency effects on coroutine elision. Done means HALO triggers at the default threshold again, producing one allocation in the reproducer without requiring PGO or a lowered -mllvm threshold.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.