Skip to content

[Coroutines] HALO through [[clang::coro_await_elidable_argument]] no longer happens at -O1/-O2/-O3 in clang 23 (regression from 22) #241761

Closed as duplicate

Description

@kelbon

Summary

Starting with clang 23, a coroutine passed as an argument to a [[clang::coro_await_elidable_argument]] parameter is no longer elided, even though the outer call is directly co_awaited. This is exactly the documented when_all use case of the attribute

Maybe related to #214151

Reproducer

#include <coroutine>
#include <cstdio>
#include <cstdlib>
#include <new>
#include <utility>
 
static int allocations = 0;
 
template <typename T>
struct [[clang::coro_await_elidable]] Task {
  struct promise_type {
    T value{};
    std::coroutine_handle<> continuation;
 
    static void* operator new(std::size_t size) {
      ++allocations;
      return ::operator new(size);
    }
    static void operator delete(void* ptr, std::size_t size) {
      ::operator delete(ptr, size);
    }
 
    Task get_return_object() {
      return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
    }
    std::suspend_always initial_suspend() noexcept {
      return {};
    }
    auto final_suspend() noexcept {
      struct awaiter {
        bool await_ready() noexcept {
          return false;
        }
        std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h) noexcept {
          auto c = h.promise().continuation;
          return c ? c : std::noop_coroutine();
        }
        void await_resume() noexcept {
        }
      };
      return awaiter{};
    }
    void return_value(T v) {
      value = v;
    }
    void unhandled_exception() {
      std::abort();
    }
  };
 
  std::coroutine_handle<promise_type> handle;
 
  explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {
  }
  Task(Task&& other) noexcept : handle(std::exchange(other.handle, nullptr)) {
  }
  ~Task() {
    if (handle)
      handle.destroy();
  }
 
  auto operator co_await() && noexcept {
    struct awaiter {
      std::coroutine_handle<promise_type> h;
      bool await_ready() noexcept {
        return false;
      }
      std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) noexcept {
        h.promise().continuation = caller;
        return h;
      }
      T await_resume() noexcept {
        return h.promise().value;
      }
    };
    return awaiter{handle};
  }
};
 
Task<int> leaf(int x) {
  co_return x;
}
 
// `arg` is in safe elide context when `forward(...)` itself is co_awaited
Task<int> forward([[clang::coro_await_elidable_argument]] Task<int> arg) {
  co_return co_await std::move(arg);
}
 
Task<int> direct() {
  co_return co_await leaf(1);  // safe elide context: immediate operand of co_await
}
 
Task<int> via_argument() {
  co_return co_await forward(leaf(2));  // leaf(2) is in safe elide context via forward's parameter
}
 
template <typename F>
int count_callee_allocations(F f) {
  Task<int> root = f();  // root frame itself is heap-allocated, do not count it
  allocations = 0;
  root.handle.resume();
  if (!root.handle.done())
    std::abort();
  return allocations;
}
 
int main() {
  std::printf("direct co_await leaf():        %d allocation(s) (expected 0)\n", count_callee_allocations(direct));
  std::printf("co_await forward(leaf()):      %d allocation(s) (expected 0)\n",
              count_callee_allocations(via_argument));
}
clang++ -std=c++20 -O2 repro.cpp -o repro && ./repro

Results (x86_64 Linux, official release binaries / Ubuntu packages)

clang co_await leaf() co_await forward(leaf())
20.1.2 0 0
21.1.8 0 0
22.1.8 0 0
23.1.5 0 1

The result on 23.1.5 is the same with -O1, -O2 and -O3.

With -mllvm -coro-elide-branch-ratio=0 on 23.1.5 both cases report 0 allocations.

Proposal: add kelcoro and its full test suite to llvm-test-suite

This regression was caught by the test suite of kelcoro, a C++20 coroutine library I maintain

Every clang release since 14 to 22 has failed on kelcoro with different ICE. clang 23 - this regression

kelcoro is small enough to build and run on every release candidate, so regressions like this one would be caught in rc1 instead of in user code.

LLVM code analysis from llm (looks correct):

The frontend still marks the call as `coro_elide_safe`, so the problem is in the middle end. `-mllvm -coro-elide-branch-ratio=0` restores the elision, so this is very likely the same root cause as #214151 (branch frequency changes from #194281). I am filing it separately because the effect here is stronger: no PGO, no unusual control flow, and the documented pattern for `coro_await_elidable_argument` stops working at every optimization level.

