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 heap | Custom allocator solution |
|---|---|
new/delete call the OS β unpredictable latency | Arena: bulk-allocate once, hand out slices in nanoseconds |
| Fragmentation over time | Monotonic 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 order | Arena: destroy everything at once by releasing the arena |
The std::pmr::memory_resource interface
All PMR allocators derive from one abstract base:
#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 leastbytesaligned toalignment; throwsstd::bad_allocon failuredo_deallocate: releases memory previously returned bydo_allocate; must acceptnullptrgracefullydo_is_equal: returns true ifotherallocates 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.
#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:
// 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.
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 heapsynchronized_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:
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
}#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 operationNote 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:
#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:
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 useAllocator-extended constructors
PMR containers propagate the allocator through nested containers automatically. But custom types need to opt in using allocator-extended constructors:
#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.
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
# 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:
#include <memory_resource>
// If <memory_resource> is available, std::pmr is usable.
// No feature-test macro needed for C++17 mode.