TWO-LAYER BUFFERS · BATCH PUBLICATION AND TRANSFER · PRODUCER HOT PATH

TwoBufQueue

TwoBufQueue is a high-performance C++ producer–consumer message queue component for transferring data between threads at high speed.

Its two-layer buffer design prioritizes producer hot-path performance. Batch publication and transfer let producers use small buffers while consumers use larger buffers, reducing the effect of message transfer on business execution. Supports single or multiple producers and consumers. Currently requires C++20.

1. A few interesting questions

1.1 Why can business work slow down after sending, even without backpressure?

The queue has room and sends never block, so everything appears normal. Yet after sending a continuous batch, the same business code can take several times longer to execute. If the producer never waited, where did the extra time go?

1.2 How much faster can batch submission be for tiny messages?

Copying an 8 or 16 B message should be quick. The smaller the message, the more the other work involved in sending deserves attention. With the same amount of data and the same number of messages, how much can performance differ between individual and batch submission?

1.3 When message buffers are full, must the producer keep waiting?

Consumption slows down, messages accumulate and the buffers eventually fill. Waiting interrupts business execution; dropping messages can lose important records. Could there be another place for pending messages while keeping memory use bounded?

1.4 How can large messages be sent more efficiently?

Most records may be only a few dozen bytes, with an occasional snapshot, sample batch or data block measuring several MiB. A single send then writes a broad memory range. How can it transfer quickly while minimizing its effect on subsequent business work?

1.5 Small buffers cause backpressure; large buffers waste memory. What should you do?

Small buffers may not cover bursts and can make the producer wait. Large buffers may retain memory for peaks that happen only occasionally. When normal traffic and peak traffic differ greatly, how can burst capacity and memory use be balanced?

These questions guide the design and continued improvement of TwoBufQueue. See Problems and solutions for the approaches and planned features. Continue below for the performance measurements.

2. How fast is it?

2.1 Producer submission throughput

The four charts compare producer submission throughput in million messages/s; higher is better. TwoBufQueue uses WriteSession; consumers run concurrently.

Session submission throughputMillion messages/s · higher is better

8 B

TwoBufQueue2,306.5
ReaderWriterQueue801.0
rigtorp::SPSCQueue326.1
atomic_queue186.3

16 B

TwoBufQueue1,697.7
ReaderWriterQueue767.4
rigtorp::SPSCQueue267.5
atomic_queue48.7

32 B

TwoBufQueue658.6
ReaderWriterQueue422.6
rigtorp::SPSCQueue220.7
atomic_queue30.2

64 B

TwoBufQueue383.2
ReaderWriterQueue204.3
rigtorp::SPSCQueue99.4
atomic_queue45.2
Million messages/s, higher is better. Full report and parameters

Full Session results · Ordinary write, batch publication · Ordinary write, per-record publication

2.2 Business performance after a continuous message batch

Every queue follows the same sequence: normal business work → send a continuous message batch → immediately repeat the same business work. The chart shows post-send cost / normal cost; lower is better.

Post-send cost / normal cost · p50Normal reference: 1.00× (13 ns)

2 MiB / batch

TwoBufQueue1.20×
ReaderWriterQueue3.00×
rigtorp::SPSCQueue1.20×
atomic_queue3.34×

4 MiB / batch

TwoBufQueue1.20×
ReaderWriterQueue3.34×
rigtorp::SPSCQueue1.74×
atomic_queue3.67×

8 MiB / batch

TwoBufQueue1.20×
ReaderWriterQueue3.61×
rigtorp::SPSCQueue3.34×
atomic_queue3.81×

16 MiB / batch

TwoBufQueue1.40×
ReaderWriterQueue4.47×
rigtorp::SPSCQueue3.27×
atomic_queue7.68×
Ratio = business cost ÷ 13 ns; lower is better. 1.00× is normal cost. Full report and parameters

Normal business work costs about 13 ns/call on this host. After a 16 MiB batch, TwoBufQueue costs 18.2 ns/call, ReaderWriterQueue 58.2, rigtorp 42.5 and atomic_queue 99.8. Full experiment and parameters

These are separate experiments on Apple M3. Submission uses 256 MiB per TwoBufQueue layer and callback loop count 0. Business impact uses a 16 KiB producer pool, a 256 MiB consumer pool and callback loop count 64. Native queues use 256 MiB of message storage. Three-run means, high QoS and no hard CPU affinity; see the reports for timing scope and limitations.

3. When does it fit?

3.1 Useful scenarios

If the questions in chapter 1 interest you, or you have encountered similar problems, consider TwoBufQueue.

In practical terms: the producer runs on the hot-path thread and the consumer runs on a supporting thread. You want the hot path to remain stable and efficient while letting the producer send messages at high speed, as in low-latency applications such as robotics, sensors and quantitative trading.

3.2 Tradeoffs to consider

Two-layer transfer adds copying and background work. The single-layer queues mentioned earlier may be a better fit when individual send-to-receive time matters most, or when traffic and processing are light. Also, the producer and consumer threads must not be pinned to the same CPU core; otherwise, two-layer buffering effectively behaves like single-layer buffering.

Detailed benefits, tradeoffs and design comparison

4. Get started

4.1 Minimal example

Submit through a Writer and receive messages in the on_record callback.

C++
#include <TwoBufQueue.hpp>
#include <cstdio>
#include <string_view>

int main() {
    //---------------1. Initialization--------------------------------------
    auto writer = TwoBufQueue::make_writer<TwoBufQueue::Writer<>>();
    // Callback that receives each message.
    auto on_record = [](std::int32_t, const std::byte* data,
                        std::size_t size) noexcept {
        std::printf("%.*s\n", static_cast<int>(size),
                    reinterpret_cast<const char*>(data));
    };
    TwoBufQueue::TwoBufQueue queue(on_record, writer);

    //---------------2. Producer writes-------------------------------------
    bool ok = true;
    for (std::string_view text : {"hello", "telemetry", "goodbye"})
        ok &= writer.write_mem(text.data(), static_cast<std::uint16_t>(text.size()));
    writer.flush();   // Publish and notify; does not wait for delivery.

    //---------------3. Finish----------------------------------------------
    writer.finish();  // Stop production and hand off the tail block.
    queue.stop();     // Drain copying and callbacks before leaving this scope.

    //---------------4. Check the result------------------------------------
    return ok && queue.error_code() == TwoBufQueue::ReadError::none ? 0 : 1;
}

4.2 Build and integrate

Requires CMake 3.20+, a C++20 compiler and thread support. No third-party runtime dependency. macOS requires 14.4+ and a supporting SDK; detailed tests additionally require Python 3.9+.

Shell
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release --parallel
ctest --test-dir build -C Release --output-on-failure

Link the CMake target TwoBufQueue::TwoBufQueue. Installation and integration · More examples

5. Full documentation

What you needStart here
Two-layer design, use cases and roadmapFull guide README_DETAIL
Writing, publication, callbacks, threads and parametersAPI guide
Session, multiple producers and synchronous callbacksExamples
Measurement scope, parameters and reproductionBenchmark methodology
Application logging through the queueAsync logging guide

Apache-2.0. License · Contributing