Mastering C++ Allocators and PMR: Build Robust, High‑Performance Memory Management
The article explains why ordinary heap allocation can cause fragmentation and slowdown, introduces the C++ allocator interface and its allocate/deallocate functions, shows how to implement a custom stack‑based allocator, compares traditional allocators with C++17 polymorphic memory resources (PMR), and provides practical code examples, performance benchmarks, and best‑practice guidelines.
In C++ development, frequent creation and destruction of many small objects can turn the heap into a chaotic warehouse, leading to memory fragmentation and degraded allocation performance. In performance‑critical scenarios, the overhead of system calls made by the default allocator can become a bottleneck.
The C++ standard library defines allocator as the fundamental contract between containers and memory allocation. By default, containers such as std::vector<int> use std::allocator<int>, which forwards allocation to new and deallocation to delete.
Allocator mechanics
Two key member functions drive an allocator: allocate(size_type n) – requests storage for n objects of the container’s value type and returns a pointer to the beginning of the block. deallocate(pointer p, size_type n) – releases the block previously obtained from allocate.
When a std::vector needs to grow, it calls its internal allocator’s allocate, moves existing elements, then calls deallocate on the old storage. Prior to C++17, containers also used construct and destroy to manage object lifetimes; since C++17 these are superseded by std::construct_at and std::destroy_at.
Custom stack‑based allocator (StackAllocator)
In environments where memory is scarce or deterministic latency is required (e.g., embedded systems or high‑performance servers), a custom allocator can replace the default. The article walks through the essential steps:
Define required typedefs – value_type, pointer, const_pointer, reference, const_reference, size_type, and difference_type – to satisfy the STL allocator interface.
Implement allocate – a fixed‑size stack buffer T stack[stack_size] is declared; allocate checks whether used + n > stack_size and either returns a pointer into the buffer or throws std::bad_alloc.
Implement deallocate – for a stack allocator the memory is reclaimed only when the whole buffer is reset, so the function is a no‑op.
Provide construct and destroy (pre‑C++17) – use placement new and explicit destructor calls.
Provide rebind – a nested template that maps the allocator to a different value type, enabling use with any container.
A complete example demonstrates a StackAllocator<int> used with std::vector<int, StackAllocator<int>> to push and print values without any heap allocation.
Why use a custom allocator? It separates memory management from object logic, improves readability, and allows specialized strategies such as memory pools for game development, hardware‑specific allocation in embedded systems, or deterministic allocation in real‑time tasks. It also enables swapping the allocation strategy without changing container code.
Polymorphic Memory Resources (PMR)
C++17 introduced std::pmr::memory_resource, an abstract base class defining allocate and deallocate. Derived classes implement concrete policies. Standard resources include: std::pmr::monotonic_buffer_resource – fast, single‑direction allocation; deallocation occurs only when the resource is destroyed. std::pmr::unsynchronized_pool_resource – lock‑free pool for fixed‑size objects, ideal for single‑threaded high‑frequency allocations. std::pmr::synchronized_pool_resource – thread‑safe version of the pool resource. std::pmr::new_delete_resource – the default fallback that forwards to global new / delete.
Unlike traditional allocators, PMR decouples the allocation strategy from the container type. A std::pmr::vector<int> always has the same type regardless of the underlying resource, allowing runtime switching based on configuration or memory pressure.
Advantages of PMR
Eliminates template bloat and reduces compile‑time because the same container type is reused for all resources.
Enables dynamic strategy changes without recompilation.
Provides specialized resources that can achieve allocation speeds comparable to stack allocation (e.g., monotonic_buffer_resource) or reduce fragmentation (e.g., unsynchronized_pool_resource).
Typical usage examples are shown:
#include <iostream>
#include <memory_resource>
#include <array>
int main() {
std::array<std::byte, 1024> buf; // stack buffer
std::pmr::monotonic_buffer_resource mbr(buf.data(), buf.size(), nullptr);
std::pmr::vector<int> v(&mbr);
for (int i = 0; i < 20; ++i) v.push_back(i);
std::cout << "size: " << v.size() << std::endl;
}The article also presents a pool‑based example using unsynchronized_pool_resource for a vector of a custom Node struct, and a multithreaded scenario with synchronized_pool_resource shared across four threads.
Zero‑heap allocation
By combining a stack buffer with monotonic_buffer_resource, one can achieve “zero‑heap” allocation: all memory comes from the stack, and the entire buffer is reclaimed automatically when the resource goes out of scope.
Performance comparison
Two benchmark functions are provided: one using the default std::vector allocator and another using std::pmr::vector backed by unsynchronized_pool_resource. In typical runs, the PMR version completes significantly faster because it avoids frequent system calls and reduces fragmentation.
Practical recommendations and pitfalls
Be aware of the virtual‑function overhead of std::pmr::polymorphic_allocator; for fixed strategies, use the concrete resource directly.
Handle alignment requirements explicitly when customizing resources.
Ensure the lifetime of a memory resource exceeds that of any container that uses it; otherwise undefined behavior occurs.
When nesting containers, prefer PMR containers (e.g., std::pmr::vector<std::pmr::string>) or scoped_allocator_adaptor to keep allocation policies consistent.
Correctly pass the resource to the container constructor; forgetting to do so leaves the container using the default allocator.
Overall, the article equips readers with a clear understanding of both classic allocators and modern PMR, demonstrates how to write a custom allocator, and offers concrete guidance for choosing the right memory‑management strategy in performance‑sensitive C++ applications.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
