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.

7 min read View on GitHub More from Bruce-Lee-LY

A mechanical sorting machine routes one incoming request into a ladder of size bins that grow by powers of two. Some bins are packed with reusable parts, while another bin feeds new raw components into the system, showing how the allocator hides routing behind a simple call site.
One request enters, then gets routed into a power-of-two size class before the pool decides whether it can reuse memory or must ask the system for more.
Key Takeaways

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.

Bruce-Lee-LY, Author · memory_pool GitHub Repository

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.

Dimensionmemory_poolWhat readers may expect from a faster allocator
API shapeSingle pool entry point with size routing hidden underneathCallers manage size classes or headers themselves
Allocation pathChecks a free list first, then allocates if neededAlways hits a complex metadata path
Free pathSearches used chunks, then may search blocksConstant-time ownership recovery
Bookkeeping styleReadable lists and block routingPointer tagging, hashes, or header math
Design goalClarity plus reuseMaximum 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

One request becomes one size class, then one block, then one chunk. The diagram makes the routing obvious and shows where the free path gets expensive.

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.

A hand tries to return a key to a wall of drawers, checking them one by one because the key has no direct label. Nearby, a second worker moves a reusable key from a free drawer back into circulation, illustrating that free needs to search where alloc only needs to route.
Allocation routes by size. Free has to rediscover ownership, and that is where the extra work appears.

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.

AllocatorAlloc pathFree pathWhat it optimizes for
memory_poolRoute by size class, then reuse free chunksScan used chunks, then search blocks if neededReadability and simple reuse
Boost.PoolPool-specific object managementTypically more direct, depending on pool typeGeneral-purpose pooling flexibility
tcmallocThread-local caching and central transferFast ownership and cache managementThroughput under concurrency
jemallocArena and bin based allocationMetadata-rich deallocationFragmentation 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.

ProjectBest atComplexity footprintBest fit
memory_poolReadable block routing and reuseSmallLearning, internal tools, controlled workloads
Boost.PoolFlexible pooling inside the Boost ecosystemMediumGeneral C++ applications
tcmallocHigh-throughput concurrent allocationLargeLatency-sensitive services
jemallocFragmentation control and scalingLargeSystems 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.

Bruce-Lee-LY, Author · memory_pool GitHub Repository

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.