Memory Pools & Arenas

Reviewed & published by Brayan K

By the end of this lesson you'll understand why per-object new/delete is slow in a hot loop, and you'll be able to build a fixed-block pool allocator and a bump-pointer arena by hand, recycle objects with an object pool, and reach for std::pmr when you want the speed without writing the allocator yourself.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Calling new for every object is like driving to the store for one ingredient each time you cook — generic, flexible, and slow because each trip has fixed overhead. A pool is a tray of identical lunchboxes by the door: grab one when you need it, drop it back when you're done, and an empty box is instantly reusable — no searching, no oddly-shaped gaps. An arena is a whiteboard: you keep writing left to right (bumping a pointer), and when the meeting ends you wipe the whole board in one stroke instead of erasing each note. General new/delete is the all-purpose store; pools and arenas are purpose-built and far faster for the right pattern.

📊 Why default new/delete hurts in hot paths

CostWhat happens on every new/delete
BookkeepingSearches free lists for a big-enough block, may split/merge blocks.
LockingThe global heap is shared, so allocation often takes a lock — contention under threads.
FragmentationMixed sizes leave gaps too small to reuse; memory grows and cache behaviour worsens.
Cache missesObjects land far apart in memory, so iterating them thrashes the CPU cache.

A pool or arena removes nearly all of this for a known pattern: allocation becomes "pop one node" or "bump a pointer", and the objects sit packed together for cache-friendly iteration. That's why game engines, network servers, and compilers lean on them in their hottest loops.

1. The Fixed-Block Pool Allocator

A pool carves one big block into equal-sized slots and hands them out one at a time. The clever bit is the free list: while a slot is unused, you reuse its own bytes to store a pointer to the next free slot. Allocation pops the head of that list and deallocation pushes a slot back onto it — both are O(1) with zero extra memory. Read this worked example carefully; every non-obvious line is commented with what it does.

#include <iostream>
using namespace std;

// A POOL hands out fixed-size SLOTS from one big block.
// Trick: while a slot is free we reuse its bytes to store a
// pointer to the NEXT free slot -> a "free list" with zero extra memory.
class BytePool {
    static const int SLOT = 32;   // bytes per slot
    static const int COUNT = 4;   // how many slots
    char buffer[SLOT * COUNT];    // one contiguous block
    void* freeList = nullptr;     // head of the free list

public:
    BytePool() {
        // Link every slot into the free list, back to front.
        for (int i = 0; i < COUNT; i++) {
            void* slot = buffer + i * SLOT;
            *reinterpret_cast<void**>(slot) = freeList; // store "next" in the slot
            freeList = slot;                            // push it on the list
        }
        cout << "Pool ready: " << COUNT << " slots of " << SLOT << " bytes\n";
    }

    void* allocate() {
        if (!freeList) { cout << "Pool empty!\n"; return nullptr; }
        void* slot = freeList;                          // pop the head -> O(1)
        freeList = *reinterpret_cast<void**>(freeList); // head = its "next"
        return slot;
    }

    void deallocate(void* slot) {
        *reinterpret_cast<void**>(slot) = freeList;     // push back -> O(1)
        freeList = slot;
    }
};

int main() {
    BytePool pool;

    void* a = pool.allocate();   // takes a slot
    void* b = pool.allocate();   // takes another
    cout << "a and b differ? " << (a != b) << "\n";   // 1 (true)

    pool.deallocate(a);          // return a's slot to the list
    void* c = pool.allocate();   // reuses the SAME slot a had
    cout << "c reused a's slot? " << (c == a) << "\n"; // 1 (true)

    pool.allocate();             // 4th slot
    pool.allocate();             // pool now empty -> prints "Pool empty!"
    return 0;
}

// ✅ Expected output:
//    Pool ready: 4 slots of 32 bytes
//    a and b differ? 1
//    c reused a's slot? 1

A raw byte pool is fine for learning, but in real code you want a typed object pool that constructs a real object in the slot. That needs placement new — new (ptr) T(args) runs a constructor on memory you already own, without allocating anything. Because placement new never pairs with delete, you must call the destructor yourself with ptr->~T() before recycling the slot.

#include <iostream>
#include <new>     // placement new
using namespace std;

struct Bullet {
    int id; float x;
    Bullet(int i, float px) : id(i), x(px) {
        cout << "  Bullet " << id << " spawned at x=" << x << "\n";
    }
    ~Bullet() { cout << "  Bullet " << id << " destroyed\n"; }
};

