TGViewer
C++ - Reddit C++ - Reddit @r_cpp · 229 subscribers
Post #24948 17
We benchmarked sender-based I/O against coroutine-based I/O. Here's what we found.

When I/O operations return senders, they incur an unnecessary per-operation allocation. This explains why.

|Stream Type|capy::task|bex::task|sender pipeline|
|:-|:-|:-|:-|
|Native|0|0|0|
|Abstract|0|1|1|
|Type-erased|0|1|1|

When an I/O stream is type-erased, sender/receiver's connect() produces an operation state whose type depends on both the sender and the receiver. The size is unknown at construction time. It must be heap-allocated per operation. Under awaitables, await_suspend takes a coroutine_handle<> — the consumer type is already erased — so the awaitable can be preallocated once and reused. The allocation cannot be eliminated. It follows from connect producing a type that depends on both the sender and the receiver.

We measured this. The benchmark executes 20,000,000 read_some calls per configuration on a single thread using a stream that isolates the execution model overhead from I/O latency. Five independent runs plus warmup; values are mean ± standard deviation. The benchmark source is public:

https://github.com/cppalliance/capy/tree/develop/bench/beman

Anyone is invited to inspect the code, suggest improvements, and help make it better. The architects of P2300 are especially welcome — their expertise would strengthen the comparison.

Two papers address the cost asymmetry. P4003R0 "Coroutines for I/O" defines the IoAwaitable protocol for standard I/O operations. P4126R0 "A Universal Continuation Model" is purely additive — it gives sender/receiver pipelines zero-allocation access to every awaitable ever written. Together they make coroutines and senders both first-class citizens of the I/O stack.

# Benchmark Results

All values are mean ± stddev over 5 runs (warmup pass discarded). Each table measures one execution model consuming two I/O return types (awaitable and sender). The native column is the model's own I/O type; the other column goes through a bridge.

# Table 1: sender/receiver pipeline

|Stream Type|sender (native)|awaitable (bridge)|
|:-|:-|:-|
|Native|34.3 ± 0.1 ns/op, 0 al/op|46.3 ± 0.0 ns/op, 1 al/op|
|Abstract|47.1 ± 0.2 ns/op, 1 al/op|46.4 ± 0.0 ns/op, 1 al/op|
|Type-erased|57.5 ± 0.0 ns/op, 1 al/op|54.1 ± 0.1 ns/op, 1 al/op|
|Synchronous|2.6 ± 0.3 ns/op, 0 al/op|5.1 ± 0.1 ns/op, 0 al/op|

# Table 2: capy::task

|Stream Type|awaitable (native)|sender (bridge)|
|:-|:-|:-|
|Native|31.4 ± 0.2 ns/op, 0 al/op|48.1 ± 0.3 ns/op, 0 al/op|
|Abstract|32.3 ± 0.2 ns/op, 0 al/op|72.2 ± 0.2 ns/op, 1 al/op|
|Type-erased|36.4 ± 0.1 ns/op, 0 al/op|72.1 ± 0.0 ns/op, 1 al/op|
|Synchronous|1.0 ± 0.2 ns/op, 0 al/op|19.0 ± 0.0 ns/op, 0 al/op|

# Table 3: beman::execution::task

Note: bex::task's await_transform calls the sender's as_awaitable member directly when available, bypassing connect and start. Table 3's native sender column measures the as_awaitable path, not the full sender protocol.

|Stream Type|sender (native)|awaitable (bridge)|
|:-|:-|:-|
|Native|31.9 ± 0.0 ns/op, 0 al/op|43.5 ± 0.1 ns/op, 1 al/op|
|Abstract|55.2 ± 0.0 ns/op, 1 al/op|43.4 ± 0.0 ns/op, 1 al/op|
|Type-erased|55.2 ± 0.0 ns/op, 1 al/op|48.7 ± 0.1 ns/op, 1 al/op|
|Synchronous|1.0 ± 0.2 ns/op, 0 al/op|2.9 ± 0.2 ns/op, 0 al/op|

The full formatted report with detailed analysis is here: https://gist.github.com/sgerbino/2a64990fb221f6706197325c03e29a5e

# Analysis

Native performance is equivalent. Both models achieve \~31–34 ns/op with zero allocations when consuming their native I/O type on a concrete stream. There is no inherent speed advantage to either model at the baseline.

Type erasure costs diverge. capy::any_read_stream adds \~5 ns/op and zero allocations. The awaitable is preallocated at stream construction and reused across every read_some call. This is possible because
GitHub capy/bench/beman at develop · cppalliance/capy Provides facilities for creating and accessing optional services during runtime. - cppalliance/capy
More from @r_cpp
  1. Oct 6, 2026I built MrDocs, an open-source C++ reference doc generator on the Clang/LLVM AST. Looking…
  2. Oct 6, 2026WasmBolt — The LLVM Project in your browser https://anutosh21.github.io/WasmBolt/ https://…
  3. Oct 6, 2026How I speed up my overall build times by 4x and incremental builds by 9x for test builds *…
  4. Oct 6, 2026Why do people on Windows mostly choose Visual Studio? https://redd.it/1weku08 @r_cpp
  5. Oct 6, 2026Jonas Persson: Making every byte count https://youtu.be/GbwrztrY4kc https://redd.it/1wf1l3…
  6. Oct 6, 2026It annoys me that coroutines are described as "functions that can be suspended" Any functi…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →