Skip to content

Latest commit

 

History

History
155 lines (106 loc) · 13 KB

File metadata and controls

155 lines (106 loc) · 13 KB

pbr-cpp-memory-pool v0.2.0 — Milestone 2: Core Memory Pool (single-threaded MVP)

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.

What's in the box

Real implementation of the four spec §5 functions

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).

C++ RAII wrapper, Factory Method, and Builder

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);
}

Metadata-overhead budget

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 from memory_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.

Microbenchmark and reference numbers

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).

Verification gates

Three new CI cells gate master:

  • valgrind (Ubuntu 24.04) — runs the spec §6.2 demonstrative test under valgrind --leak-check=full --show-leak-kinds=all --errors-for-leak-kinds=definite,indirect --error-exitcode=1 and greps the output for the literal spec success criterion ERROR 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 new bench preset (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.

Architecture Decision Records

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_size constraints, and alignment guarantee.
  • ADR-0010 — RAII Pool wrapper 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.

Design-patterns catalogue

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.

Spec Coverage Map flips

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).

What this release does not contain

  • TypedPool<T> template, allocator-aware adapter, and the std::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_alloc returns NULL on 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.

Verifying the release

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 SHA256SUMS

The 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.

Build and use

cmake --preset release
cmake --build --preset release
ctest --preset release

Minimal 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);
    }
}

Links