memory_pool: A C++ Memory Pool That Chooses Clarity Over Cleverness
A tiered power-of-two allocator with a simple API, thread-safe blocks, and a surprisingly linear free path that makes its trade-offs easy to see.
- The project’s main appeal is not raw allocator speed, but the way it makes size-class routing easy to understand from the caller’s side.
- Allocation is cheap because each block checks reusable chunks first, then only falls back to fresh memory when the free list is empty.
- The interesting trade-off is on free, where ownership is recovered by searching lists instead of using heavier metadata tricks.
- This is less a production allocator replacement than a compact systems reference that shows how far readable C++11 can go.
The first thing to understand about Bruce-Lee-LY/memory_pool is not what it speeds up. It is what it makes easy. Callers get a clean allocator API, but the project pays for that convenience with a lookup strategy that is more readable than ruthless.
A high-performance C++ memory pool with lock-free design.
The hidden cost of a friendly API
Most memory pools advertise the same promise: fewer calls to the system allocator, less fragmentation, faster reuse. This repo keeps that promise, but it does something more interesting. It hides the size-class machinery well enough that the caller does not have to think about it, then accepts extra work inside Free to preserve that simplicity.
| Dimension | memory_pool | What readers may expect from a faster allocator |
|---|---|---|
| API shape | Single pool entry point with size routing hidden underneath | Callers manage size classes or headers themselves |
| Allocation path | Checks a free list first, then allocates if needed | Always hits a complex metadata path |
| Free path | Searches used chunks, then may search blocks | Constant-time ownership recovery |
| Bookkeeping style | Readable lists and block routing | Pointer tagging, hashes, or header math |
| Design goal | Clarity plus reuse | Maximum bookkeeping speed |
That trade-off is the article’s spine. The project is not trying to be the cleverest allocator on the page. It is trying to be legible, and it is willing to spend cycles to stay legible.
The allocator is really three layers
size_t MemoryPool::get_block_idx(size_t bytes) {
if (bytes <= 256) return 0;
return floor_log2((bytes - 1) >> 8) + 1;
}
void* MemoryPool::Alloc(size_t bytes) {
return m_blocks[get_block_idx(bytes)]->Alloc(bytes);
}
The routing logic is compact. A request lands in one of 21 blocks, each block representing a power-of-two size class from 256 bytes upward. That means the caller sees a simple allocator, while the pool quietly turns size into an index.
This is why the project feels small but not simplistic. The code is built from ordinary C++11 pieces, but the combination gives it a clean mental model: pool, block, chunk.
Why the pool feels fast on alloc
The fast path is straightforward. A block checks m_free_chunks first. If a chunk is available, it gets reused. Only when the free list is empty does the block create a fresh chunk.
void* MemoryBlock::Alloc(size_t bytes) {
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_free_chunks.empty()) {
auto chunk = m_free_chunks.front();
m_free_chunks.pop_front();
m_use_chunks.push_back(chunk);
return chunk->Get();
}
auto chunk = new MemoryChunk(m_chunk_bytes, bytes);
m_use_chunks.push_back(chunk);
return chunk->Get();
}
That is the classic object-pool pattern, and it is the reason the repo is useful for repetitive allocation patterns. If the workload keeps asking for the same kinds of chunks, the allocator stops paying the full setup cost again and again.
The interesting part is not that this works. It is that the code stays readable while doing it.
Why free is the interesting trade-off
Here is the part that changes the story. MemoryBlock::Free does not instantly know which chunk owns the pointer. It scans the used list, finds the match, then moves that chunk back to the free list. At the pool level, the code may scan blocks to find the right owner first.
void MemoryBlock::Free(void *ptr) {
std::lock_guard<std::mutex> lock(m_mutex);
for (auto iter = m_use_chunks.begin(); iter != m_use_chunks.end(); ++iter) {
if ((*iter)->Get() == ptr) {
m_free_chunks.push_back(*iter);
m_use_chunks.erase(iter);
break;
}
}
}
That is not the usual performance story people expect from allocator code. Industrial allocators often lean on headers, hashes, or other metadata tricks to make ownership recovery constant time. This project chooses the cleaner path instead.
| Allocator | Alloc path | Free path | What it optimizes for |
|---|---|---|---|
| memory_pool | Route by size class, then reuse free chunks | Scan used chunks, then search blocks if needed | Readability and simple reuse |
| Boost.Pool | Pool-specific object management | Typically more direct, depending on pool type | General-purpose pooling flexibility |
| tcmalloc | Thread-local caching and central transfer | Fast ownership and cache management | Throughput under concurrency |
| jemalloc | Arena and bin based allocation | Metadata-rich deallocation | Fragmentation control and scale |
The key point is asymmetry. This repo makes alloc feel cheap because the pool owns the routing. It makes free interesting because the caller never had to carry the size-class label around.
Thread safety without overcomplication
The synchronization story is intentionally plain. Each block uses a std::mutex and std::lock_guard around its lists, which keeps the code easy to reason about in concurrent paths. That is not the same as lock-free design, but it is much easier to audit.
std::mutex m_mutex;
void* MemoryBlock::Alloc(size_t bytes) {
std::lock_guard<std::mutex> lock(m_mutex);
// reuse or create chunk
}
void MemoryBlock::Free(void *ptr) {
std::lock_guard<std::mutex> lock(m_mutex);
// find chunk and recycle it
}
For a systems repo, that matters. The code does not try to win a benchmark contest with exotic tricks. It tries to keep the concurrency model visible.
Where this fits in the allocator landscape
Placed next to Boost.Pool, tcmalloc, and jemalloc, this project sits at the small, readable end of the spectrum. It is not trying to replace a production allocator used across an entire stack. It is offering a compact model of how pooled allocation can work.
| Project | Best at | Complexity footprint | Best fit |
|---|---|---|---|
| memory_pool | Readable block routing and reuse | Small | Learning, internal tools, controlled workloads |
| Boost.Pool | Flexible pooling inside the Boost ecosystem | Medium | General C++ applications |
| tcmalloc | High-throughput concurrent allocation | Large | Latency-sensitive services |
| jemalloc | Fragmentation control and scaling | Large | Systems that need a hardened allocator |
That is not a weakness. It is a scope decision. A finished, understandable systems repo can be more useful than a sprawling one when you want to study the shape of a solution instead of inherit a whole allocator stack.
The value of a finished, readable systems repo
The strongest argument for memory_pool is that it looks complete. The structure is tidy, the C++11 idioms are modern, and the scope is clear. You can trace a request from the public API down to the raw chunk without needing to reverse-engineer a maze of abstractions.
Supports both fixed-size and variable-size memory allocations.
That makes the repo a good reference implementation. It is not pretending to be the final word on memory allocation. It is showing one disciplined way to make a small allocator understandable, thread-aware, and useful.