Classes do not share, and the pool says how to size them (L0 substrate)

pool_source_t recycles through segregated free lists keyed on the whole (bytes, align) pair. So a freed 64-byte block cannot serve a 128-byte request, and a block carved on an 8-byte boundary is never handed back out to an align=64 caller. That is the price of the header-free scheme, and the reason the class span is a sizing decision a deployment has to make rather than a detail it can ignore.

What to notice

  • The limitation is measured, and it still wins. Replaying 70,937 recorded allocations from the host suite — 12 distinct sizes, three of which cover 99.8 % of them — this policy needed 26,176 B against a 23,552 B peak-live floor (+11.1 %), where first-fit with boundary-tag coalescing needed 27,448 B (+16.5 %) and TLSF 28,440 B (+20.8 %). Re-running with the header zeroed decomposes the gap as 1,088 B of external fragmentation against only 184 B of header: splitting a remainder under geometric growth rarely produces the size of the next request (ADR-0067). Note what that says about the usual argument for a header-free pool — here the header is worth 0.7 % of the difference, so it is not the reason to choose this shape.

  • Alignment is part of the key, not folded away. It has to be: a block carved on an 8-byte boundary cannot be promised to a caller who asked for 64. The example allocates at both and watches the two classes stay separate.

  • classes_used() is the number to size the span against. It reports the distinct shapes actually seen, which is a measurement a deployment can take on its own workload instead of guessing.

  • overflowed() is the alarm, and a non-zero value is safe but lossy. When the class table is full, a released block of an unfiled shape stays carved rather than being written anywhere unsafe — bounded and correct, never corrupt — and the loss is counted. The example deliberately injects a two-slot span, uses three shapes, and reads the counter.

  • An overflowed table degrades recycling; it does not break the source. The recorded classes keep recycling and the slab keeps serving. Treat a non-zero overflowed() as “the injected span is too small”, not as a fault.

  • Nothing here is conditional — the target builds and runs under every CI leg.

Source

 1/*
 2 * SPDX-License-Identifier: Apache-2.0
 3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
 4 */
 5
 6/**
 7 * @file
 8 * @brief ONE CONCEPT — the pool's classes DO NOT SHARE, and it tells you how to size them.
 9 *
10 * `tr::mem::pool_source_t` recycles through segregated exact-size free lists keyed on the
11 * whole `(bytes, align)` pair. A freed 64-byte block therefore cannot serve a 128-byte
12 * request, and a block carved for `align=8` is never handed back out for `align=64`. That
13 * limitation is the price of the header-free scheme and it is measured, not assumed: replaying
14 * 70,937 recorded allocations, this policy needed 26,176 B against a 23,552 B peak-live floor
15 * (+11.1 %), where first-fit-with-coalescing needed 27,448 B (+16.5 %) — splitting a remainder
16 * under geometric growth rarely produces the size of the next request (ADR-0067).
17 *
18 * So the class SPAN is a sizing decision, and the source hands the deployer both instruments
19 * for making it: `classes_used()` is the number of distinct shapes actually seen, and
20 * `overflowed()` counts blocks that could not be filed because the span was too small. A
21 * non-zero `overflowed()` never corrupts and never leaks outside the slab — the block simply
22 * stays carved — but it does mean the injected span is undersized.
23 *
24 * Runs under ctest as `example_mem_size_classes`; returns non-zero on any failed check.
25 */
26
27#include <array>
28#include <cstddef>
29#include <cstdint>
30#include <cstdio>
31
32#include "libtracer/mem_source.hpp"
33
34namespace {
35
36/** @brief Report expectation @p what and record a failure on @p ok. */
37void check(bool& ok, bool cond, const char* what) {
38    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
39    ok = ok && cond;
40}
41
42}  // namespace
43
44int main() {
45    bool ok = true;
46    alignas(std::max_align_t) std::array<std::byte, 512> slab{};
47    std::array<tr::mem::size_class_t, 2> classes{};  // deliberately too few — see below
48    tr::mem::pool_source_t<> pool{slab, classes};
49
50    // Size is part of the key: a freed block of one size cannot serve another.
51    void* const small = pool.try_alloc(64, 8);
52    pool.release(small, 64, 8);
53    void* const large = pool.try_alloc(128, 8);
54    check(ok, large != nullptr && large != small, "a freed 64 B block does not serve a 128 B ask");
55    check(ok, pool.used() == 192, "both shapes are carved — that is the +11.1 % this policy pays");
56
57    // Alignment is part of the key too, for the same reason: a block carved on an 8-byte
58    // boundary cannot be promised to an align=64 caller.
59    void* const wide = pool.try_alloc(32, 64);
60    check(ok, wide != nullptr && reinterpret_cast<std::uintptr_t>(wide) % 64 == 0,
61          "an over-aligned request is served, and is actually 64-aligned");
62    pool.release(wide, 32, 64);
63    void* const narrow = pool.try_alloc(32, 8);
64    check(ok, narrow != wide, "the freed align=64 block is a DIFFERENT class, so it is not reused");
65
66    // Two class slots were injected and this run has now seen three distinct shapes, so the
67    // third has nowhere to be filed. The block stays carved — bounded and safe — and is counted.
68    check(ok, pool.classes_used() == classes.size(), "the class table filled at its injected size");
69    pool.release(narrow, 32, 8);
70    std::printf("shapes seen: %zu class slots of %zu, %zu block(s) lost to overflow\n",
71                pool.classes_used(), classes.size(), pool.overflowed());
72    check(ok, pool.overflowed() == 1,
73          "a block of an unfiled shape is COUNTED as lost, not written somewhere unsafe");
74    check(ok, pool.try_alloc(64, 8) != nullptr,
75          "an overflowed table degrades the recycling; it does not break the source");
76    return ok ? 0 : 1;
77}

See also: backends · memory substrate reference · a long-lived seam has to recycle.