template <typename T, int N>
class ObjectPool {
    // A slot is raw, correctly-sized, correctly-aligned bytes for ONE T,
    // OR a pointer to the next free slot when the slot is empty.
    union Slot { T value; Slot* next; Slot() {} ~Slot() {} };
    Slot storage[N];
    Slot* freeList = nullptr;

public:
    ObjectPool() {
        for (int i = 0; i < N; i++) {            // build the free list
            storage[i].next = freeList;
            freeList = &storage[i];
        }
    }

    // allocate = pop a slot, then construct T in place with placement new
    template <typename... Args>
    T* create(Args&&... args) {
        if (!freeList) { cout << "Pool full!\n"; return nullptr; }
        Slot* s = freeList;
        freeList = s->next;
        return new (&s->value) T(forward<Args>(args)...); // placement new
    }

    // destroy = call the destructor BY HAND, then push the slot back
    void destroy(T* obj) {
        obj->~T();                                // placement new never calls delete
        Slot* s = reinterpret_cast<Slot*>(obj);
        s->next = freeList;
        freeList = s;
    }
};

int main() {
    ObjectPool<Bullet, 3> pool;

    Bullet* b1 = pool.create(1, 10.0f);
    Bullet* b2 = pool.create(2, 20.0f);
    cout << "b1 id = " << b1->id << ", b2 id = " << b2->id << "\n";

    pool.destroy(b1);                  // frees the slot (and runs ~Bullet)
    Bullet* b3 = pool.create(3, 30.0f); // reuses b1's slot
    cout << "b3 reused b1's slot? " << (reinterpret_cast<void*>(b3)
                                     == reinterpret_cast<void*>(b1)) << "\n"; // 1

    pool.destroy(b2);
    pool.destroy(b3);
    return 0;
}

// ✅ Expected output:
//      Bullet 1 spawned at x=10
//      Bullet 2 spawned at x=20
//    b1 id = 1, b2 id = 2
//      Bullet 1 destroyed
//      Bullet 3 spawned at x=30
//    b3 reused b1's slot? 1
//      Bullet 2 destroyed
//      Bullet 3 destroyed

2. The Arena (Bump) Allocator

An arena (also called a bump allocator) is even simpler than a pool. It keeps one cursor — an offset — and every allocation just hands back the current position then moves the cursor forward. You never free individual objects; instead you reset() the whole arena, which rewinds the cursor to 0 and reclaims everything at once in O(1). This is ideal for data with a clear phase: per-frame game state, per-request server scratch memory, or a compiler's AST nodes. Now you finish one.

The program below is almost complete — fill in the two blanks marked ___ using the // 👉 hints, then run it and check it against the expected output.

#include <iostream>
using namespace std;

// 🎯 YOUR TURN — finish this ARENA (bump) allocator.
// An arena hands out memory by moving "offset" forward. Freeing
// everything is just resetting offset back to 0 -> O(1).
class Arena {
    char buffer[256];
    size_t offset = 0;
public:
    void* allocate(size_t bytes) {
        if (offset + bytes > sizeof(buffer)) return nullptr;
        void* ptr = buffer + offset;

        // 1) Move the cursor forward by 'bytes' so the next
        //    allocation starts after this one.
        offset = ___;     // 👉 add bytes to the current offset

        return ptr;
    }

    // 2) "Free everything at once" by rewinding the cursor.
    void reset() {
        ___;              // 👉 set offset back to 0
    }

    size_t used() const { return offset; }
};

int main() {
    Arena arena;
    arena.allocate(40);
    arena.allocate(60);
    cout << "Used: " << arena.used() << " bytes\n"; // Used: 100 bytes
    arena.reset();
    cout << "After reset: " << arena.used() << " bytes\n"; // After reset: 0 bytes
    return 0;

    // ✅ Expected output:
    //    Used: 100 bytes
    //    After reset: 0 bytes
}

3. std::pmr — Standard Allocators Without the Boilerplate

Hand-writing allocators is great for understanding, but C++17 gives you a ready-made framework: std::pmr (polymorphic memory resources). A std::pmr::memory_resource is an object that knows how to hand out memory, and pmr containers like pmr::vector take their memory from it. Two come built in: monotonic_buffer_resource is an arena (bump, reset on destruction), and unsynchronized_pool_resource is a pool. You get custom-allocator speed without writing the allocator — and you can swap strategies without changing the container's type.

#include <iostream>
#include <vector>
#include <memory_resource>   // std::pmr (C++17)
using namespace std;