Activity

  1. added and removed
    clangClang issues not falling into any other category
    on Nov 12, 2026
  2. llvmorg-github-actions commented on Nov 12, 2026

    @llvmorg-github-actions

    @llvm/issue-subscribers-coroutines

    Author: kelbon

    Details Summary

    Starting with clang 23, a coroutine passed as an argument to a [[clang::coro_await_elidable_argument]] parameter is no longer elided, even though the outer call is directly co_awaited. This is exactly the documented when_all use case of the attribute

    Maybe related to #214151

    Reproducer

    #include &lt;coroutine&gt;
    #include &lt;cstdio&gt;
    #include &lt;cstdlib&gt;
    #include &lt;new&gt;
    #include &lt;utility&gt;
     
    static int allocations = 0;
     
    template &lt;typename T&gt;
    struct [[clang::coro_await_elidable]] Task {
      struct promise_type {
        T value{};
        std::coroutine_handle&lt;&gt; continuation;
     
        static void* operator new(std::size_t size) {
          ++allocations;
          return ::operator new(size);
        }
        static void operator delete(void* ptr, std::size_t size) {
          ::operator delete(ptr, size);
        }
     
        Task get_return_object() {
          return Task{std::coroutine_handle&lt;promise_type&gt;::from_promise(*this)};
        }
        std::suspend_always initial_suspend() noexcept {
          return {};
        }
        auto final_suspend() noexcept {
          struct awaiter {
            bool await_ready() noexcept {
              return false;
            }
            std::coroutine_handle&lt;&gt; await_suspend(std::coroutine_handle&lt;promise_type&gt; h) noexcept {
              auto c = h.promise().continuation;
              return c ? c : std::noop_coroutine();
            }
            void await_resume() noexcept {
            }
          };
          return awaiter{};
        }
        void return_value(T v) {
          value = v;
        }
        void unhandled_exception() {
          std::abort();
        }
      };
     
      std::coroutine_handle&lt;promise_type&gt; handle;
     
      explicit Task(std::coroutine_handle&lt;promise_type&gt; h) : handle(h) {
      }
      Task(Task&amp;&amp; other) noexcept : handle(std::exchange(other.handle, nullptr)) {
      }
      ~Task() {
        if (handle)
          handle.destroy();
      }
     
      auto operator co_await() &amp;&amp; noexcept {
        struct awaiter {
          std::coroutine_handle&lt;promise_type&gt; h;
          bool await_ready() noexcept {
            return false;
          }
          std::coroutine_handle&lt;&gt; await_suspend(std::coroutine_handle&lt;&gt; caller) noexcept {
            h.promise().continuation = caller;
            return h;
          }
          T await_resume() noexcept {
            return h.promise().value;
          }
        };
        return awaiter{handle};
      }
    };
     
    Task&lt;int&gt; leaf(int x) {
      co_return x;
    }
     
    // `arg` is in safe elide context when `forward(...)` itself is co_awaited
    Task&lt;int&gt; forward([[clang::coro_await_elidable_argument]] Task&lt;int&gt; arg) {
      co_return co_await std::move(arg);
    }
     
    Task&lt;int&gt; direct() {
      co_return co_await leaf(1);  // safe elide context: immediate operand of co_await
    }
     
    Task&lt;int&gt; via_argument() {
      co_return co_await forward(leaf(2));  // leaf(2) is in safe elide context via forward's parameter
    }
     
    template &lt;typename F&gt;
    int count_callee_allocations(F f) {
      Task&lt;int&gt; root = f();  // root frame itself is heap-allocated, do not count it
      allocations = 0;
      root.handle.resume();
      if (!root.handle.done())
        std::abort();
      return allocations;
    }
     
    int main() {
      std::printf("direct co_await leaf():        %d allocation(s) (expected 0)\n", count_callee_allocations(direct));
      std::printf("co_await forward(leaf()):      %d allocation(s) (expected 0)\n",
                  count_callee_allocations(via_argument));
    }
    clang++ -std=c++20 -O2 repro.cpp -o repro &amp;&amp; ./repro
    

    Results (x86_64 Linux, official release binaries / Ubuntu packages)

    clang co_await leaf() co_await forward(leaf())
    20.1.2 0 0
    21.1.8 0 0
    22.1.8 0 0
    23.1.5 0 1

    The result on 23.1.5 is the same with -O1, -O2 and -O3.

    With -mllvm -coro-elide-branch-ratio=0 on 23.1.5 both cases report 0 allocations.

    Proposal: add kelcoro and its full test suite to llvm-test-suite

    This regression was caught by the test suite of kelcoro, a C++20 coroutine library I maintain

    Every clang release since 14 to 22 has failed on kelcoro with different ICE. clang 23 - this regression

    kelcoro is small enough to build and run on every release candidate, so regressions like this one would be caught in rc1 instead of in user code.

    LLVM code analysis from llm (looks correct):

    The frontend still marks the call as `coro_elide_safe`, so the problem is in the middle end. `-mllvm -coro-elide-branch-ratio=0` restores the elision, so this is very likely the same root cause as #<!-- -->214151 (branch frequency changes from #<!-- -->194281). I am filing it separately because the effect here is stronger: no PGO, no unusual control flow, and the documented pattern for `coro_await_elidable_argument` stops working at every optimization level.
    
  3. yuxuanchen1997 commented on Nov 12, 2026

    @yuxuanchen1997
    Member

    Thanks for the report. This appears to be a duplicate of #214151

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions