Step 5 of 5
Custom allocators with std::pmr
new and malloc are general-purpose: they handle any size, any order of frees, from any thread. That generality costs. An allocation may take a lock, search free lists, or ask the operating system for more memory, and its time varies. Low-latency code (games, trading, audio) often forbids heap allocation in its hot loop entirely, and uses arenas and pools instead:
- An arena (monotonic buffer) hands out memory by moving a pointer forward through one big block, and frees everything at once when it's reset. Allocation is a few instructions. You built one in the allocator project.
- A pool keeps free lists of fixed-size blocks, so allocating and freeing a block is a push or pop.
C++17's polymorphic memory resources (std::pmr, header <memory_resource>) let standard containers use them. A std::pmr::vector<int> is a vector that asks a std::pmr::memory_resource*, chosen at run time, for its memory:
#include <array>
#include <cstddef>
#include <iostream>
#include <memory_resource>
#include <vector>
int main() {
std::array<std::byte, 4096> buffer; // lives on the stack
std::pmr::monotonic_buffer_resource arena(
buffer.data(), buffer.size(),
std::pmr::null_memory_resource()); // no fallback: never touch the heap
std::pmr::vector<int> squares(&arena);
squares.reserve(100);
for (int i = 0; i < 100; i++) squares.push_back(i * i);
std::pmr::vector<int> firsts(squares.begin(), squares.begin() + 10, &arena);
std::cout << squares.back() << " " << firsts.size() << "\n";
} // the arena releases everything at once
9801 10
How it works
monotonic_buffer_resourcehands out pieces ofbuffer. Freeing an individual piece does nothing; all of it is released when the arena is destroyed. Perfect for "build some data, use it, throw it all away", like handling one request.- The last argument is the upstream resource, used when the buffer runs out.
null_memory_resource()refuses every request (it throwsstd::bad_alloc), which turns "we accidentally allocated on the heap" into an immediate failure instead of a silent slowdown. std::pmr::unsynchronized_pool_resourceandsynchronized_pool_resource(thread-safe) are pools, andnew_delete_resource()is the normal heap.- A
std::pmr::vector<int>is a different type fromstd::vector<int>(their allocator types differ), so the two don't mix without copying. There arepmrversions ofstring,mapand the other containers. - You write your own resource by deriving from
std::pmr::memory_resourceand overriding three functions:do_allocate(bytes, alignment),do_deallocate(p, bytes, alignment)anddo_is_equal(other).
Your turn: write CountingResource, a memory resource that passes every request on to an upstream resource (the normal heap by default) and counts: allocations() is the number of allocations so far, and bytes_in_use() is the number of bytes currently allocated and not yet freed. It's a tool for proving that code allocates as little as you think.