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 — onestd::spanper link, each pointing into its original segment; this is exactly what you hand towritev/sendmsg.flatten()is the one copy you can measure — the single contiguousmemcpy, taken only at a boundary that cannot scatter-gather. TheRESULTline prints both so the trade-off is explicit.It self-checks —
total_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.