|
| 1 | +--- |
| 2 | +title: "r23 / ch54 — Boost.Fiber — plan (internal)" |
| 3 | +published: false |
| 4 | +--- |
| 5 | + |
| 6 | +# r23 ch54 — example `54-boost-fiber` + chapter + diagrams |
| 7 | + |
| 8 | +Sixth chapter of Part 14. |
| 9 | + |
| 10 | +## Overlap check (done FIRST, as in r22) |
| 11 | + |
| 12 | +- **ch44** covers Go's GMP: M:N scheduling, work-stealing, netpoller, 2000 goroutines on |
| 13 | + ~12 threads. So ch54 must not re-derive M:N *generically* — it must show C++'s version |
| 14 | + and what is different about choosing it explicitly. |
| 15 | +- **ch53** measured a stackless coroutine frame (32 B) and the HALO split. |
| 16 | +- **ch27** owns the coroutine/epoll mechanism. |
| 17 | +- `grep -il fiber _docs/*.md` → only outline/ch49/ch50 forward-references, plus ch35 |
| 18 | + (unrelated, KVM). **No prior chapter covers stackful context switching.** |
| 19 | + |
| 20 | +## The spine: stackful is the whole difference |
| 21 | + |
| 22 | +ch53 established that a coroutine frame is 32 bytes and holds only what crosses a |
| 23 | +suspension. A fiber has a **real stack**. That single fact produces both the cost and |
| 24 | +the capability, and it completes Part 14's memory story: |
| 25 | + |
| 26 | +| paused computation | per-unit cost | measured in | |
| 27 | +| --- | --- | --- | |
| 28 | +| thread | **8388608 B** (8 MiB) | ch50 | |
| 29 | +| **fiber** | **131072 B** (128 KiB) | ch54 | |
| 30 | +| coroutine frame | **32 B** | ch53 | |
| 31 | + |
| 32 | +8 MiB → 128 KiB → 32 B. Roughly two orders of magnitude at each step, every number |
| 33 | +measured on this host rather than quoted. |
| 34 | + |
| 35 | +## Host audit (2026-08-20, Fedora 44, Boost 1.90.0, GCC 16.1.1) |
| 36 | + |
| 37 | +`boost-devel-1.90.0-7.fc44`; `libboost_fiber.so.1.90.0` and `libboost_context.so.1.90.0` |
| 38 | +present. **No Conan, no network.** Link: `-lboost_fiber -lboost_context -pthread`. |
| 39 | + |
| 40 | +| fact | measured | |
| 41 | +| --- | --- | |
| 42 | +| `ctx::stack_traits::default_size()` | **131072** (128 KiB) | |
| 43 | +| `ctx::stack_traits::minimum_size()` | **14528** | |
| 44 | +| `ctx::stack_traits::page_size()` | 4096 | |
| 45 | +| `is_unbounded()` | yes | |
| 46 | +| `fixedsize_stack(64 KiB).allocate().size` | **65536** — exactly what was asked | |
| 47 | +| `protected_fixedsize_stack(64 KiB)` | **69632** = 65536 + one 4096 guard page | |
| 48 | +| fiber runs on | the **calling thread's tid** (same as main) | |
| 49 | + |
| 50 | +### Guard page, both directions (measured) |
| 51 | + |
| 52 | +| case | result | |
| 53 | +| --- | --- | |
| 54 | +| recurse 5 frames on a 64 KiB protected stack | completes, exit 0 | |
| 55 | +| recurse 1000 frames (~4 MiB) on the same stack | **SIGSEGV, exit 139** | |
| 56 | + |
| 57 | +The guard page turns a stack overflow into an immediate fault instead of silent |
| 58 | +corruption of whatever is mapped next. This is the ch49 `oob` pattern one layer up. |
| 59 | + |
| 60 | +### M:N, and the scheduler is a choice (measured) |
| 61 | + |
| 62 | +Same 16 fibers, same work, two schedulers, tids collected with `gettid()`: |
| 63 | + |
| 64 | +| scheduler | distinct tids | |
| 65 | +| --- | --- | |
| 66 | +| default `round_robin` | **1** | |
| 67 | +| `algo::shared_work` (4 threads) | **4** | |
| 68 | + |
| 69 | +This is **ch49's grid applied to fibers**: identical concurrency structure, and whether |
| 70 | +any parallelism happens is one `use_scheduling_algorithm` call. Unlike Go, where the |
| 71 | +runtime decides, C++ makes you choose — and the choice is observable. |
| 72 | + |
| 73 | +### The capability the stack buys (measured) |
| 74 | + |
| 75 | +A fiber can suspend from **deep inside an ordinary call tree**: |
| 76 | + |
| 77 | +``` |
| 78 | +deep: yielding from depth=6 inside a plain function |
| 79 | +deep: resumed at depth=6 |
| 80 | +``` |
| 81 | + |
| 82 | +`leaf()` is not a coroutine and neither is anything above it. `co_await` is only valid |
| 83 | +in the body of a coroutine, so a stackless coroutine fundamentally cannot do this — every |
| 84 | +frame in the suspend path must itself be a coroutine ("function colouring"). The fiber's |
| 85 | +128 KiB is what buys the ability to suspend anywhere. |
| 86 | + |
| 87 | +## The core demo — `fiberdemo` (C++-only, `langs: [cpp]`) |
| 88 | + |
| 89 | +Subcommands: `stacks` (traits + three allocators), `versus` (the 3-way memory table), |
| 90 | +`deep` (suspend from a plain function 6 frames down), `roundrobin` / `sharedwork` (tid |
| 91 | +counts), `guard-ok` / `guard-overflow` (the fault), `versions`. |
| 92 | + |
| 93 | +`guard-overflow` dies of SIGSEGV **by design** — its own subcommand, invoked only under |
| 94 | +the harness's expectation of failure, exactly as ch50's `cancel-swallow` and ch51's |
| 95 | +`deadlock-naive` were. |
| 96 | + |
| 97 | +Digest continuity: `digest=0x481984990deee5ff`. |
| 98 | + |
| 99 | +## Gate tiers |
| 100 | + |
| 101 | +**Hard (A–F):** |
| 102 | +- A. digest identical across every non-faulting subcommand. |
| 103 | +- B. stacks: default is 131072; a 64 KiB request yields exactly 65536; the protected |
| 104 | + allocator adds exactly one page (69632 - 65536 == 4096 == `page_size()`). Gate the |
| 105 | + page-delta relationship, not the absolute default. |
| 106 | +- C. versus: fiber stack sits strictly between ch53's 32 B frame and ch50's 8388608 B |
| 107 | + thread stack, and both ratios are computed in the program. |
| 108 | +- D. deep: a plain non-coroutine function suspends and resumes at depth > 1. |
| 109 | +- E. M:N: `roundrobin` reports exactly 1 distinct tid; `sharedwork` reports > 1. Same |
| 110 | + fibers, so the scheduler is the only variable. |
| 111 | +- F. guard: in-budget completes (exit 0); overflow dies of a signal (exit >= 128), and |
| 112 | + the gate asserts the *signal*, never any output the faulting run produced. |
| 113 | + |
| 114 | +**Gated-if-present (G):** clang parity. |
| 115 | + |
| 116 | +**Not gated:** context-switch timings (ch39); the exact `minimum_size()`; anything |
| 117 | +requiring more than one core for correctness. |
| 118 | + |
| 119 | +## Risks |
| 120 | + |
| 121 | +- **`sharedwork` tid count is scheduler-dependent.** Measured 4/4 here, but gate `> 1` |
| 122 | + and never `== 4`; a loaded host could legitimately keep the work on fewer threads. |
| 123 | + Bounded retry if it comes back 1, as ch49's gate B did. |
| 124 | +- **`guard-overflow` must fault, not hang.** Recursion depth 1000 × ~4 KiB is ~4 MiB |
| 125 | + against a 64 KiB stack — 60x past it, so it cannot accidentally fit. Run under |
| 126 | + `timeout` anyway. |
| 127 | +- **Never assert on what the faulting run printed.** Same rule as ch49's `oob_unchecked`: |
| 128 | + the output after a stack overflow is undefined. Assert the exit signal only. |
| 129 | +- **`-O0` for the guard case**: an optimizing compiler can turn the recursion into a |
| 130 | + loop and never touch the guard page. Force `-O0` on that translation unit and say why. |
| 131 | +- **Do not re-derive ch44.** ch54 shows C++'s explicit scheduler choice; ch44 owns Go's |
| 132 | + automatic one. Cite it, contrast it, do not re-explain M:N from scratch. |
| 133 | + |
| 134 | +## Steps |
| 135 | + |
| 136 | +S1 mint + strip · S2 `cpp/src/fiberdemo.cpp` + CMakeLists · S3 `verify.lua` · S4 manifest · |
| 137 | +S5 transcripts · S6 `_docs/54-boost-fiber.md` · S7 2 diagrams + catalogue · S8 gate matrix · |
| 138 | +S9 PR, stop there. |
| 139 | + |
| 140 | +## Acceptance |
| 141 | + |
| 142 | +`verify.lua` PASS/FAIL 0 with E and F running for real; runner 1 passed; `validate.py` OK; |
| 143 | +every chapter code block verbatim; footer states Boost 1.90.0, the three stack numbers, |
| 144 | +the guard-page delta, and the two tid counts as measured. |
| 145 | + |
| 146 | +## Outcome (2026-08-20) |
| 147 | + |
| 148 | +All steps complete. `verify.lua` → **PASS 32 / FAIL 0** with gates E (migration), F (the |
| 149 | +guard-page fault) and G (clang parity) all running for real. Runner → 1 passed. |
| 150 | +`validate.py` → OK. Banned-words clean. **19/19 chapter code blocks verbatim**; all 7 |
| 151 | +`Chapter N` cross-references resolve. |
| 152 | + |
| 153 | +### The overlap check (second time doing this before planning, and it paid again) |
| 154 | + |
| 155 | +`grep -il fiber _docs/*.md` found only forward-references. ch44 owns Go's *automatic* |
| 156 | +M:N; ch54 shows C++'s *explicit* one and contrasts rather than re-explains. ch53 owns |
| 157 | +stackless; ch54 is the stackful counterpart and the two compose into the trilogy. |
| 158 | + |
| 159 | +### Measured, and worth keeping |
| 160 | + |
| 161 | +| paused computation | bytes | instrument | chapter | |
| 162 | +| --- | --- | --- | --- | |
| 163 | +| thread | 8388608 | `pthread_getattr_np` | ch50 | |
| 164 | +| fiber | 131072 | `stack_traits::default_size()` | ch54 | |
| 165 | +| coroutine frame | 32 | `operator new` in `promise_type` | ch53 | |
| 166 | + |
| 167 | +Three different instruments, three chapters, one ordering — and the ordering falls out of |
| 168 | +structure rather than benchmark noise. Both ratios (64, 4096) are computed in the program. |
| 169 | + |
| 170 | +Guard page: `fixedsize_stack(64 KiB)` → exactly 65536; `protected_fixedsize_stack(64 KiB)` |
| 171 | +→ 69632, delta **4096 == `page_size()`**. Overflow 60x past it → **exit 139 (SIGSEGV)**. |
| 172 | + |
| 173 | +Scheduler: same 16 fibers → `round_robin` **1 tid**, `shared_work` **4 tids**, stable at |
| 174 | +4/4 across five runs. |
| 175 | + |
| 176 | +Deep suspend: yielded and resumed at **depth 6** inside ordinary functions. |
| 177 | + |
| 178 | +### Deltas |
| 179 | + |
| 180 | +- **`-O0` forced on the translation unit**, and documented in `CMakeLists.txt` rather |
| 181 | + than left implicit. At `-O2` the recursion can become a loop and never reach the guard |
| 182 | + page, which would make `guard-overflow` exit 0 and silently stop demonstrating |
| 183 | + anything — ch49's optimizer lesson, one layer down. The `volatile` array is the same |
| 184 | + defence. |
| 185 | +- **Gate F rejects exit 124 explicitly.** `timeout(1)` returns 124 when it kills |
| 186 | + something, and a *hung* run must not pass a gate that means "faulted promptly". |
| 187 | +- **Guard delta gated against `page_size()`**, not a hardcoded 4096, so it holds on a |
| 188 | + host with different page sizes. |
| 189 | +- **`fiber_specific_ptr` header path corrected** before shipping: it is in |
| 190 | + `<boost/fiber/fss.hpp>`, not `fiber_specific_ptr.hpp`. Verified by grep rather than |
| 191 | + assumed. All three named-but-unexercised APIs were confirmed present on this host and |
| 192 | + the footer says they are named, not gated. |
0 commit comments