int main() {
    // Give a fixed stack buffer to a monotonic_buffer_resource:
    // it's an ARENA -> bump allocate, never reuses freed memory,
    // releases it all when the resource dies. No heap calls at all.
    char buffer[1024];
    pmr::monotonic_buffer_resource arena{buffer, sizeof(buffer)};

    // A pmr::vector takes its memory FROM that resource.
    pmr::vector<int> v{&arena};
    for (int i = 1; i <= 5; i++) v.push_back(i * 10);

    cout << "pmr vector: ";
    for (int n : v) cout << n << " ";   // 10 20 30 40 50
    cout << "\n";

    // Strings in the same arena -> still no global new/delete.
    pmr::vector<pmr::string> names{&arena};
    names.emplace_back("Ada");
    names.emplace_back("Linus");
    for (auto& s : names) cout << s << " "; // Ada Linus
    cout << "\n";

    return 0;
}

// ✅ Expected output:
//    pmr vector: 10 20 30 40 50
//    Ada Linus

🔎 Deep Dive: placement new, destructors & alignment

Pools and arenas hand out raw bytes, not constructed objects. Placement new bridges that gap: new (ptr) T(args) calls T's constructor at the address ptr and allocates nothing. The price is symmetry — there is no "placement delete", so you must run the destructor yourself: ptr->~T();. Forget that, and any work the destructor does (closing files, freeing inner buffers) silently leaks.

Alignment matters too: a double usually must sit on an 8-byte boundary. A union-of-T slot (as in the object pool) is automatically aligned for T; a raw char buffer is not, so a real arena rounds each offset up to the right boundary before handing it out.

void* mem = pool.raw_slot();      // raw, owned bytes
T* obj = new (mem) T(args);        // placement new: construct in place
// ... use obj ...
obj->~T();                         // run the destructor BY HAND
pool.recycle(mem);                 // now the slot is safe to reuse

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

AllocatorAllocateFreeBest for
PoolO(1) pop free listO(1) push backMany same-size objects
ArenaO(1) bump offsetO(1) reset allPer-frame / per-request
StackO(1) bumpO(1) LIFO popScoped temporaries
pmr::monotonicO(1) bumpon destructionStandard containers, arena
new / deleteO(1)–O(n)O(1)–O(n)General purpose

Key calls: new (ptr) T(args) (placement new), ptr->~T() (manual destructor), #include <memory_resource> for std::pmr.

Mini-Challenge: Build a Token Pool

No blanks this time — just a brief and an outline. Build a fixed-block pool for a tiny Token type, hand out and recycle slots, and prove a freed slot gets reused by comparing pointers. This is the exact pattern a real engine uses for particles, entities, and packets.

#include <iostream>
#include <new>
using namespace std;

struct Token { int kind; };

int main() {
    // 🎯 MINI-CHALLENGE: a tiny fixed-block Token pool
    // 1. Make a union Slot holding either a Token or a Slot* next.
    // 2. Make an array of, say, 4 Slots and link them into a free list.
    // 3. Write create(): pop the free list, placement-new a Token,
    //    return the pointer (or nullptr if the pool is empty).
    // 4. Write destroy(Token*): call ~Token(), push the slot back.
    // 5. In main(): create 2 tokens, destroy 1, create 1 more, and
    //    show the new one reused the freed slot (compare the pointers).
    //
    // ✅ Example output:
    //    reused freed slot? 1

    // your code here
    return 0;
}

🎉 Lesson Complete

Practice quiz

Why is per-object new/delete slow in a hot loop compared to a pool?

  • It searches free lists, may split/merge blocks, and often takes a lock
  • It always zero-initialises the memory it returns
  • It compresses the heap on every call
  • It runs the constructor twice for safety

Answer: It searches free lists, may split/merge blocks, and often takes a lock. A general allocator does bookkeeping (find/split/merge a block) and is usually thread-safe (a lock), while a pool just pops one node.

In a fixed-block pool's free list, where is the 'next free slot' pointer stored?

  • In a separate side table indexed by slot number
  • Inside the unused slot's own bytes
  • In a global hash map keyed by address
  • At the end of the buffer, after all slots

Answer: Inside the unused slot's own bytes. While a slot is free its bytes are reused to hold the next pointer, so the pool needs zero extra bookkeeping memory.

What are the time complexities of pool allocate and deallocate?

  • O(n) allocate, O(1) deallocate
  • O(log n) for both
  • O(1) for both — pop the head, push it back
  • O(n) for both

