C++ Memory Pools — How Exhaustion Crashed a Trading Engine
Fixed-capacity pool of 100,000 slots returns null on exhaustion, causing crashes.
20+ years shipping performance-critical C and C++ systems. Written from production experience, not tutorials.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
- A memory pool pre-allocates a large block and serves fixed-size slots in O(1) time
- Heap fragmentation disappears because all allocations are same size from a contiguous region
- Pool allocators eliminate per-allocation lock contention using thread-local caches
- Typical allocation latency: ~10-20ns vs 100-300ns for malloc (varies by implementation)
- Production failure: pool exhaustion causes silent crashes when objects outlive pool lifetime
- Biggest mistake: using pool for variable-size objects, causing internal fragmentation and wasted space
Imagine you run a parking lot. Instead of letting cars park anywhere they want — creating gaps and making it hard to fit new ones — you pre-paint numbered spaces before the day starts. Every car goes into a pre-assigned slot instantly, no searching, no gaps. A memory pool allocator does exactly this for your program's RAM: it grabs a big chunk of memory upfront, slices it into fixed-size slots, and hands them out in microseconds — no rummaging through the heap, no fragmentation.
In a game engine rendering 120 frames per second, a trading system processing 10 million orders a minute, or an embedded firmware loop that must respond in under 50 microseconds, one thing will kill you faster than a logic bug: unpredictable memory allocation latency. The default new and delete operators are general-purpose tools — they handle any size, any time, with synchronization locks baked in. That generality has a cost, and in performance-critical C++ that cost is often unacceptable.
The standard heap allocator, whether it's glibc's ptmalloc, jemalloc, or TCMalloc, must maintain bookkeeping metadata, walk free-lists of variable sizes, coalesce adjacent freed blocks, and acquire locks in multithreaded contexts. Every call to new can trigger a system call, invalidate cache lines, and introduce allocations that are milliseconds apart in memory yet logically adjacent — fragmenting your address space until reallocation itself becomes a bottleneck. Memory pool allocators solve this by inverting the model: instead of asking the OS for memory on demand, you reserve a large contiguous arena upfront and serve allocations from it yourself, with full knowledge of your objects' sizes and lifetimes.
By the end of this article you'll understand how pool allocators work at the bit level, how to implement a thread-safe fixed-size pool and a more flexible slab-style allocator from scratch, how to plug them into STL containers via a custom allocator, and exactly when the tradeoffs make sense — and when they don't. You'll also leave with the mental model interviewers are probing for when they ask about custom allocators in systems programming interviews.
What is a Memory Pool Allocator?
A memory pool allocator (or fixed-size allocator) pre-allocates a large contiguous block of memory and divides it into slots of equal size. When you request an allocation, it returns a pointer to the next free slot — no searching, no coalescing. When you free, it marks the slot as available, often by pushing it onto a free list.
You'll find pool allocators in game engines, trading systems, embedded firmware, and custom STL allocators. They shine when you allocate many objects of the same type (e.g., particles, network packets, database rows) and you control their lifetimes tightly.
The key difference from general-purpose allocators is that pools don't handle arbitrary sizes. That's a feature, not a bug — by giving up generality, you gain speed and predictability.
- Pre-allocated memory = stack of trays
- free_head_ = pointer to the next tray
- allocate = pop the tray (O(1))
- deallocate = push it back (O(1))
Slab Allocator: Handling Multiple Sizes
A slab allocator maintains several fixed-size pools (slabs) for different size classes. When you request 24 bytes, it returns a chunk from the 32-byte slab. This trades external fragmentation for some internal fragmentation per slab, but still avoids the overhead of walking free lists of variable sizes.
The Linux slab allocator popularised this approach. It also caches recently freed objects in a per-CPU cache to avoid locking entirely.
Implementing a simple slab allocator means creating an array of FixedPool objects, each with a size from a predefined size table (usually powers of two plus some mid-range entries: 16, 32, 64, 128, ...). The allocator rounds up the requested size to the next slab size.
In production, slab allocators work well for networking stacks and kernel-level memory management.
Thread-Safety Considerations
The simple pool above uses a single free list protected by nothing. That's safe only in a single-threaded context. In multi-threaded environments, you can't share the free_head_ pointer without protection — threads will race, corrupt the list, and crash.
Three common strategies: 1. Global mutex around allocate/deallocate — simple but kills performance under contention. 2. Thread-local pools — each thread has its own pool, no sharing. Objects must be freed on the same thread that allocated them, or you need a transfer mechanism. 3. Lock-free free list — use atomic compare-and-swap (CAS) to manipulate the head pointer. This is the fastest option for moderate contention.
Let's implement a lock‑free pool using std::atomic<Slot*>.
Plugging Into the STL: Custom Allocators
The STL containers like std::vector and std::map accept custom allocators via the template parameter. This lets you make std::vector use your pool instead of new/delete.
A custom allocator must satisfy the Allocator concept: it needs and allocate() methods, a deallocate()rebind struct, and equality operators. The C++17 std::pmr::memory_resource provides a higher-level interface, but we'll show the classic approach.
Here's an adaptor that wraps our FixedPool into an STL allocator.
rebind to allocate internal nodes (e.g., std::list nodes are different from value_type). Your allocator must support rebinding or the container will fall back to std::allocator. Also, std::vector may request n > 1 — you can't assume single-object allocation.bad_alloc or if exceptions disabled, undefined behavior.reserve() to pre-allocate.std::pmr::memory_resource in C++17 is a cleaner abstraction for runtime polymorphic allocators.std::vector and want it to use your poolPoolAllocator<T> adapter. But note: std::vector may request multiple items.std::map or std::list on the poolrebind — it will be used for nodes.When Pool Allocators Lose: Trade-offs and Failure Modes
Pool allocators are not a silver bullet. They fail badly when object sizes vary widely, lifetimes are unpredictable, or memory must be returned to the OS. Here are the problems that catch teams in production:
- Internal fragmentation — Fixed-size slots waste space if objects are smaller than the slot. A 64-byte pool for 16-byte objects wastes 75% of memory.
- Memory blow-up under load — Pools never shrink. If an allocator reserves 1MB for 1024 objects of 1KB, a single peak usage event pins that 1MB forever.
- No way to return memory to OS — The
::operator newinside the pool is freed only when the pool destructs. Long-lived pools are effectively memory leaks from the OS perspective. - Debugging difficulty — Use-after-free errors inside a pool are hard to detect because the slot memory still belongs to the pool and appears valid. Standard tools like ASan often can't catch intra-pool violations unless you poison freed slots.
For these reasons, pool allocators are best for hot paths where allocation counts are high and object sizes are uniform. For cold paths or unpredictable workloads, stick with general-purpose allocators.
malloc replacement with built-in leak detection.Why the Default Allocator Betrays You Under Load
You don't notice malloc fragmentation until your latency spikes 200x. The heap is a shared resource. Every thread's deallocation can dirty a cache line another thread needs. Pool allocators solve this by treating memory as a flat array of fixed-size slots. No fragmentation, no coalescing, no kernel calls. You reserve a contiguous block once, then hand out slots with a pointer bump or a free list. When every allocation is O(1) and every deallocation just pushes a pointer back onto a stack, your allocator stops being a bottleneck. The real win is CPU cache behavior: sequential allocations land on adjacent cache lines. The WHY is simple: malloc pays for generality you don't need. A pool pays for exactly your workload.
reset() leaks destructors. Use a stack-based free list for per-object lifetimes.Stack Allocator: The Arena Pattern for Transient Work
Your game loop or request handler allocates thousands of small objects, then frees them all at once. Default allocators thrash the heap for no reason. The arena (or stack allocator) is your weapon: grab a large contiguous buffer, bump a pointer for each allocation, and reset the pointer when the frame or request ends. No deallocation calls during the hot path. No fragmentation. No thread contention if each thread owns its arena. The WHY: your allocation pattern is LIFO or bulk-free. You don't need individual frees. This is how game engines handle per-frame allocations and how HTTP servers handle per-request buffers. The cost is one: you cannot free a single object—only the whole arena. That constraint forces you to write code that batches lifetimes.
reset(), or skip if POD.std::pmr::memory_resource: Polymorphic Allocators
Polymorphic allocators, introduced in C++17 via std::pmr::memory_resource, decouple container allocation from the underlying memory source. Instead of hardcoding new/delete or a custom allocator type, containers like std::pmr::vector accept a memory_resource* at construction, allowing runtime selection of allocation strategies. This is especially useful in trading engines where different subsystems (e.g., order book, market data) may require distinct pool behaviors.
A memory_resource is an abstract base class with two pure virtual functions: do_allocate(size_t bytes, size_t alignment) and do_deallocate(void* p, size_t bytes, size_t alignment). The standard provides concrete resources: std::pmr::new_delete_resource() (default heap), std::pmr::null_memory_resource() (throws std::bad_alloc on any allocation), and std::pmr::monotonic_buffer_resource (a fast arena allocator).
To use a pool, you can wrap it in a custom memory_resource or use the standard pool resources. For example, a monotonic buffer resource can be used for transient work: ```cpp #include
int main() { std::arraybuffer.data(), buffer.size()); std::pmr::vector This pattern eliminates per-element free` overhead, ideal for high-frequency trading loops where allocation bursts are followed by bulk teardown.
monotonic_buffer_resource for per-message processing and synchronized_pool_resource for shared structures like order books to balance speed and thread safety.C++23: std::pmr::synchronized_pool_resource and unsynchronized_pool_resource
C++23 introduces two new standard pool resources: std::pmr::synchronized_pool_resource and std::pmr::unsynchronized_pool_resource. These are thread-safe and non-thread-safe versions, respectively, of a pool allocator that manages fixed-size blocks. They are designed to reduce fragmentation and improve cache locality for frequent allocations of similar sizes.
Both resources maintain pools of memory chunks, each chunk subdivided into blocks of a specific size. When a request arrives, the resource picks the pool whose block size is large enough (rounded up to the nearest power of two). This minimizes waste and speeds up allocation/deallocation. The synchronized variant uses mutexes internally, making it safe for concurrent access; the unsynchronized variant is faster but requires external synchronization.
Example usage: ```cpp #include <pmr/memory_resource> #include <pmr/vector> #include <pmr/synchronized_pool_resource>
int main() { std::pmr::synchronized_pool_resource pool; // uses new_delete as upstream std::pmr::vector<int> vec(&pool); vec.reserve(1000); // allocates from pool // Thread-safe allocations from multiple threads } `` These resources are ideal for trading engines where multiple threads allocate and deallocate objects of varying sizes (e.g., order objects, trade records). The synchronized variant can replace custom thread-safe pools, reducing code complexity. However, they still rely on an upstream resource (default new_delete`) for large allocations, so monitor for unexpected upstream calls.
unsynchronized_pool_resource per-thread for lock-free performance, and synchronized_pool_resource for shared data structures. Profile to ensure pool sizes match your allocation patterns.Custom Memory Resource Implementation Pattern
When standard pool resources don't meet your latency or fragmentation requirements, implementing a custom memory_resource gives you full control. The pattern involves inheriting from std::pmr::memory_resource and overriding do_allocate, do_deallocate, and do_is_equal. This allows you to integrate existing pool allocators (e.g., a slab allocator) into the polymorphic allocator framework.
A minimal custom resource that wraps a simple bump allocator: ```cpp #include
class BumpResource : public std::pmr::memory_resource { char buffer; size_t capacity; size_t offset = 0; public: BumpResource(char buf, size_t cap) : buffer(buf), capacity(cap) {} private: void do_allocate(size_t bytes, size_t alignment) override { // Align offset size_t aligned = (offset + alignment - 1) & ~(alignment - 1); if (aligned + bytes > capacity) throw std::bad_alloc(); void ptr = buffer + aligned; offset = aligned + bytes; return ptr; } void do_deallocate(void, size_t, size_t) override { / no-op */ } bool do_is_equal(const memory_resource& other) const noexcept override { return this == &other; } }; `` This bump allocator is extremely fast (no free) and perfect for transient work. For a full-featured pool, you'd manage free lists and reuse blocks. The do_is_equal` method is critical for container operations like swap; it should return true only if both resources are identical (same pool).
Custom resources allow you to implement advanced strategies: thread-local caches, NUMA-aware allocation, or leak detection. In a trading engine, you might create a resource that allocates from a pre-allocated shared memory segment for inter-process communication.
memory_resource lets you encapsulate any allocation strategy into the polymorphic allocator framework, enabling seamless integration with STL containers.The Trading Engine That Crashed at Market Open
malloc when the pool is full, with a warning log. Also added a free-list validation (xor-linked list or magic numbers) to catch double-frees immediately. The pool size was adjusted to handle 3x the 99.9th percentile burst.- Never assume your pool will be large enough — always have a fallback allocator.
- Check every allocation return — pools aren't guaranteed to succeed.
- Instrument pool usage: log depletion warnings and monitor free-list integrity.
g++ -fsanitize=address -g myapp.cpp -o myapp && ./myappvalgrind --tool=memcheck --leak-check=full ./myapp| File | Command / Code | Purpose |
|---|---|---|
| io | namespace io::thecodeforge { | What is a Memory Pool Allocator? |
| io | namespace io::thecodeforge { | Slab Allocator |
| io | namespace io::thecodeforge { | Thread-Safety Considerations |
| io | namespace io::thecodeforge { | Plugging Into the STL |
| io | struct Particle { int x, y, vx, vy; }; // 16 bytes | When Pool Allocators Lose |
| pool_allocator.cpp | template | Why the Default Allocator Betrays You Under Load |
| arena.cpp | class Arena { | Stack Allocator |
| pmr_example.cpp | int main() { | std |
| pool_resource_example.cpp | int main() { | C++23 |
| custom_resource.cpp | class BumpResource : public std::pmr::memory_resource { | Custom Memory Resource Implementation Pattern |
Key takeaways
Interview Questions on This Topic
Describe how a fixed-size pool allocator works internally. What are the trade-offs compared to malloc?
Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Written from production experience, not tutorials.
That's C++ Advanced. Mark it forged?
6 min read · try the examples if you haven't