Allocators and polymorphic memory resources

C++ · Expert · 18 min

Allocation is often the hidden cost in hot paths; custom memory resources can make code several times faster and more predictable.

Every standard container gets its memory through an allocator, the last template parameter: std::vector<T, Alloc>. The default, std::allocator<T>, calls operator new. A custom allocator only needs a value_type, allocate(n), deallocate(p, n), equality comparison and a converting constructor; std::allocator_traits fills in the rest:

template <class T> struct Arena {
    using value_type = T;
    T* allocate(std::size_t n);
    void deallocate(T* p, std::size_t n) noexcept;
};

The classic model has drawbacks. The allocator is part of the type, so vector<int, A> and vector<int, B> don't mix, and stateful allocators bring tricky propagation rules on copy, move and swap.

C++17's polymorphic memory resources (<memory_resource>, namespace std::pmr) solve this. A std::pmr::memory_resource is an abstract class with virtual allocate/deallocate. std::pmr::polymorphic_allocator forwards to one, and aliases like std::pmr::vector<T> and std::pmr::string use it, so containers backed by different resources share one type. The allocator also propagates to elements: a pmr::vector<pmr::string> places its strings' buffers in the same resource.

Standard resources:

  • monotonic_buffer_resource: bump-pointer arena. Allocation is a pointer increment, deallocate is a no-op, and everything is freed at once when the resource dies. Ideal for per-request or per-frame data, optionally starting from a stack buffer.
  • unsynchronized_pool_resource / synchronized_pool_resource: pools of fixed-size blocks, without or with locking.
  • new_delete_resource() and null_memory_resource() (which always throws, handy as an upstream to catch overflow).

The usual caveat: the resource must outlive every container using it, and measure before and after to confirm allocation was actually the bottleneck.

Example

#include <array>
#include <cstddef>
#include <iostream>
#include <memory_resource>
#include <string>
#include <vector>

int main() {
    std::array<std::byte, 4096> buffer;                        // arena on the stack
    std::pmr::monotonic_buffer_resource arena{
        buffer.data(), buffer.size(), std::pmr::null_memory_resource()};  // throw if exceeded

    std::pmr::vector<std::pmr::string> names{&arena};          // vector AND strings use the arena
    names.reserve(8);
    names.emplace_back("a string long enough to need its own heap buffer");
    names.emplace_back("short");

    bool same = names[0].get_allocator().resource() == &arena; // allocator propagated
    std::cout << names.size() << ' ' << same << '\n';          // 2 1

    std::pmr::unsynchronized_pool_resource pool;               // size-class pools
    std::pmr::vector<int> nums{&pool};
    nums.assign(1000, 7);
    std::cout << nums.back() << '\n';                          // 7
}   // arena memory is released all at once; monotonic deallocate() is a no-op

Remember

  • Containers allocate through their allocator parameter
  • std::pmr puts the strategy in a runtime memory_resource, keeping one container type
  • Monotonic arenas make allocation a pointer bump and free everything at once

Watch out for

  • Letting a memory resource be destroyed while a pmr container still uses it.

Check yourself

  1. What is the main advantage of std::pmr::vector<int> over std::vector<int, MyAlloc<int>>?
  2. Why is deallocate a no-op in monotonic_buffer_resource?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/memory/polymorphic_allocator

Builds on

Unlocks

Nothing builds on this yet.