Skip to content
C++
Library
since C++17
Expert

Custom Allocators and PMR: Controlling Memory at Runtime

std::pmr memory resources and polymorphic allocators: arena allocation, pool resources, writing a custom memory_resource, and when custom allocators win on latency.

The global heap (new/delete, malloc/free) is a general-purpose allocator: it handles any size, any lifetime, any thread. That generality costs latency, fragmentation, and lock contention. When you need predictable allocation β€” in a real-time audio callback, a game frame, an HFT order handler β€” you replace the global heap with a custom memory resource.

C++17 standardized this through std::pmr (Polymorphic Memory Resources), providing a runtime-polymorphic allocator interface that works with all standard containers.


Why custom allocators

Problem with global heapCustom allocator solution
new/delete call the OS β€” unpredictable latencyArena: bulk-allocate once, hand out slices in nanoseconds
Fragmentation over timeMonotonic arena: no fragmentation by construction
Thread-safety overhead (global lock or per-CPU structures)Per-thread arena: zero synchronization
Each allocation has a header (8–32 bytes overhead)Pool: fixed-size slots, no per-object header
Destructors must run in unpredictable orderArena: destroy everything at once by releasing the arena

The std::pmr::memory_resource interface

All PMR allocators derive from one abstract base:

cpp
#include <memory_resource>

class my_resource : public std::pmr::memory_resource {
protected:
    void* do_allocate(std::size_t bytes,
                      std::size_t alignment) override;

    void  do_deallocate(void* ptr,
                        std::size_t bytes,
                        std::size_t alignment) override;

    bool  do_is_equal(const memory_resource& other)
                      const noexcept override;
};
  • do_allocate: returns memory of at least bytes aligned to alignment; throws std::bad_alloc on failure
  • do_deallocate: releases memory previously returned by do_allocate; must accept nullptr gracefully
  • do_is_equal: returns true if other allocates from the same pool β€” used by containers to decide if storage can be transferred without reallocation

The public API (allocate/deallocate/is_equal) delegates to the do_* virtuals. End users call the public versions; the virtuals are implementation hooks.


Built-in resources

monotonic_buffer_resource β€” the arena

The fastest built-in resource. Allocations advance a pointer through a fixed buffer; deallocations are no-ops. Reset by destroying the resource.

cpp
#include <memory_resource>
#include <vector>
#include <string>

void process_frame(const RawData& data) {
    // 64 KB stack buffer β€” no heap involvement at all
    std::byte buf[64 * 1024];
    std::pmr::monotonic_buffer_resource arena{buf, sizeof(buf)};

    // All allocations draw from 'buf':
    std::pmr::vector<std::pmr::string> names{&arena};
    names.push_back("Alice");
    names.push_back("Bob");
    // ... work with names ...

    // arena destructor resets the buffer β€” all memory freed at once
    // No individual deallocations needed
}

If the buffer overflows, monotonic_buffer_resource falls back to an upstream resource (default: global heap). You can provide a custom upstream:

cpp
// Stack buffer first; heap if it overflows:
std::pmr::monotonic_buffer_resource arena{buf, sizeof(buf),
    std::pmr::new_delete_resource()};

// Stack buffer first; throw if it overflows:
std::pmr::monotonic_buffer_resource arena{buf, sizeof(buf),
    std::pmr::null_memory_resource()};

Performance characteristics:

  • Allocation: pointer-bump, ~1-3 ns (vs ~50-200 ns for malloc)
  • Deallocation: no-op
  • Reset: single pointer write
  • Thread safety: none (use per-thread arenas)

unsynchronized_pool_resource β€” fixed-size pool

Manages pools of same-size chunks. Efficient for many allocations of the same or similar sizes (e.g., lots of small objects). No synchronization.

cpp
std::pmr::pool_options opts;
opts.max_blocks_per_chunk  = 32;    // 32 objects per slab
opts.largest_required_pool_block = 512;  // objects up to 512 bytes pooled

std::pmr::unsynchronized_pool_resource pool{opts,
    std::pmr::new_delete_resource()};

std::pmr::vector<Widget> widgets{&pool};
// Each Widget allocation comes from a pool slab, not the global heap

synchronized_pool_resource

Thread-safe version of the pool resource. Has a mutex. Use when multiple threads allocate from the same pool; prefer per-thread unsynchronized_pool_resource when possible.


std::pmr::polymorphic_allocator and PMR containers

std::pmr provides aliases for all standard containers that use polymorphic_allocator:

cpp
namespace std::pmr {
    using vector   = std::vector<T, std::pmr::polymorphic_allocator<T>>;
    using string   = std::basic_string<char, ..., std::pmr::polymorphic_allocator<char>>;
    using map      = std::map<K, V, ..., std::pmr::polymorphic_allocator<...>>;
    // ... and the rest
}
cpp
#include <memory_resource>

