Skip to content

Instantly share code, notes, and snippets.

@vittorioromeo
Last active May 2, 2026 13:02
Show Gist options
  • Select an option

  • Save vittorioromeo/75de982a2950257e6407cf319debb394 to your computer and use it in GitHub Desktop.

Select an option

Save vittorioromeo/75de982a2950257e6407cf319debb394 to your computer and use it in GitHub Desktop.
////////////////////////////////////////////////////////////
// Scenario 5: nested coroutines with per-layer state (16 doubles)
//
// Highlights one of SFEX's costs: every top-level resume re-walks the
// entire C++ call chain from the outermost coroutine down to the leaf.
// At depth 64 with non-trivial per-layer state, each level pays for a
// real function call and an actual touch of its members on every
// resume.
//
// To make the per-layer work observable on every resume (not just on
// first entry), each layer's `operator()` touches its 16-double array
// *before* `SFEX_CO_BEGIN` -- code outside the dispatch switch runs
// unconditionally on every call. The C++20 equivalent does the same
// touch inside its body, which runs between every yield/resume cycle.
////////////////////////////////////////////////////////////
template <int Depth>
struct SfexNestedFat : sfex::Coroutine
{
SfexNestedFat<Depth - 1> inner;
double s[16] = {};
Yield operator()()
{
// Pre-dispatch work: runs on every entry, including resumes.
for (int j = 0; j < 16; ++j)
s[j] += 1.0;
SFEX_CO_BEGIN;
while (true)
{
SFEX_CO_AWAIT(inner());
}
SFEX_CO_END;
}
};
template <>
struct SfexNestedFat<0> : sfex::Coroutine
{
double s[16] = {};
int counter = 0;
Yield operator()()
{
for (int j = 0; j < 16; ++j)
s[j] += 1.0;
SFEX_CO_BEGIN;
while (true)
{
SFEX_CO_YIELD(yieldVal(counter++));
}
SFEX_CO_END;
}
};
// C++20 fat nesting -- each level holds 16 doubles of state and touches
// them on every resume cycle. State lives in the (heap-allocated)
// coroutine frame.
Generator<int> cpp20NestedFat(int depth)
{
double s[16] = {};
if (depth == 0)
{
int counter = 0;
while (true)
{
for (int j = 0; j < 16; ++j)
s[j] += 1.0;
++counter;
co_yield counter;
}
}
else
{
Generator<int> inner = cpp20NestedFat(depth - 1);
while (true)
{
for (int j = 0; j < 16; ++j)
s[j] += 1.0;
inner.resume();
co_yield inner.value();
}
}
}
////////////////////////////////////////////////////////////
// Self-contained Google Benchmark comparing the runtime cost of
// `sfex::Coroutine` (macro-based stackless coroutines) vs C++20 coroutines.
//
// Build (standalone):
//
// cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
// cmake --build build
// ./build/coroutine_benchmark
//
// Or one-shot with all-in-one g++:
//
// g++ -std=c++20 -O3 -DNDEBUG CoroutineBenchmark.cpp \
// -lbenchmark -lpthread -o coroutine_benchmark
//
// The benchmark only measures the cost of *running* (resuming) coroutines.
// Construction and destruction happen outside the timed loop in every test.
//
// Scenarios:
// - Trivial: single-yield infinite loop; one resume per iteration.
// - SingleYield: inner `for` loop with one yield per iteration.
// - MultipleYields: inner `for` loop with four yields per iteration.
// - Nested[N]: N levels of coroutines awaiting each other; one
// top-level resume propagates to a leaf yield.
////////////////////////////////////////////////////////////
#include <benchmark/benchmark.h>
#include <coroutine>
#include <exception>
#include <utility>
#include <cstdint>
////////////////////////////////////////////////////////////
// SFEX coroutine library (inlined from `SfexCoroutine.hpp`,
// stripped to the macros this benchmark uses)
////////////////////////////////////////////////////////////
#if defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wc2y-extensions"
#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
#elif defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
#endif
namespace sfex
{
struct Coroutine
{
std::uint32_t state = 0;
};
} // namespace sfex
#define SFEX_CO_BEGIN \
static constexpr std::uint32_t _sfex_base = __COUNTER__; \
\
switch (state) \
{ \
case 0:;
#define SFEX_CO_YIELD(value) \
do \
{ \
state = (__COUNTER__ + 1) - _sfex_base; \
return value; \
\
case (__COUNTER__ - _sfex_base):; \
} while (0)
#define SFEX_CO_RETURN(value) \
do \
{ \
state = 0; \
return value; \
} while (0)
#define SFEX_CO_END \
} \
\
__builtin_unreachable()
#define SFEX_CO_AWAIT(sub_call) \
do \
{ \
state = (__COUNTER__ + 1) - _sfex_base; \
\
case (__COUNTER__ - _sfex_base): \
{ \
auto _r = (sub_call); \
\
if (!isFinished(_r)) \
return _r; \
} \
} while (0)
////////////////////////////////////////////////////////////
// Yield type used by every SFEX scenario in this benchmark.
//
// Carries a runtime-variable `value` so the compiler can't constant-fold
// the entire chain away when it sees `Yield{false}` in every position.
// `done` would mean "finished" -- none of the benchmark scenarios reach
// that state because they all loop forever.
////////////////////////////////////////////////////////////
struct Yield
{
int value = 0;
bool done = false;
};
[[gnu::always_inline]] inline bool isFinished(Yield y) noexcept
{
return y.done;
}
// Helper: constructs a non-finished Yield carrying a value. Wraps the
// braced-init expression so the comma inside `Yield{v, false}` doesn't
// confuse the function-like `SFEX_CO_YIELD(...)` macro.
[[gnu::always_inline]] inline Yield yieldVal(int v) noexcept
{
return Yield{v, false};
}
////////////////////////////////////////////////////////////
// Minimal `std::generator<T>`-shaped coroutine return type for C++20.
//
// Just enough to support `co_yield` and explicit `resume()` from the
// caller. No iterator support, no `co_await` chaining (we drive nested
// coroutines manually -- see `cpp20Nested` below).
////////////////////////////////////////////////////////////
template <typename T>
struct Generator
{
struct promise_type
{
T currentValue{};
Generator get_return_object()
{
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept
{
return {};
}
std::suspend_always final_suspend() noexcept
{
return {};
}
std::suspend_always yield_value(T value) noexcept
{
currentValue = value;
return {};
}
void return_void() noexcept
{
}
void unhandled_exception()
{
std::terminate();
}
};
using handle_type = std::coroutine_handle<promise_type>;
handle_type handle;
explicit Generator(handle_type h) noexcept : handle{h}
{
}
Generator(const Generator&) = delete;
Generator& operator=(const Generator&) = delete;
Generator(Generator&& o) noexcept : handle{std::exchange(o.handle, {})}
{
}
Generator& operator=(Generator&& o) noexcept
{
if (handle)
handle.destroy();
handle = std::exchange(o.handle, {});
return *this;
}
~Generator()
{
if (handle)
handle.destroy();
}
void resume()
{
handle.resume();
}
[[nodiscard]] T value() const noexcept
{
return handle.promise().currentValue;
}
};
////////////////////////////////////////////////////////////
// Scenario 1: trivial yield in a tight loop
////////////////////////////////////////////////////////////
struct SfexTrivial : sfex::Coroutine
{
int counter = 0;
Yield operator()()
{
SFEX_CO_BEGIN;
while (true)
{
SFEX_CO_YIELD(yieldVal(counter++));
}
SFEX_CO_END;
}
};
Generator<int> cpp20Trivial()
{
int counter = 0;
while (true)
co_yield counter++;
}
////////////////////////////////////////////////////////////
// Scenario 2: single yield inside a fixed-iteration inner loop
//
// The outer `while (true)` keeps the coroutine alive forever so we can
// keep resuming it -- creation cost stays out of the timed region.
////////////////////////////////////////////////////////////
struct SfexSingleYieldLoop : sfex::Coroutine
{
int i = 0;
int counter = 0;
static constexpr int N = 100;
Yield operator()()
{
SFEX_CO_BEGIN;
while (true)
{
for (i = 0; i < N; ++i)
{
SFEX_CO_YIELD(yieldVal(counter++));
}
}
SFEX_CO_END;
}
};
Generator<int> cpp20SingleYieldLoop()
{
constexpr int N = 100;
while (true)
{
for (int i = 0; i < N; ++i)
co_yield i;
}
}
////////////////////////////////////////////////////////////
// Scenario 3: multiple yields inside a fixed-iteration inner loop
//
// 25 iterations * 4 yields per iteration = 100 yields per outer cycle,
// matching the total yield count of scenario 2 for fair comparison.
////////////////////////////////////////////////////////////
struct SfexMultipleYieldsLoop : sfex::Coroutine
{
int i = 0;
int counter = 0;
static constexpr int N = 25;
Yield operator()()
{
SFEX_CO_BEGIN;
while (true)
{
for (i = 0; i < N; ++i)
{
SFEX_CO_YIELD(yieldVal(counter++));
SFEX_CO_YIELD(yieldVal(counter++));
SFEX_CO_YIELD(yieldVal(counter++));
SFEX_CO_YIELD(yieldVal(counter++));
}
}
SFEX_CO_END;
}
};
Generator<int> cpp20MultipleYieldsLoop()
{
constexpr int N = 25;
while (true)
{
for (int i = 0; i < N; ++i)
{
co_yield i;
co_yield i;
co_yield i;
co_yield i;
}
}
}
////////////////////////////////////////////////////////////
// Scenario 4: nested coroutines (N levels)
//
// `SfexNested<Depth>` awaits an `SfexNested<Depth - 1>`, which awaits an
// `SfexNested<Depth - 2>`, all the way down to a leaf that yields.
// Each top-level resume propagates through every level once.
////////////////////////////////////////////////////////////
template <int Depth>
struct SfexNested : sfex::Coroutine
{
SfexNested<Depth - 1> inner;
Yield operator()()
{
SFEX_CO_BEGIN;
while (true)
{
SFEX_CO_AWAIT(inner());
}
SFEX_CO_END;
}
};
template <>
struct SfexNested<0> : sfex::Coroutine
{
int counter = 0;
Yield operator()()
{
SFEX_CO_BEGIN;
while (true)
{
SFEX_CO_YIELD(yieldVal(counter++));
}
SFEX_CO_END;
}
};
// C++20 nesting is built recursively at construction time. Each parent
// owns its child via `Generator<int>`'s move-only handle and ticks it
// once per resume, then yields. One top-level resume produces N+1
// internal resumes (one per level).
Generator<int> cpp20Nested(int depth)
{
if (depth == 0)
{
int counter = 0;
while (true)
co_yield counter++;
}
else
{
Generator<int> inner = cpp20Nested(depth - 1);
while (true)
{
inner.resume();
co_yield inner.value();
}
}
}
////////////////////////////////////////////////////////////
// Benchmarks
////////////////////////////////////////////////////////////
static void BM_SfexTrivial(benchmark::State& state)
{
SfexTrivial co;
benchmark::DoNotOptimize(co);
for (auto _ : state)
{
Yield y = co();
benchmark::DoNotOptimize(y);
benchmark::DoNotOptimize(co);
benchmark::ClobberMemory();
}
}
BENCHMARK(BM_SfexTrivial);
static void BM_Cpp20Trivial(benchmark::State& state)
{
auto co = cpp20Trivial();
benchmark::DoNotOptimize(co);
for (auto _ : state)
{
co.resume();
benchmark::DoNotOptimize(co);
benchmark::ClobberMemory();
}
}
BENCHMARK(BM_Cpp20Trivial);
static void BM_SfexSingleYieldLoop(benchmark::State& state)
{
SfexSingleYieldLoop co;
benchmark::DoNotOptimize(co);
for (auto _ : state)
{
Yield y = co();
benchmark::DoNotOptimize(y);
benchmark::DoNotOptimize(co);
benchmark::ClobberMemory();
}
}
BENCHMARK(BM_SfexSingleYieldLoop);
static void BM_Cpp20SingleYieldLoop(benchmark::State& state)
{
auto co = cpp20SingleYieldLoop();
benchmark::DoNotOptimize(co);
for (auto _ : state)
{
co.resume();
benchmark::DoNotOptimize(co);
benchmark::ClobberMemory();
}
}
BENCHMARK(BM_Cpp20SingleYieldLoop);
static void BM_SfexMultipleYieldsLoop(benchmark::State& state)
{
SfexMultipleYieldsLoop co;
benchmark::DoNotOptimize(co);
for (auto _ : state)
{
Yield y = co();
benchmark::DoNotOptimize(y);
benchmark::DoNotOptimize(co);
benchmark::ClobberMemory();
}
}
BENCHMARK(BM_SfexMultipleYieldsLoop);
static void BM_Cpp20MultipleYieldsLoop(benchmark::State& state)
{
auto co = cpp20MultipleYieldsLoop();
benchmark::DoNotOptimize(co);
for (auto _ : state)
{
co.resume();
benchmark::DoNotOptimize(co);
benchmark::ClobberMemory();
}
}
BENCHMARK(BM_Cpp20MultipleYieldsLoop);
////////////////////////////////////////////////////////////
// Nested benchmarks. SFEX uses `BENCHMARK_TEMPLATE` since the depth is
// part of the type; C++20 uses `Arg` since depth is a runtime value.
////////////////////////////////////////////////////////////
template <int Depth>
static void BM_SfexNested(benchmark::State& state)
{
SfexNested<Depth> co;
benchmark::DoNotOptimize(co);
for (auto _ : state)
{
Yield y = co();
benchmark::DoNotOptimize(y);
benchmark::DoNotOptimize(co);
benchmark::ClobberMemory();
}
}
BENCHMARK_TEMPLATE(BM_SfexNested, 2);
BENCHMARK_TEMPLATE(BM_SfexNested, 4);
BENCHMARK_TEMPLATE(BM_SfexNested, 8);
BENCHMARK_TEMPLATE(BM_SfexNested, 16);
BENCHMARK_TEMPLATE(BM_SfexNested, 32);
BENCHMARK_TEMPLATE(BM_SfexNested, 64);
static void BM_Cpp20Nested(benchmark::State& state)
{
auto co = cpp20Nested(static_cast<int>(state.range(0)));
benchmark::DoNotOptimize(co);
for (auto _ : state)
{
co.resume();
benchmark::DoNotOptimize(co);
benchmark::ClobberMemory();
}
}
BENCHMARK(BM_Cpp20Nested)->Arg(2)->Arg(4)->Arg(8)->Arg(16)->Arg(32)->Arg(64);
BENCHMARK_MAIN();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment