Last active
May 2, 2026 13:02
-
-
Save vittorioromeo/75de982a2950257e6407cf319debb394 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //////////////////////////////////////////////////////////// | |
| // 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(); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //////////////////////////////////////////////////////////// | |
| // 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