std::byte buf[4096];
std::pmr::monotonic_buffer_resource arena{buf, sizeof(buf)};

// All three containers draw from the same arena:
std::pmr::vector<int>              ids{&arena};
std::pmr::string                   name{&arena};
std::pmr::map<std::pmr::string, int> index{&arena};

ids.push_back(1);
name = "Alice";
index[name] = 1;
// arena destroyed β†’ all memory released in one operation

Note that std::pmr::string stores its short-string-optimization buffer inside the object (stack), so short strings don't allocate from the resource at all.


Writing a custom memory_resource

Here is a minimal bump-pointer arena with tracking:

cpp
#include <memory_resource>
#include <cstddef>
#include <cassert>
#include <cstring>

class BumpArena : public std::pmr::memory_resource {
    std::byte* begin_;
    std::byte* end_;
    std::byte* ptr_;
    std::size_t alloc_count_ = 0;

public:
    BumpArena(void* buf, std::size_t size) noexcept
        : begin_{static_cast<std::byte*>(buf)}
        , end_  {begin_ + size}
        , ptr_  {begin_}
    {}

    void reset() noexcept {
        ptr_ = begin_;
        alloc_count_ = 0;
    }

    std::size_t used()  const noexcept { return ptr_ - begin_; }
    std::size_t alloc_count() const noexcept { return alloc_count_; }

protected:
    void* do_allocate(std::size_t bytes, std::size_t align) override {
        // Align ptr_ up to the required alignment:
        void* p = ptr_;
        std::size_t space = end_ - ptr_;
        if (!std::align(align, bytes, p, space))
            throw std::bad_alloc{};

        ptr_ = static_cast<std::byte*>(p) + bytes;
        ++alloc_count_;
        return p;
    }

    void do_deallocate(void*, std::size_t, std::size_t) noexcept override {
        // No-op: memory is freed en masse via reset() or destructor
    }

    bool do_is_equal(const memory_resource& other)
                     const noexcept override {
        return this == &other;
    }
};

Usage:

cpp
alignas(std::max_align_t) std::byte storage[8192];
BumpArena arena{storage, sizeof(storage)};

{
    std::pmr::vector<int> v{&arena};
    v.reserve(100);
    for (int i = 0; i < 100; i++) v.push_back(i);
    // arena.used() > 0; arena.alloc_count() == 1 (one reserve)
}   // vector destructor runs but deallocation is a no-op

arena.reset();   // reclaim all memory for next use

Allocator-extended constructors

PMR containers propagate the allocator through nested containers automatically. But custom types need to opt in using allocator-extended constructors:

cpp
#include <memory_resource>

struct Packet {
    std::pmr::string  header;
    std::pmr::vector<std::byte> payload;

    // Allocator-extended constructor: uses_allocator protocol
    using allocator_type = std::pmr::polymorphic_allocator<>;

    Packet(std::string_view h, std::span<const std::byte> p,
           allocator_type alloc = {})
        : header {h,    alloc}
        , payload{p.begin(), p.end(), alloc}
    {}

    allocator_type get_allocator() const {
        return header.get_allocator();
    }
};

Because Packet exposes allocator_type and a corresponding constructor, std::pmr::vector<Packet> will automatically pass its allocator to each Packet it constructs β€” so all nested strings and byte vectors also draw from the arena.

cpp
std::pmr::monotonic_buffer_resource arena{buf, sizeof(buf)};
std::pmr::vector<Packet> packets{&arena};

// Packet::header and Packet::payload also use 'arena':
packets.emplace_back("GET / HTTP/1.1", body_bytes);

This is the uses-allocator construction protocol defined in <memory>. The standard library uses std::uses_allocator<T, Alloc> to check whether to pass the allocator.


When PMR wins vs when it doesn't

Use PMR / custom allocators when:

  • Allocation latency must be sub-microsecond and predictable (real-time, HFT)
  • A batch of objects is created and destroyed together (game frame, HTTP request, parser run)
  • Many small same-size allocations fragment the global heap over time
  • You want to measure exact memory used by a subsystem (arena tracks used())

Don't bother when:

  • Objects have long, independent lifetimes (the arena model doesn't fit)
  • Allocation is not on your profiler's hot path
  • You'd need to rewrite large amounts of code to use PMR containers

Quick check: if malloc/free appear in a profiler flame graph under your hot path at >1%, investigate a monotonic arena. Otherwise, the global heap is fine.


PMR in CMake / compiler support

cmake
# C++17 required for std::pmr:
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# On GCC < 9, you may need to link tbb or explicitly enable pmr:
# target_link_libraries(my_target PRIVATE tbb)

GCC 9+ and Clang 6+ ship full PMR support. MSVC 2017 15.6+ has partial support; full support from MSVC 2019.

Check availability:

cpp
#include <memory_resource>
// If <memory_resource> is available, std::pmr is usable.
// No feature-test macro needed for C++17 mode.