Status — reconciled with the as-built system. This document was originally a short greenfield brief. The library has since shipped (public API frozen at
v1.0.0) and every non-trivial decision is recorded in an Architecture Decision Record. Each requirement below now cross-links the ADR(s) that realize it; Section 7 maps the whole specification to its ADRs and lists the explicitly deferred items.
Many high-performance systems (e.g. graphics engines, financial trading servers, databases) suffer from memory fragmentation and the overhead generated by frequent malloc/free or new/delete calls.
This component provides a custom memory allocator (Memory Pool) that pre-allocates a contiguous block of memory, handling the allocation and deallocation of fixed-size blocks in constant time
Fragmentation scope (precise claim). Fixed-size blocks eliminate external fragmentation within a pool: any free block satisfies any request, so free memory never degrades into unusable holes. The pool does not eliminate internal fragmentation — a caller that stores an object smaller than block_size wastes the difference. Choosing block_size to match the dominant object size is the caller's responsibility; the pool trades internal fragmentation for
The system pre-allocates a memory pool, specifying the block size (block_size) and the maximum number of blocks (block_count). The block_size * block_count product must not overflow size_t; the constructor rejects overflow and degenerate arguments. See ADR-0009.
Return a pointer to a free block in constant time. When the pool is exhausted the behaviour depends on the growth mode:
- Fixed pool (default): return
NULL(C) or throw (C++ wrapper, ADR-0016). - Dynamic pool: acquire a new, separate contiguous chunk and satisfy the request from it. The growth strategy is non-contiguous chunked expansion via a linked chunk list — the pool is not a single contiguous region once it has grown, and it never relocates: every previously returned pointer stays valid across growth (no
realloc-style invalidation). Growth is geometric (a configurable factor). Consequently, pointer-range validation (Section 6.1) is a multi-chunk membership test, not a single range check. This resolves the original brief's ambiguity in favour of chunk-linking over address-space reserve-and-commit. See ADR-0022, ADR-0023, ADR-0024.
Mark a previously allocated block available again in constant time, without returning it to the operating system immediately. Freeing NULL or a pointer foreign to the pool is a defined, silent no-op; see Section 5.3 and ADR-0012.
Concurrent access is governed by a compile-time policy knob (PBR_MEMORY_POOL_THREAD_SAFETY) selecting one of three strategies, so single-thread users pay nothing:
NONE— single-threaded fast path (intentionally racy; never share such a pool across threads);MUTEX— a mutex guards the free list;LOCKFREE— a Treiber-stack free list whose head is an ABA-tagged pointer ({pointer, tag}, the tag incremented on every CAS) so the classic ABA hazard on a LIFO free list cannot mis-link.
See ADR-0020. A contended, multi-thread benchmark exists (Section 6.3).
When the pool is destroyed, all pre-allocated memory (every chunk, for a grown pool) is returned to the operating system. Verified under Valgrind and the sanitizers (Section 6).
Free blocks carry zero per-block metadata — the free-list next-pointer is stored in-band inside the free block (Section 4). The only fixed cost is one pool-header struct plus, for dynamic pools, one small descriptor per chunk. The per-pool metadata budget is quantified and guarded in ADR-0015.
Written to standard ANSI C ABI (the four-function surface) with an idiomatic C++17 wrapper layer, no external runtime dependencies. Toolchain matrix and supported platforms: ADR-0005.
The pool manages free memory using a Free List implicit within the blocks themselves. When a block is free, its first bytes store a pointer to the next free block. This zeroes the metadata overhead for unused blocks.
+-------------------------------------------------------------------+
| Memory Pool |
+-------------------------------------------------------------------+
| [Block 1 (Free)] -> Holds a pointer to Block 2 |
| [Block 2 (Allocated)] -> Holds user data |
| [Block 3 (Free)] -> Holds a pointer to Block 4 |
| [Block 4 (Free)] -> Holds NULL (end of list) |
+-------------------------------------------------------------------+
Storing a next-pointer in-band imposes constraints, all enforced by the implementation (ADR-0009):
- Minimum block size:
block_size >= sizeof(void*)— the constructor rejects smaller sizes. - Alignment: every returned pointer is aligned to
alignof(std::max_align_t), uniformly. A weaker or caller-specified per-pool alignment was considered and deliberately rejected (the frozen C signature carries no alignment parameter; uniform strong alignment keepsTypedPool<T>a trivial cast). - Strict-aliasing-safe access: the in-band pointer is read/written through the canonical
*static_cast<void**>(slot)idiom, not a type-punning cast of the user type.
The intrusive-free-list design was chosen over a bitmap allocator; the rejected alternative and its rationale are recorded in ADR-0009.
The C4-model Component view below relates the public surface, the core engine (which lives behind the opaque memory_pool_t handle / Pimpl), and the operating-system backing store. It is authored in Mermaid per ADR-0041; a viewer without Mermaid support sees the equivalent source. Solid arrows are runtime call / ownership paths; dashed arrows are optional or diagnostic relationships. Bracketed tags name the realizing technology or the design pattern applied.
%% C4 model — Component level for the pbr-cpp-memory-pool library.
%% Authored as a Mermaid flowchart with C4 boundary subgraphs (ADR-0041).
flowchart TB
app["Consumer application<br/>[C or C++]<br/>fixed-size allocation on a hot path"]
subgraph lib["pbr-cpp-memory-pool — static library"]
direction TB
subgraph surface["Public surface"]
direction TB
capi["C API — memory_pool.h [extern C]<br/>frozen 4-function ABI + create_dynamic + introspection"]
cpp["Pool / PoolBuilder [C++ RAII]<br/>move-only owner; factory / builder construction"]
adapters["TypedPool<T> · PoolAllocator<T> [templates]<br/>type-safe and STL-allocator adapters"]
instr["InstrumentedPool [Decorator]<br/>counters + lifecycle Observers"]
diag["FreeListView [diagnostics, gated]<br/>read-only free-list iterator"]
end
subgraph core["Core engine — behind the Pimpl / C ABI"]
direction TB
state["memory_pool [Pimpl struct]<br/>backing · head · sizes · chunk list · grow factor"]
skel["alloc / free skeleton [Template Method]<br/>range-checks, dispatches to the sync policy"]
policy["Sync policy [Strategy, compile-time]<br/>SingleThreaded / Mutex / LockFree (ABA-tagged Treiber)"]
freelist["Intrusive free list [LIFO]<br/>in-band next-pointer, zero per-block metadata"]
chunks["Chunk list [Composite]<br/>non-contiguous geometric growth; pointers stay valid"]
end
end
os[("Backing storage<br/>[over-aligned operator new]<br/>alignof(max_align_t)")]
app -->|"C ABI"| capi
app -->|"idiomatic C++"| cpp
adapters -->|"compose"| cpp
instr -.->|"decorate"| cpp
diag -.->|"iterate (gated)"| freelist
cpp -->|"own via C core"| capi
capi --> state
state --> skel
skel -->|"pop / push"| policy
policy --> freelist
skel -->|"grow on exhaustion"| chunks
chunks -->|"seed a sub-list"| freelist
chunks --> os
state --> os
typedef struct memory_pool memory_pool_t;
// Initialize a fixed-capacity memory pool
memory_pool_t* memory_pool_create(size_t block_size, size_t block_count);
// Initialize a dynamically-growing pool (geometric chunk expansion; ADR-0022)
memory_pool_t* memory_pool_create_dynamic(size_t block_size, size_t block_count,
size_t growth_factor);
// Allocate a block from the pool (O(1)); NULL when a fixed pool is exhausted
void* memory_pool_alloc(memory_pool_t* pool);
// Release a block back into the pool (O(1))
void memory_pool_free(memory_pool_t* pool, void* block);
// Destroy the pool, releasing all memory (every chunk) back to the OS
void memory_pool_destroy(memory_pool_t* pool);The opaque memory_pool_t handle hides the implementation (Pimpl across the C/C++ boundary, ADR-0010).
A layered, idiomatic C++ surface built on the C core:
Pool— move-only RAII owner;PoolBuilder/ factory methods for construction (ADR-0010, ADR-0011).TypedPool<T>— type-safeconstruct/destroyover the pool (ADR-0017).PoolAllocator<T>— an STLAllocatoradapter so the pool can backstd::list,std::map, … (ADR-0018).PoolMemoryResource— astd::pmr::memory_resourceadapter so one pool can back anystd::pmrcontainer viastd::pmr::polymorphic_allocator, without the per-type rebind. Header-only and gated behindPBR_MEMORY_POOL_HAS_PMR(compiled where<memory_resource>is available) (ADR-0042).InstrumentedPool— a Decorator adding counters and lifecycle Observers (ADR-0025, ADR-0026).
-
Freeing
NULL— no-op (mirrors Cfree(NULL)). -
Freeing a foreign / out-of-range pointer — defined, silent no-op detected in
$O(1)$ ; no corruption (ADR-0012). - Double-free — a double-free of an in-range, currently-free block is not detected by the default build (accepted trade-off, documented in ADR-0012); opt-in detection is tracked as future hardening (Section 7).
- C++ exhaustion — throws per ADR-0016; exceptions never cross the C ABI.
Observability is provided two ways: an optional debug diagnostics surface (compiled out of release, gated by PBR_MEMORY_POOL_DIAGNOSTICS) and the production-usable InstrumentedPool decorator, which exposes live-block count, capacity, and a high-water mark (ADR-0015, ADR-0025).
Allocate all blocks until exhaustion; verify behaviour with null inputs and pointers foreign to the pool (defined no-op, Section 5.3). For dynamic pools, verify that pointers remain valid across a growth event and that foreign-pointer validation is a correct multi-chunk membership test.
gcc -g -O0 test_pool.c memory_pool.c -o test_pool
valgrind --leak-check=full --show-leak-kinds=all ./test_poolSuccess criterion: ERROR SUMMARY: 0 errors from 0 contexts and definitely lost: 0 bytes in 0 blocks.
memory_pool_alloc/free are compared against standard malloc/free. The methodology is fixed by ADR-0014: warm-up repeat discarded, min/median/mean/max/stddev reported (not a single wall-clock loop), anti-optimization barriers, disclosed compiler flags and host, per-release reports under docs/bench/, and a non-asserting CI smoke run. A concurrent scenario (T threads on a shared pool, MUTEX vs LOCKFREE) provides the contended baseline.
ASan, UBSan and TSan run via dedicated CMake presets; TSan covers the thread-safe configurations. All of the above is wired into a multi-OS CI matrix (warnings-as-errors, clang-tidy, Valgrind). A coverage-guided fuzz target is deferred (Section 7).
Every requirement above is realized and recorded. The table maps the spec to its ADRs.
| Spec area | Realized by |
|---|---|
| §2.2 growth model | ADR-0022, ADR-0023, ADR-0024 |
| §2.4 thread safety (mutex / lock-free + ABA tag) | ADR-0020 |
| §3.2 overhead budget & introspection | ADR-0015 |
| §4 free-list layout, constraints, alignment, intrusive-vs-bitmap | ADR-0009 |
| §4.2 component (C4) diagram & diagram tooling | ADR-0041 |
| §5.1–5.2 API, RAII, Pimpl, builder, typed pool, STL adapter | ADR-0010, ADR-0011, ADR-0017, ADR-0018 |
§5.2 std::pmr adapter (PoolMemoryResource) |
ADR-0042 |
| §5.3 error semantics | ADR-0012, ADR-0016 |
| §5.4 instrumentation / observers | ADR-0025, ADR-0026 |
| §6.3 benchmark methodology | ADR-0014 |
| Spec-compliance acceptance audit | ADR-0029 |
These are explicitly out of the current build and tracked as issues:
- Coverage-guided fuzzing harness (issue #108).
- Opt-in debug hardening — freed-block poisoning, canaries, free-list safe-linking; would also add double-free detection (issue #109).
- Benchmark extension — external baselines (jemalloc/tcmalloc) and p99 percentile reporting (issue #111).
The C4 component diagram of the pool internals, once deferred here, now ships in Section 4.2 (its tooling decision is ADR-0041).