Second tagged release of pbr-cpp-memory-pool. The Milestone 1 stubs that returned NULL / no-op are replaced with the real, O(1), Valgrind-clean, benchmark-backed implementation of the fixed-block memory pool described in the spec. The C++ surface gains a move-only RAII Pool wrapper, a static Pool::make Factory Method, and a fluent PoolBuilder. The library is now usable in production code paths, not just linkable as a build skeleton.
memory_pool_create(size_t block_size, size_t block_count) validates the three ADR-0009 §2 preconditions (block_size > 0, block_size >= sizeof(void*), multiple of alignof(std::max_align_t)), enforces block_count > 0 and the size_t-overflow guard from §3, obtains the over-aligned contiguous backing via ::operator new(total, std::align_val_t{alignof(std::max_align_t)}) (with std::bad_alloc caught at the C ABI boundary), and initialises the implicit free list in ascending address order. Every failure path returns NULL; the implementation never silently rounds up.
memory_pool_alloc(memory_pool_t*) is a constant-time pop of the implicit free-list head — three loads, one store, zero branches on the hot path. Returns NULL on a null pool or an exhausted pool (fixed mode per ADR-0009 §7; dynamic growth on exhaustion arrives in Milestone 5).
memory_pool_free(memory_pool_t*, void*) is the symmetric constant-time push. Per ADR-0012, the function additionally runs an O(1) range + alignment check against the pool's backing extents and silently no-ops on foreign / out-of-range / misaligned pointers. The comparison uses std::uintptr_t arithmetic to avoid [expr.rel]/4 unspecified behaviour on cross-allocation pointer comparison. Double-free detection (in-range, aligned, already-on-free-list pointer) stays UB and is deferred to Milestone 6's Decorator instrumentation.
memory_pool_destroy(memory_pool_t*) releases every byte of pre-allocated backing storage back to the OS via the matching aligned ::operator delete, then frees the metadata struct. Passing NULL is a defined no-op. Valgrind reports ERROR SUMMARY: 0 errors from 0 contexts on every code path the demonstrative test exercises (spec §6.2).
it::d4np::memorypool::Pool is a move-only RAII owner of memory_pool_t* per ADR-0010 — copy deleted, single memory_pool_t* handle_ data member, sizeof(Pool) == sizeof(void*), ctor calls memory_pool_create, dtor calls memory_pool_destroy, move leaves the source in a valid empty state. The Pimpl boundary is closed by struct memory_pool being defined exclusively in memory_pool.cpp (the C handle is the Impl; no separate Pool::Impl struct).
static std::optional<Pool> Pool::make(block_size, block_count) is the Factory Method per ADR-0011 §1 — engaged optional on successful construction, std::nullopt on any precondition failure or backing-storage OOM. Cleaner success/failure signal than the ctor's silent-empty-state path (which is preserved for backward compatibility). Marked [[nodiscard]].
class PoolBuilder is the fluent Builder per ADR-0011 §2 — chainable .with_block_size(...) / .with_block_count(...) setters returning *this by reference (noexcept), and a const .build() returning std::optional<Pool> via Pool::make. Const build() enables reusing the same configured builder for multiple identically-configured pools (useful for tests and for benchmark setup). Default-constructed or partially-configured builders return std::nullopt — fail-loud for forgotten configuration.
if (auto pool = it::d4np::memorypool::PoolBuilder{}
.with_block_size(64)
.with_block_count(1024)
.build()) {
void* block = pool->allocate();
// ... use block ...
pool->deallocate(block);
}Per ADR-0015, the pool's external memory footprint is gated at compile time and at runtime:
- Per-block external metadata: 0 bytes by construction. The implicit free list (ADR-0009 §1) stores next-free links inside the free blocks' own first
sizeof(void*)bytes; allocated blocks carry no overhead. - Per-pool metadata: 40 bytes on every Tier-1 64-bit host (the five ADR-0009 §6 fields, no padding) — capped at 128 bytes by
static_assert(sizeof(memory_pool) <= 128U, ...)that fires on every cell of the 14-cell CI build matrix. - O(1) in
block_count: a pool with one million blocks reports the same number as a pool with one. The runtime test pulls a 1024-block pool and a 1,000,000-block pool and asserts they return identical values frommemory_pool_metadata_bytes.
The new public C function size_t memory_pool_metadata_bytes(const memory_pool_t* pool) exposes the runtime value (NULL-tolerant, returns 0 on NULL); the C++ wrapper gains a [[nodiscard]] std::size_t Pool::metadata_bytes() const noexcept forwarder. Both are exercised under the M1.10 ANSI C -std=c89 -pedantic -Werror and -std=c99 verification jobs.
pool_vs_malloc_bench under src/bench/cpp/it/d4np/memorypool/ implements the spec §6.3 benchmark contract — 1,000,000 iterations × 10 repeats per scenario, first repeat dropped as warm-up. Two scenarios: bulk (allocate N then free N) and interleaved (allocate + immediate free × N). Statistical summary (min / median / mean / max / stddev) plus a headline malloc_median / pool_median ratio per region. Per-iteration volatile byte write + a portable do_not_optimize barrier defeat Release-mode dead-code elimination identically on both allocators. Methodology fully recorded in ADR-0014 with six rejected alternatives.
Canonical numbers from the maintainer's Intel Core i5-6600K Skylake @ 3.5 GHz workstation with MSVC 19.51 Release (full report at docs/bench/v0.2.0-windows-msvc-x64.md):
| Scenario | median malloc (ns/op) |
median pool (ns/op) |
malloc / pool |
|---|---|---|---|
| bulk-alloc | 75.5 | 6.9 | 11.02 × |
| bulk-free | 44.5 | 8.3 | 5.35 × |
| interleaved | 49.9 | 11.2 | 4.45 × |
The interleaved-scenario ratio (4.45 ×) is the most defensible headline number for a single-threaded recycling workload — both allocators reach steady state, the working set fits in L1, and stddev is sub-1-ns/op on both sides. The bench-smoke CI cell builds and runs the binary on every PR; numeric thresholds are deliberately not asserted (ADR-0014 §8 — shared GHA runner noise would produce flaky red).
Three new CI cells gate master:
valgrind(Ubuntu 24.04) — runs the spec §6.2 demonstrative test undervalgrind --leak-check=full --show-leak-kinds=all --errors-for-leak-kinds=definite,indirect --error-exitcode=1and greps the output for the literal spec success criterionERROR SUMMARY: 0 errors from 0 contexts. The C → C++ structural substitution required by the C++17 implementation (ADR-0009 §1) is documented in the directory README.bench-smoke(Ubuntu 24.04) — builds the bench binary with the newbenchpreset (Release + benchmarks ON + tests OFF) and runs it briefly to prove it compiles, links, and exits 0.- The existing 14-cell build matrix gains the per-pool static_assert (ADR-0015 §3 budget gate) plus 20 new doctest
TEST_CASEs covering precondition violations, exhaustion + re-allocation, distinct-and-aligned pointer guarantees, foreign-pointer scenarios, Factory + Builder happy + failure paths, the metadata-bytes accessor invariants, and the C++ wrapper forwarders.
Eight ADRs accepted in Milestone 2, taking the running total from 7 to 15:
- ADR-0008 — Delegate annotated-tag creation and push to the agent. Amends ADR-0004 §6. The maintainer retains whether (PR review/merge) and when (Publish on the draft release).
- ADR-0009 — Free-list layout,
block_sizeconstraints, and alignment guarantee. - ADR-0010 — RAII
Poolwrapper and Pimpl across the C/C++ boundary. - ADR-0011 — Factory Method (
Pool::make) and Builder (PoolBuilder) for pool construction. - ADR-0012 — Foreign-pointer and out-of-range pointer policy in
memory_pool_free. - ADR-0013 — Doxygen for the API contract, Markdown for the narrative corpus (retroactive formalisation).
- ADR-0014 — Microbenchmark methodology (pool vs. malloc).
- ADR-0015 — Metadata-overhead budget and introspection contract.
Each ADR records 5–7 rejected alternatives so the decision rationale stays auditable from a single file.
Four patterns flip from Planned to Implemented in docs/patterns/README.md: RAII (the Pool wrapper), Pimpl (the C-style boundary), Factory Method (Pool::make), and Builder (PoolBuilder). Adapter, Iterator, Strategy, Template Method, Composite, Decorator, and Observer remain Planned against their respective Milestone 3–6 items.
Of the 15 traceability rows from the spec to the roadmap, nine flip to ✅ in this release, one moves from ⏳ to 🚧 (single-threaded coverage of the benchmark; full ✅ at M4.5), and three remain ⏳ (return policy on the C++ side / dynamic growth, thread safety, and the M4.5 concurrent re-run that re-uses the bench binary).
Concretely flipping to ✅ in v0.2.0: §2.1, §2.3, §3.1, §3.2, §4, §5 (create / alloc / free / destroy), §6.1, §6.2. Now at 🚧: §6.3. Still ⏳: §2.2 (partially), §2.4, §6.3 (concurrent half).
TypedPool<T>template, allocator-aware adapter, and thestd::bad_alloc-translating exception policy at the C++ boundary — Milestone 3 →v0.3.0.- Thread safety — Milestone 4 →
v0.4.0. The current implementation is single-threaded; concurrent use produces a data race. - Dynamic growth on exhaustion — Milestone 5 →
v0.5.0.memory_pool_allocreturnsNULLon exhaustion in fixed mode. - Instrumented / observable variants (Decorator pattern, statistics, double-free detection) — Milestone 6 →
v0.6.0. - Doxygen-rendered API site, install / packaging layout (vcpkg, Conan) — Milestone 7 →
v1.0.0.
Each platform tarball produced by release.yml contains the public headers under include/it/d4np/memorypool/, the static archive under lib/, and the top-level LICENSE, README.md, CHANGELOG.md. SHA-256 checksums of every attached artifact live in SHA256SUMS:
sha256sum --check SHA256SUMSThe release pipeline re-runs the full CI matrix against the tagged commit — a green release workflow run is the canonical "the release is reproducible from a cold runner" signal.
cmake --preset release
cmake --build --preset release
ctest --preset releaseMinimal C consumer:
#include <it/d4np/memorypool/memory_pool.h>
int main(void) {
memory_pool_t* pool = memory_pool_create(64, 1024);
void* block = memory_pool_alloc(pool);
/* ... use block ... */
memory_pool_free(pool, block);
memory_pool_destroy(pool);
return 0;
}Minimal C++ consumer:
#include <it/d4np/memorypool/memory_pool.hpp>
int main() {
using namespace it::d4np::memorypool;
if (auto pool = Pool::make(64, 1024)) {
void* block = pool->allocate();
// ... use block ...
pool->deallocate(block);
}
}- Changelog entry:
CHANGELOG.md—[0.2.0] - Milestone plan:
ROADMAP.md— Milestone 2 - Specification:
docs/specs/01_spec_cpp_memory_pool.md - Benchmark report:
docs/bench/v0.2.0-windows-msvc-x64.md - Local Build Guide:
docs/development/local-build.md