Answer: O(1) for both — pop the head, push it back. Allocation pops the head of the free list and deallocation pushes a slot back, both O(1).

What does placement new — new (ptr) T(args) — do?

  • Allocates fresh memory and constructs T in it
  • Runs T's constructor on memory you already own, allocating nothing
  • Frees ptr and then reallocates T
  • Only zeroes the bytes at ptr

Answer: Runs T's constructor on memory you already own, allocating nothing. Placement new constructs an object at an address you provide; it allocates no memory itself.

After constructing an object with placement new, how do you destroy it before recycling the slot?

  • Call delete on the pointer
  • Call delete[] on the pointer
  • Call the destructor by hand: ptr->~T()
  • Nothing — the slot is freed automatically

Answer: Call the destructor by hand: ptr->~T(). There is no placement delete, so you must run the destructor yourself with ptr->~T(); never call delete on placement-new memory.

How does an arena (bump) allocator free everything?

  • It calls delete on each object in reverse order
  • It walks a free list and frees each node
  • It resets the offset cursor back to 0 in O(1)
  • It cannot free — the OS reclaims it at exit

Answer: It resets the offset cursor back to 0 in O(1). An arena hands out memory by bumping an offset; reset() rewinds the offset to 0, reclaiming everything at once.

In the arena's allocate(), how is the cursor advanced after handing back the current position?

  • offset = offset + bytes;
  • offset = 0;
  • offset = bytes - offset;
  • offset = sizeof(buffer);

Answer: offset = offset + bytes;. Each allocation returns buffer + offset, then moves offset forward by the requested number of bytes.

Which std::pmr resource behaves like an arena (bump-allocate, reclaim on destruction)?

  • std::pmr::synchronized_pool_resource
  • std::pmr::monotonic_buffer_resource
  • std::pmr::new_delete_resource
  • std::pmr::null_memory_resource

Answer: std::pmr::monotonic_buffer_resource. monotonic_buffer_resource bump-allocates and never reuses freed memory, releasing it all when the resource dies.

Which header must you include to use std::pmr, and from which standard?

  • <memory_resource>, C++17
  • <memory>, C++11
  • <pmr>, C++20
  • <allocator>, C++14

Answer: <memory_resource>, C++17. std::pmr (polymorphic memory resources) lives in <memory_resource> and was added in C++17.

When is it appropriate to reach for a custom pool or arena allocator?

  • Always — it is faster than new/delete in every program
  • Only in a proven hot path after a profiler shows allocation is the bottleneck
  • Whenever a class has a destructor
  • Only when the program is single-threaded

Answer: Only in a proven hot path after a profiler shows allocation is the bottleneck. Default new/delete is fine for most code; measure first and use a custom allocator only where allocation is genuinely the bottleneck.

Continue this course

Frequently asked questions

Why is new/delete slow if it's just allocating memory?

Because a general-purpose allocator has to do real work on every call: find a free block big enough, split or merge blocks, update internal bookkeeping, and stay thread-safe (often a lock). A pool or arena skips almost all of that — it just bumps a pointer or pops one node off a list — so it can be many times faster in a hot loop.

What is memory fragmentation and why do pools avoid it?

Fragmentation is when free memory exists but is split into gaps too small to satisfy a request, so allocation fails or slows down even though there is 'enough' memory. A pool gives out fixed-size slots that are all interchangeable, so a freed slot can always be reused for the next object — there are no oddly-sized gaps to strand.

When should I NOT write a custom allocator?

Most of the time. Default new/delete is fine for ordinary code, and a custom allocator adds complexity and bugs. Reach for a pool or arena only when a profiler shows allocation is a real bottleneck, or in a hot path like a game frame loop, a packet handler, or a request handler where you allocate and free many objects of the same shape.

What is placement new and why do pools use it?

Placement new — new (ptr) T(args) — runs a constructor on memory you already own instead of allocating fresh memory. Pools and arenas hand out raw bytes, so they use placement new to construct the object in those bytes. The catch: placement new does not call delete, so you must call the destructor yourself with ptr->~T().

What is std::pmr and how is it different from writing my own pool?

std::pmr (polymorphic memory resources, C++17) is a standard framework that lets containers like std::vector take their memory from a memory_resource you choose at run time — for example a monotonic_buffer_resource (an arena) or an unsynchronized_pool_resource (a pool). You get the speed of a custom allocator without hand-writing one, and you can swap strategies without changing the container's type.

Related lessons