Rope scatter-gather (L1 views)

A rope_t chains several view_t windows into one logical payload without ever copying the bytes (ADR-0053). This example composes a 16-link rope over independently-allocated segments, checks the logical bytes match a hand-built reference, and contrasts the two ways to hand that payload to a consumer.

What to notice

  • Composition is chaining, not copying — 16 appends produce a 16-link rope; only the chain metadata (a single vector once the inline capacity spills) is ever allocated, never the payload bytes.

  • to_iovec() is the zero-copy egress path — one std::span per link, each pointing into its original segment; this is exactly what you hand to writev/sendmsg.

  • flatten() is the one copy you can measure — the single contiguous memcpy, taken only at a boundary that cannot scatter-gather. The RESULT line prints both so the trade-off is explicit.

  • It self-checkstotal_length, the iovec coverage, and byte-for-byte equality of the flattened bytes against the scatter-gather order are all asserted.

Note

The absolute nanoseconds come from whatever build ran (CI builds the examples in a debug configuration); treat them as a shape, not a spec number. The canonical, release-build, CI-published figures live on the performance page.

Source

  1/*
  2 * SPDX-License-Identifier: Apache-2.0
  3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
  4 */
  5
  6/**
  7 * @file
  8 * @brief L1 scatter-gather — compose a multi-link `rope_t` with zero byte copies,
  9 *        then measure the cost of scatter-gather egress vs. the one flatten copy.
 10 *
 11 * A `rope_t` (`docs/modules/views.md`, ADR-0053) chains several `view_t` windows
 12 * into one logical payload without ever copying the bytes. This example builds a
 13 * @p kLinks -link rope over independently-allocated segments, checks that the
 14 * logical bytes match a hand-built reference, and contrasts the two ways to hand
 15 * the payload to a consumer:
 16 *   - `to_iovec()` — one span per link, pointing INTO the original segments
 17 *     (zero copy — what you give `writev`/`sendmsg`);
 18 *   - `flatten(backend)` — the single contiguous copy, taken only at a boundary
 19 *     that cannot scatter-gather.
 20 *
 21 * The perf line is informational (RESULT), so CI never flakes on timing; the
 22 * self-checks guard correctness and return non-zero on any mismatch. Runs under
 23 * ctest as `example_rope_scatter`.
 24 */
 25
 26#include <chrono>
 27#include <cstddef>
 28#include <cstdint>
 29#include <cstdio>
 30#include <span>
 31#include <vector>
 32
 33#include "libtracer/tracer.hpp"
 34
 35namespace {
 36
 37using clock_t_ = std::chrono::steady_clock;
 38
 39/** @brief A heap segment of @p n bytes, each byte set to @p fill. */
 40tr::view::view_t chunk(std::size_t n, std::uint8_t fill) {
 41    tr::view::segment_ptr_t seg = tr::view::heap_alloc(n);
 42    for (std::size_t i = 0; i < n; ++i) seg->bytes[i] = static_cast<std::byte>(fill);
 43    return tr::view::view_t::over(std::move(seg));
 44}
 45
 46/** @brief Record a failed expectation on @p ok and report it. */
 47void check(bool& ok, bool cond, const char* what) {
 48    if (!cond) {
 49        std::printf("  [FAIL] %s\n", what);
 50        ok = false;
 51    }
 52}
 53
 54}  // namespace
 55
 56int main() {
 57    constexpr std::size_t kLinks = 16;   // chain length
 58    constexpr std::size_t kChunk = 256;  // bytes per link
 59    constexpr std::size_t kLogical = kLinks * kChunk;
 60
 61    // Compose the rope link by link. No bytes are copied — each append chains one
 62    // more window; only the third link spills the inline chain to a single heap
 63    // vector (the sole allocation), never the payload bytes.
 64    tr::view::rope_t rope;
 65    for (std::size_t i = 0; i < kLinks; ++i)
 66        rope.append(chunk(kChunk, static_cast<std::uint8_t>('A' + (i % 26))));
 67
 68    std::printf("composed a %zu-link rope, %zu logical bytes (zero payload copies)\n",
 69                rope.link_count(), rope.total_length());
 70
 71    bool ok = true;
 72    check(ok, rope.link_count() == kLinks, "rope has one link per appended view");
 73    check(ok, rope.total_length() == kLogical, "total_length sums the links");
 74
 75    // to_iovec(): one span per link, each pointing into its original segment.
 76    const std::vector<std::span<const std::byte>> iov = rope.to_iovec();
 77    check(ok, iov.size() == kLinks, "to_iovec yields one span per link");
 78    std::size_t iov_bytes = 0;
 79    for (const auto& s : iov) iov_bytes += s.size();
 80    check(ok, iov_bytes == kLogical, "the scatter-gather spans cover every logical byte");
 81
 82    // flatten(): the single contiguous copy. Its bytes must equal the walk order.
 83    const tr::view::view_t flat = rope.flatten();
 84    const auto fb = flat.bytes();
 85    check(ok, fb.size() == kLogical, "flattened view is the full logical length");
 86    bool contents_match = true;
 87    std::size_t p = 0;
 88    for (const auto& s : iov)
 89        for (const std::byte b : s)
 90            if (p >= fb.size() || fb[p++] != b) {
 91                contents_match = false;
 92                break;
 93            }
 94    check(ok, contents_match, "flatten reproduces the scatter-gather byte order exactly");
 95
 96    // --- perf: scatter-gather (zero copy) vs. the one flatten copy ---
 97    constexpr int kIters = 20000;
 98    std::size_t sink = 0;  // defeat dead-code elimination
 99
100    auto t0 = clock_t_::now();
101    for (int i = 0; i < kIters; ++i) sink += rope.to_iovec().size();
102    auto t1 = clock_t_::now();
103    for (int i = 0; i < kIters; ++i) sink += rope.flatten().bytes().size();
104    auto t2 = clock_t_::now();
105
106    const double iovec_ns = std::chrono::duration<double, std::nano>(t1 - t0).count() / kIters;
107    const double flat_ns = std::chrono::duration<double, std::nano>(t2 - t1).count() / kIters;
108    const double flat_gbps = (double(kLogical) / (flat_ns * 1e-9)) / 1e9;
109    std::printf(
110        "RESULT rope_scatter links=%zu logical_bytes=%zu iovec_ns=%.0f flatten_ns=%.0f "
111        "flatten_GBps=%.2f (sink=%zu)\n",
112        kLinks, kLogical, iovec_ns, flat_ns, flat_gbps, sink);
113    std::printf(
114        "scatter-gather is O(links) pointer work; flatten is the one memcpy you pay "
115        "only at a non-scatter boundary.\n");
116
117    std::printf("%s\n", ok ? "rope scatter-gather OK" : "rope scatter-gather FAILED");
118    return ok ? 0 : 1;
119}

See also: views module · views & ownership reference.