
C++ Experiments & Internals — Part 1: Fixed Size Memory Pool Allocator
Part 1 of 1
Fixed-Size Memory Pool Allocator — Detailed Explanation
This document explains how your allocator in allocator.cpp works, why each part exists, and what tradeoffs it has.
1) What problem this allocator solves
General-purpose allocators (new, malloc) are flexible, but they can be slower and may fragment memory over time.
A fixed-size pool allocator is optimized for cases where:
- Many objects have the same size
- Allocation/free happens frequently
- Predictable speed is important
Core idea
Reserve one big memory block once, split it into equal chunks, and reuse chunks.
pool_alloc()gives one free chunkpool_free()returns a chunk back
Both operations are O(1).
2) Data structure used in your code
In allocator.cpp, the allocator state is:
memory: start address of the full poolfree_head: head of free-listchunk_size: real chunk size used internallychunk_count: total number of chunksfree_count: how many are currently free
The free-list is intrusive: each free chunk stores a pointer to the next free chunk in its own first bytes.
So no external array (free_list[]) is needed.
3) Why alignment is needed (kAlignment)
You have:
constexpr size_t kAlignment = alignof(std::max_align_t);
What this means
std::max_align_t is a type whose alignment is at least as strict as any scalar type on the platform.
So alignof(std::max_align_t) gives a safe "maximum normal alignment".
Why used here
If chunk starts are not aligned correctly, writing certain types into them can cause:
- Undefined behavior
- Crashes on strict architectures
- Slow unaligned accesses
Using this alignment ensures chunk starts are broadly safe for common objects.
4) What align_up() does
Function in allocator.cpp:
size_t align_up(size_t value, size_t alignment)
Purpose
Rounds value up to the next multiple of alignment.
Mathematically:
aligned = ceil(value / alignment) * alignment
Your bitwise implementation does the same efficiently (for power-of-two alignment, which this is).
Example
If requested_chunk_size = 30 and alignment = 16, then align_up(30, 16) = 32.
5) pool_init() step-by-step
pool_init(PoolAllocator* allocator, size_t requested_chunk_size, size_t chunk_count)
Steps
- Validate inputs (not null, sizes not zero)
- Compute
actual_chunk_size- At least
sizeof(void*)(needed because free chunks store next-pointer) - Aligned to
kAlignment
- At least
- Allocate one large block:
actual_chunk_size * chunk_count
- Save allocator metadata
- Build intrusive free-list:
- chunk 0 points to chunk 1
- chunk 1 points to chunk 2
- ...
- last chunk points to
nullptr
- Set
free_headto first chunk
After this, all chunks are free.
6) pool_alloc() step-by-step
pool_alloc(PoolAllocator* allocator)
Steps
- If allocator invalid or no free chunk (
free_head == nullptr), returnnullptr - Save current
free_headas result chunk - Move
free_headto next chunk (*reinterpret_cast<void**>(chunk)) - Decrement
free_count - Return chunk
This is a classic stack pop on singly linked list.
7) pool_free() step-by-step
pool_free(PoolAllocator* allocator, void* ptr)
Steps
- Reject null args
- Validate that
ptrbelongs to this pool and is chunk-aligned usingpointer_belongs_to_pool() - Write old
free_headintoptr(as next pointer) - Set
free_head = ptr - Increment
free_count
This is a stack push.
8) How pointer validation works
pointer_belongs_to_pool() checks:
- Allocator and pointer are non-null
ptrin range[base, end)(ptr - base) % chunk_size == 0(exact chunk boundary)
This avoids freeing random pointers not produced by this pool.
9) pool_deinit()
pool_deinit(PoolAllocator* allocator):
- Frees
memory - Resets all fields to safe defaults
After this, allocator can't be used unless re-initialized.
10) Complexity and memory cost
Time complexity
pool_init: $O(n)$ (build free-list)pool_alloc: $O(1)$pool_free: $O(1)$pool_deinit: $O(1)$
Extra memory overhead
- No side array
- Only allocator metadata + one pointer stored inside each free chunk
11) Important caveats
- No double-free detection currently
- Freeing same pointer twice can corrupt free-list
- Not thread-safe
- Needs lock or lock-free design for multithreading
- Fixed-size only
- Each allocation is exactly one chunk
- Object lifetime
- In C++, if storing non-trivial objects, manage constructors/destructors properly (placement new / explicit destructor)
12) Suggested improvements (optional)
- Add debug mode bitmap to detect double-free
- Add
pool_owns(ptr)public helper - Add stats (
peak_used, alloc/free counters) - Add typed wrapper template, e.g.
Pool<T> - Add thread-safe variant (
std::mutex)
13) Mental model summary
Think of free chunks as a linked stack:
alloc= pop topfree= push top
Because all chunks are equal size and preallocated, operations stay fast and predictable.