SEDA bus · C++ · 0.1.0
Rust's model, translated almost mechanically — with one real surprise.
seda-bus-cpp is header-only C++20, following seda-bus-rust's concurrency
model closely: real OS threads, no garbage collector, so the shape translates directly. It's also
the one implementation in this comparison whose 8-producer throughput came in lower than
its 1-producer number — a real finding, tracked down and fixed, not a fluke.
std::deque job queue, standing in for Rust's mpscra-common-cpp onlyra::common::Envelope, aliased as ra::seda_bus::EnvelopeDynamicRoutingSlip — LIFOHow it implements the design
No GC, so Rust's shape ports almost line for line.
Bus wraps a std::shared_ptr<detail::Impl>, mirroring Rust's
Arc<Inner> — copies are cheap and share state. Consumer is
std::function<bool(Envelope&)>, the direct analogue of Rust's blanket
Fn(&mut Envelope) -> bool implementation, so lambdas subscribe directly with
no adapter class. A thrown exception from a consumer is caught and treated as a nack, exactly
like Rust's catch_unwind. std::shared_mutex guards the channel
registry and each stage's consumer list (read far more than written); a plain
std::mutex guards the dead-letter and callback maps — matching Rust's
RwLock/Mutex split exactly.
The one real design bug this port had, and the fix. Originally,
Channel::Offer (push) and Poll (pop) shared a single
std::mutex guarding the whole queue — so every producer and every consumer
on a stage contended on the same lock, regardless of which end they touched. This was this
comparison's worst throughput collapse under 8-way concurrency. A first fix attempt used a
textbook two-lock (Michael & Scott) queue over a linked list: it fixed the contention but
broke the previously-clean single-producer case, introducing a bimodal cold-stall pattern from
200,000 individual heap allocations where std::deque had amortized growth. The
working fix backs the same two-lock algorithm with a pre-allocated circular buffer
instead, removing the allocator from the hot path entirely — verified clean under
ThreadSanitizer in Docker, 13/13 tests, 75/75 assertions.
(Three unrelated construction-time bugs in ra-common-cpp — re-opened
/dev/urandom handles, a JSON serialize-then-reparse route clone, and a
process-wide mutex around random byte generation — were found and fixed in an earlier
pass of this benchmark. They mattered to anyone constructing envelopes directly, but don't
show up in the dispatch-only numbers below, which exclude envelope construction from the
timed window.)
Results
The only port where 8 producers beat 1 producer — backwards.
| Config | Sustained eps | 0.5× | 1.0× | 1.5× |
|---|---|---|---|---|
cap1 | 179,119 | 0.92 | 0.89 | 0.86 |
cap8 | 90,980 | 0.86 | 0.83 | 0.69 |
C++ is the only implementation in this comparison whose cap8 sustained
throughput is lower than its cap1 (90,980 vs 179,119 — every other
port's cap8 beats its own cap1). This is real, not a measurement
artifact: under a genuinely bounded, contended queue with 8 producers and 8 concurrent consumers,
head/tail-lock contention has a real cost that unthrottled firehose conditions don't fully expose.
It's also this port's mildest 1.5× overload drop of any config in the whole comparison
(0.86) — its single-producer dispatch path is fast enough that 150% of its own ceiling barely
moves the needle.
| Config | eps | vs. seq |
|---|---|---|
seq (1 producer) | 333,154 | — |
par (8 producers, 1 channel) | 264,553 | 0.79× |
chan (8 producers, 8 channels) | 396,987 | 1.19× |
Before the two-lock ring-buffer fix, this benchmark's worst par collapse held at
0.34–0.47× across three earlier passes, traced each time to this exact mutex under
Docker's virtualization. The fix took it from 68,998 to 253,233 eps in one Docker-verified run
(~3.7×) — the 0.79× ratio above is a normal, much smaller residual: 8 producers
still funnel into one stage, it just no longer pays a mutex tax on top.
Reading the numbers
Latency shape confirms the fix worked, not just the throughput.
seq stays this comparison's cleanest case (p50/elapsed 0.01/0.06); par
moved from a rare severe cliff to a “mild backlog” classification (0.01/0.27) as a
direct consequence of sustaining far more real throughput — by Little's law, doing more
real work per second naturally carries a bigger average queue depth, which is what a higher
p50/elapsed ratio means here. chan still shows a real, sustained backlog
(0.15/0.74), the same class of finding this comparison found across most compiled and
interpreted ports once producers weren't artificially throttled by construction cost.
The full cross-language results, including the two-lock queue investigation
Source
Read the code, or the rest of the family.
seda-bus-cpp on GitHub
— source, its own DESIGN.md, and the test suite.