C++ std::pmr Complete Guide: Boost Performance 10x with
Introduction: When malloc/new Becomes the Bottleneck
”Too many allocations/deallocations”
When small objects are frequently allocated and deallocated, heap fragmentation (scattered small free spaces making large contiguous allocations difficult) and allocator overhead accumulate. Profilers often show malloc/free consuming 20-30% of execution time. Memory pools pre-allocate large blocks and distribute slices from them, reducing allocation count. C++17 std::pmr (polymorphic memory resources) provides injectable allocators based on std::memory_resource, allowing containers of the same type to use different pools. What this guide covers:
- Problem scenarios: When malloc dominates your profile
- std::pmr complete examples: monotonic_buffer_resource, pool_resource, custom memory_resource, pmr containers
- Common errors and solutions: Lifetime management, resource mixing, alignment
- Best practices: Pool selection guide, incremental adoption
- Production patterns: Frame pools, request-scoped arenas, game entities
Conceptual analogy
Memory pools and PMR are like pre-dividing warehouse bins and taking items only when needed. When allocation/deallocation patterns are predictable, this approach is more cache-friendly than global new.
Table of Contents
- Problem Scenarios: When malloc is the Bottleneck
- Memory Pool Concepts
- std::pmr Overview
- monotonic_buffer_resource Complete Examples
- pool_resource Complete Examples
- Custom memory_resource Implementation
- pmr Containers in Practice
- Common Errors and Solutions
- Best Practices
- Performance Benchmarks
- Production Patterns
- Summary
1. Problem Scenarios: When malloc is the Bottleneck
Real-world situations
"Profiler shows malloc/free taking 30% of total execution time."
"Allocating tens of thousands of small objects causes heap fragmentation and OOM."
"Creating/deleting entities every game frame causes severe frame drops."
"Parsing HTTP requests with many allocations causes latency spikes under load."
"Parser repeatedly creates std::vector, std::map causing allocation explosion."
Root causes
- Excessive allocation count: Repeated alloc/free of small objects → malloc overhead accumulation
- Heap fragmentation: Scattered small free spaces → large contiguous allocation failures
- Cache misses: Physically scattered allocations → poor cache efficiency
- Lock contention: Global heap lock contention in multithreaded scenarios Memory pools mitigate issues 1-3; thread-local pools mitigate issue 4.
Solution by scenario
| Scenario | Characteristics | Recommended Resource |
|---|---|---|
| Game frame | Create/destroy per frame, short lifetime | monotonic_buffer_resource + release() |
| HTTP request | Allocate per request, free at request end | monotonic_buffer_resource |
| Node-based structures | Repeated fixed-size node alloc/free | synchronized_pool_resource |
| Long-lived objects | Long lifetime, varying sizes | Default heap or pool_resource |
| Parsing/serialization | Many temporary buffers, free at scope end | monotonic_buffer_resource |
Before/After: HTTP Parser Example
Before (default allocation): Every request allocates vector, map, string from heap repeatedly.
// ❌ Default allocation — many malloc calls
// 실행 예제
void handleRequest(const std::string& raw) {
std::vector<std::string> path_segments;
std::map<std::string, std::string> headers;
std::string body;
parsePath(raw, path_segments);
parseHeaders(raw, headers);
parseBody(raw, body);
process(path_segments, headers, body);
}
After (pmr applied): Allocate from request-scoped pool, deallocate all at once when function exits.
// ✅ pmr — reduced allocation count & fragmentation
void handleRequest(const std::string& raw) {
std::pmr::monotonic_buffer_resource request_pool;
std::pmr::vector<std::pmr::string> path_segments(&request_pool);
std::pmr::map<std::pmr::string, std::pmr::string, std::less<>>
headers(&request_pool);
std::pmr::string body(&request_pool);
parsePath(raw, path_segments);
parseHeaders(raw, headers);
parseBody(raw, body);
process(path_segments, headers, body);
} // request_pool destroyed → all allocations freed at once
2. Memory Pool Concepts
Reducing allocation count
- Pools allocate large memory blocks once (or a few times), then distribute fixed-size or variable-size slots from them. On deallocation, blocks are returned to the pool, and actual free happens only once when the pool is destroyed.
- Fixed-size pools: For same-sized objects (node pools, event pools) — simple and minimal fragmentation.
- Variable-size: Supporting multiple sizes complicates slot management. monotonic_buffer_resource uses a “cut from front only, no individual frees” approach — simple to implement and perfect for frame/request-scoped resets.
Memory pool operation flow
flowchart TB
subgraph Heap[Global Heap]
H[Allocate large block once]
end
subgraph Pool[Memory Pool]
B[Block]
B --> S1[Slot 1]
B --> S2[Slot 2]
B --> S3[Slot 3]
end
H --> B
S1 --> A1[Object A]
S2 --> A2[Object B]
S3 --> A3[Object C]
monotonic vs pool comparison
flowchart LR
subgraph Mono[monotonic_buffer_resource]
M1[Alloc 1] --> M2[Alloc 2] --> M3[Alloc 3]
M3 -.->|release returns all| M1
end
subgraph Pool[pool_resource]
P1[Slot] <--> P2[Slot]
P2 <--> P3[Slot]
end
HTTP request processing sequence (with pmr)
sequenceDiagram
participant Req as handleRequest
participant Pool as monotonic_buffer_resource
participant Vec as pmr::vector
participant Map as pmr::map
Req->>Pool: Create (scope entry)
Req->>Vec: path_segments(&pool)
Req->>Map: headers(&pool)
Req->>Vec: push_back, parsePath...
Req->>Map: insert, parseHeaders...
Req->>Req: process()
Req->>Pool: Destroy (scope exit)
Note over Pool: All allocations freed at once
3. std::pmr Overview
Why std::pmr?
Traditional std::allocator is a template type fixed at compile time. std::vector<int, MyAllocator<int>> and std::vector<int, OtherAllocator<int>> are different types, making it difficult to pass them to the same function. In contrast, std::pmr::polymorphic_allocator points to a memory_resource* at runtime, so the same std::pmr::vector<int> type can use different pools. This allows APIs to standardize on std::pmr::vector while callers simply switch pools.
Polymorphic Allocator
- std::pmr::memory_resource is a pure virtual interface defining only: allocate, deallocate, is_equal. Implementations (pool_resource, monotonic_buffer_resource, etc.) can be selected at runtime.
- std::pmr::polymorphic_allocator<T> is an allocator pointing to this memory_resource. Pass it to containers like std::vector<T, std::pmr::polymorphic_allocator<T>> to allocate from that resource.
- std::pmr::vector is an alias for std::vector<T, std::pmr::polymorphic_allocator<T>>. Its constructor accepts a memory_resource* to use that pool.
std::pmr architecture diagram
flowchart TB
subgraph Container[pmr containers]
V[std::pmr::vector]
M[std::pmr::map]
S[std::pmr::string]
end
subgraph Alloc[polymorphic_allocator]
PA[memory_resource*]
end
subgraph Resources[memory_resource implementations]
MONO[monotonic_buffer_resource]
POOL[synchronized_pool_resource]
CUSTOM[Custom resource]
end
subgraph Backend[Backend]
HEAP[Global heap]
BUF[Stack/static buffer]
end
V --> PA
M --> PA
S --> PA
PA --> MONO
PA --> POOL
PA --> CUSTOM
MONO --> BUF
POOL --> HEAP
CUSTOM --> HEAP
Build and run
# Requires C++17 or later
g++ -std=c++17 -O2 -o pmr_demo pmr_demo.cpp
./pmr_demo
Basic usage example
#include <memory_resource>
#include <vector>
int main() {
char buffer[1024];
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
std::pmr::vector<int> v(&pool);
v.push_back(1);
v.push_back(2);
// v's allocations come from buffer
}
4. monotonic_buffer_resource Complete Examples
Concept: Forward-only allocation
- std::pmr::monotonic_buffer_resource: Sequentially slices memory from a given buffer (or upstream resource). deallocate is a no-op; individual frees don’t happen. Use release() to reset everything. Perfect for frame buffers and request scopes.
Example 1: Stack buffer + monotonic (zero heap allocations)
#include <memory_resource>
#include <vector>
#include <array>
void processRequest() {
// Allocate 64KB buffer on stack — no heap usage
std::array<std::byte, 65536> stack_buffer;
std::pmr::monotonic_buffer_resource pool{
stack_buffer.data(), stack_buffer.size(),
std::pmr::new_delete_resource() // Use heap on overflow
};
std::pmr::vector<int> ids(&pool);
std::pmr::vector<std::pmr::string> tokens(&pool);
for (int i = 0; i < 1000; ++i) {
ids.push_back(i);
tokens.push_back("token");
}
// Function exit → stack_buffer and pool automatically freed
}
Example 2: Frame pool + release()
#include <memory_resource>
#include <vector>
struct Entity { int id; float x, y; };
struct Component { int type; void* data; };
void gameLoop() {
std::array<std::byte, 1024*1024> frame_buffer;
std::pmr::monotonic_buffer_resource frame_pool{
frame_buffer.data(), frame_buffer.size(),
std::pmr::new_delete_resource()
};
while (running) {
frame_pool.release(); // Reset previous frame memory
std::pmr::vector<Entity> entities(&frame_pool);
std::pmr::vector<Component> components(&frame_pool);
// ... create entities, frame logic ...
}
}
Example 3: Multiple pmr containers sharing same pool
#include <memory_resource>
#include <vector>
#include <string>
#include <map>
int main() {
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> nums(&pool);
std::pmr::vector<std::pmr::string> names(&pool);
std::pmr::map<std::pmr::string, int, std::less<>> scores(&pool);
nums.push_back(42);
names.push_back("Alice");
scores[Bob] = 100;
// All allocations from pool
}
5. pool_resource Complete Examples
Concept: Reusing fixed-size blocks
- std::pmr::synchronized_pool_resource / unsynchronized_pool_resource: Manage fixed-size block pools and reuse freed blocks. Use pool_options to configure block size, max block count, etc.
Example 1: Configuring block size with pool_options
#include <memory_resource>
int main() {
std::pmr::pool_options opts;
opts.max_blocks_per_chunk = 32; // Max blocks per chunk
opts.largest_required_pool_block = 256; // Max block size
std::pmr::synchronized_pool_resource pool{opts};
// Suitable for objects ≤256 bytes, thread-safe
}
Example 2: Applying pool to node-based data structures
#include <memory_resource>
#include <list>
struct TreeNode {
int value;
TreeNode* left = nullptr;
TreeNode* right = nullptr;
};
void buildTree(std::pmr::memory_resource* mr) {
std::pmr::list<TreeNode> nodes(mr);
for (int i = 0; i < 1000; ++i) {
nodes.push_back(TreeNode{i});
}
// Nodes allocated from pool, returned to pool on list destruction
}
int main() {
std::pmr::synchronized_pool_resource pool;
buildTree(&pool);
}
Example 3: unsynchronized_pool (single-thread only)
#include <memory_resource>
void singleThreadWork() {
// Single thread only — no lock overhead
std::pmr::unsynchronized_pool_resource pool;
std::pmr::vector<int> v(&pool);
for (int i = 0; i < 10000; ++i) {
v.push_back(i);
}
}
6. Custom memory_resource Implementation
Example 1: Logging memory_resource
A debugging resource that logs allocations/deallocations.
#include <memory_resource>
#include <iostream>
#include <cstddef>
class logging_memory_resource : public std::pmr::memory_resource {
public:
explicit logging_memory_resource(std::pmr::memory_resource* upstream
= std::pmr::get_default_resource())
: upstream_(upstream) {}
private:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
void* p = upstream_->allocate(bytes, alignment);
std::cout << "[alloc] " << bytes << " bytes, align " << alignment
<< " -> " << p << "\n";
return p;
}
void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
std::cout << "[dealloc] " << bytes << " bytes @ " << p << "\n";
upstream_->deallocate(p, bytes, alignment);
}
[[nodiscard]] bool do_is_equal(
const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
std::pmr::memory_resource* upstream_;
};
Example 2: Statistics-collecting memory_resource
#include <memory_resource>
#include <atomic>
#include <cstddef>
class stats_memory_resource : public std::pmr::memory_resource {
public:
explicit stats_memory_resource(std::pmr::memory_resource* upstream
= std::pmr::get_default_resource())
: upstream_(upstream) {}
std::size_t allocation_count() const noexcept { return alloc_count_.load(); }
std::size_t total_allocated() const noexcept { return total_allocated_.load(); }
std::size_t peak_allocated() const noexcept { return peak_allocated_.load(); }
private:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
void* p = upstream_->allocate(bytes, alignment);
alloc_count_.fetch_add(1);
std::size_t prev = total_allocated_.fetch_add(bytes);
std::size_t current = prev + bytes;
for (std::size_t peak = peak_allocated_.load();
current > peak && !peak_allocated_.compare_exchange_weak(peak, current);
peak = peak_allocated_.load()) {}
return p;
}
void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
total_allocated_.fetch_sub(bytes);
upstream_->deallocate(p, bytes, alignment);
}
[[nodiscard]] bool do_is_equal(
const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
std::pmr::memory_resource* upstream_;
std::atomic<std::size_t> alloc_count_{0};
std::atomic<std::size_t> total_allocated_{0};
std::atomic<std::size_t> peak_allocated_{0};
};
Example 3: Thread-local monotonic pool
#include <memory_resource>
#include <vector>
#include <thread>
thread_local std::pmr::monotonic_buffer_resource* tls_pool = nullptr;
void initThreadPool() {
tls_pool = new std::pmr::monotonic_buffer_resource(
std::pmr::new_delete_resource());
}
void cleanupThreadPool() {
delete tls_pool;
tls_pool = nullptr;
}
std::pmr::memory_resource* getThreadPool() {
if (!tls_pool) initThreadPool();
return tls_pool;
}
void worker(int id) {
std::pmr::vector<int> local_data(getThreadPool());
for (int i = 0; i < 1000; ++i) {
local_data.push_back(i * id);
}
}
Example 4: Fixed-block pool (simple implementation)
#include <memory_resource>
#include <vector>
#include <cstddef>
class fixed_block_pool : public std::pmr::memory_resource {
public:
explicit fixed_block_pool(std::size_t block_size, std::size_t block_count = 64)
: block_size_(block_size), blocks_(block_count) {
storage_.resize(block_size * block_count);
for (std::size_t i = 0; i < block_count; ++i) {
free_list_.push_back(storage_.data() + i * block_size);
}
}
private:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
if (bytes > block_size_) return nullptr;
if (free_list_.empty()) return nullptr;
void* p = free_list_.back();
free_list_.pop_back();
return p;
}
void do_deallocate(void* p, std::size_t bytes, std::size_t alignment) override {
if (p >= storage_.data() && p < storage_.data() + storage_.size()) {
free_list_.push_back(static_cast<std::byte*>(p));
}
}
[[nodiscard]] bool do_is_equal(
const std::pmr::memory_resource& other) const noexcept override {
return this == &other;
}
std::size_t block_size_;
std::vector<std::byte> storage_;
std::vector<std::byte*> blocks_;
std::vector<std::byte*> free_list_;
};
7. pmr Containers in Practice
Supported pmr containers
| Container | Alias | Use Case |
|---|---|---|
| std::pmr::string | vector<char, polymorphic_allocator<char>> | Strings |
| std::pmr::vector | vector<T, polymorphic_allocator<T>> | Dynamic arrays |
| std::pmr::map | map<K,V,…,polymorphic_allocator<pair<…>>> | Sorted maps |
| std::pmr::set | set<T,…,polymorphic_allocator<T>> | Sorted sets |
| std::pmr::unordered_map | unordered_map with pmr allocator | Hash maps |
| std::pmr::list | list<T, polymorphic_allocator<T>> | Doubly-linked lists |
Nested pmr containers: map<string, vector<string>>
#include <memory_resource>
#include <vector>
#include <string>
#include <map>
void parseConfig(const char* raw) {
std::pmr::monotonic_buffer_resource pool;
// Both map keys and values are pmr::string
// If value is vector<pmr::string>, strings inside also use pool
std::pmr::map<std::pmr::string, std::pmr::vector<std::pmr::string>, std::less<>>
config(&pool);
config[sections].push_back("a");
config[sections].push_back("b");
config[keys].push_back("x");
// All allocations from pool
}
pmr string caution: Don’t mix with std::string
// ❌ Dangerous: mixing std::string and std::pmr::string
std::pmr::map<std::pmr::string, std::string, std::less<>> m(&pool);
// value is std::string → uses default allocator, not pool
// ✅ Correct: all pmr
std::pmr::map<std::pmr::string, std::pmr::string, std::less<>> m(&pool);
8. Common Errors and Solutions
Error 1: Container outlives pool (Use-After-Free)
Symptoms: Crash, undefined behavior, heap corruption.
Cause: memory_resource destroyed before containers using it.
// ❌ Wrong code
std::pmr::vector<int>* createVector() {
std::pmr::monotonic_buffer_resource pool;
return new std::pmr::vector<int>(&pool); // pool destroyed on function exit!
}
// Returned vector points to freed pool → UB
Solution:
// ✅ Correct: pool lifetime > container lifetime
std::pmr::monotonic_buffer_resource* pool = new std::pmr::monotonic_buffer_resource;
std::pmr::vector<int>* vec = new std::pmr::vector<int>(pool);
// Manage lifetimes to ensure pool outlives vec
Or keep pool and container in same scope:
// ✅ Pool and container lifetimes match
void process() {
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> vec(&pool);
// ... use ...
} // vec, pool destroyed in order
Error 2: Expecting deallocate from monotonic
Symptoms: Memory not freed, keeps accumulating.
Cause: monotonic_buffer_resource’s deallocate is a no-op.
// ❌ Wrong expectation
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> v(&pool);
v.push_back(1);
v.pop_back(); // Internally calls deallocate, but pool doesn't free
// Memory remains in pool until release() called
Solution: Use monotonic only for release() full reset.
// ✅ monotonic: release at scope end
{
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> v(&pool);
v.push_back(1);
v.pop_back();
pool.release(); // Full pool reset at this point
}
Error 3: Copying/moving containers from different resources
Symptoms: Crash or memory corruption.
// ❌ Dangerous code
std::pmr::monotonic_buffer_resource pool1, pool2;
std::pmr::vector<int> a(&pool1);
a.push_back(42);
std::pmr::vector<int> b(&pool2);
b = a; // a's allocator copied to b. b uses pool2 but data from pool1
// b destruction passes pool1 pointer to pool2.deallocate → UB
Solution: Only copy/move containers using same resource.
// ✅ Same pool
std::pmr::monotonic_buffer_resource pool;
std::pmr::vector<int> a(&pool);
std::pmr::vector<int> b(&pool);
a.push_back(42);
b = a; // Both use pool → safe
Error 4: Insufficient stack buffer size
Symptoms: monotonic allocates additional memory from upstream (heap).
// ❌ Buffer may be too small
char buffer[256];
std::pmr::monotonic_buffer_resource pool{buffer, sizeof(buffer)};
std::pmr::vector<int> v(&pool);
for (int i = 0; i < 1000; ++i) v.push_back(i); // Exceeds 256 bytes → uses heap
Solution: Allocate generous buffer size.
// ✅ Generous buffer
std::array<std::byte, 65536> buffer;
std::pmr::monotonic_buffer_resource pool{
buffer.data(), buffer.size(),
std::pmr::new_delete_resource()
};
Error 5: Inappropriate pool_options settings
Symptoms: Memory waste or allocation failures with synchronized_pool_resource.
Solution: Configure based on actual maximum block size used.
// ✅ Settings matching usage pattern
std::pmr::pool_options opts;
opts.largest_required_pool_block = 64; // Mostly ≤64-byte objects
opts.max_blocks_per_chunk = 128;
std::pmr::synchronized_pool_resource pool{opts};
Error 6: Ignoring alignment
Symptoms: Crash or SIGBUS on certain platforms.
// ❌ Wrong implementation (ignoring alignment)
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
return upstream_->allocate(bytes, 1); // Ignoring alignment!
}
Solution: Always pass requested alignment.
// ✅ Respecting alignment
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
return upstream_->allocate(bytes, alignment);
}
Error 7: Thread safety misconceptions
Symptoms: Crash or data corruption in multithreaded scenarios.
// ❌ Dangerous: multiple threads sharing same unsynchronized_pool
// 실행 예제
std::pmr::unsynchronized_pool_resource pool;
std::thread t1([&] { std::pmr::vector<int> v(&pool); /* ... */ });
std::thread t2([&] { std::pmr::vector<int> v(&pool); /* ... */ });
Solution: Use synchronized_pool_resource for shared pools in multithreaded code.
// ✅ synchronized_pool_resource (thread-safe)
std::pmr::synchronized_pool_resource pool;
9. Best Practices
1. Pool selection guide
| Pattern | Recommended | Reason |
|---|---|---|
| Frame/request unit, bulk free | monotonic | No deallocate, reset with release() |
| Individual object repeated alloc/free | pool_resource | Block reuse |
| Thread-local independent allocation | Thread-local monotonic | Eliminates lock contention |
| Debugging/profiling | Wrap with stats_memory_resource | Track allocation count & peak |
2. Pool lifetime rules
Always: pool lifetime ≥ all containers using it
3. Use pmr for nested container elements too
// ✅ Both map keys and values are pmr types
std::pmr::map<std::pmr::string, std::pmr::vector<int>, std::less<>> m(&pool);
4. Apply after profiling
// Step 1: Identify bottleneck with profiling
// Step 2: Introduce pool only in that function/scope
// Step 3: Measure performance, then expand application
5. Use statistics resource in debug builds
#ifdef NDEBUG
std::pmr::memory_resource* resource = std::pmr::get_default_resource();
#else
static stats_memory_resource stats{std::pmr::get_default_resource()};
std::pmr::memory_resource* resource = &stats;
#endif
std::pmr::vector<int> data(resource);
// ... processing ...
#ifndef NDEBUG
std::cout << "Allocations: " << stats.allocation_count()
<< ", Peak: " << stats.peak_allocated() << " bytes\n";
#endif
10. Performance Benchmarks
Benchmark 1: Default allocation vs monotonic vs pool
#include <chrono>
#include <memory_resource>
#include <vector>
#include <iostream>
void benchmarkAllocators() {
constexpr size_t N = 100000;
constexpr size_t elem_size = 32;
// 1. Default heap allocation
auto t1 = std::chrono::high_resolution_clock::now();
{
std::vector<int> v;
v.reserve(N);
for (size_t i = 0; i < N; ++i) v.push_back(static_cast<int>(i));
}
auto t2 = std::chrono::high_resolution_clock::now();
// 2. monotonic_buffer_resource
std::vector<std::byte> buffer(N * elem_size * 2);
std::pmr::monotonic_buffer_resource mono{
buffer.data(), buffer.size(),
std::pmr::new_delete_resource()
};
auto t3 = std::chrono::high_resolution_clock::now();
{
std::pmr::vector<int> v(&mono);
v.reserve(N);
for (size_t i = 0; i < N; ++i) v.push_back(static_cast<int>(i));
}
auto t4 = std::chrono::high_resolution_clock::now();
// 3. synchronized_pool_resource
std::pmr::synchronized_pool_resource pool;
auto t5 = std::chrono::high_resolution_clock::now();
{
std::pmr::vector<int> v(&pool);
v.reserve(N);
for (size_t i = 0; i < N; ++i) v.push_back(static_cast<int>(i));
}
auto t6 = std::chrono::high_resolution_clock::now();
using namespace std::chrono;
auto d_default = duration_cast<microseconds>(t2 - t1).count();
auto d_mono = duration_cast<microseconds>(t4 - t3).count();
auto d_pool = duration_cast<microseconds>(t6 - t5).count();
std::cout << "Default: " << d_default << " μs\n";
std::cout << "Monotonic: " << d_mono << " μs (" << (double)d_default/d_mono << "x)\n";
std::cout << "Pool: " << d_pool << " μs (" << (double)d_default/d_pool << "x)\n";
}
Expected results:
| Allocator | 100k push_back (μs) | Relative Speed |
|---|---|---|
| Default vector | 2000~5000 | 1x |
| monotonic | 500~1500 | 2~4x |
| synchronized_pool | 800~2000 | 1.5~3x |
Benchmark summary table
| Scenario | Default Heap | monotonic | pool_resource | Notes |
|---|---|---|---|---|
| Sequential push_back (no frees) | 1x | 2~4x | 1.5~3x | monotonic wins |
| Repeated alloc/free (fixed size) | 1x | Not suitable | 2~5x | pool wins |
| High thread contention | 1x | Good with per-thread | Sync overhead | Thread-local monotonic |
11. Production Patterns
Pattern 1: Game frame pool
void gameLoop() {
std::array<std::byte, 1024*1024> frame_buffer;
while (running) {
std::pmr::monotonic_buffer_resource frame_pool{
frame_buffer.data(), frame_buffer.size(),
std::pmr::new_delete_resource()
};
std::pmr::vector<Entity> entities(&frame_pool);
std::pmr::vector<Component> components(&frame_pool);
// ... create entities, frame logic ...
} // frame_pool, entities destroyed → same buffer reused next frame
}
Pattern 2: HTTP request scope
void handleRequest(const Request& req) {
std::pmr::monotonic_buffer_resource request_pool(
std::pmr::new_delete_resource());
std::pmr::vector<std::pmr::string> path_segments(&request_pool);
std::pmr::map<std::pmr::string, std::pmr::string, std::less<>>
headers(&request_pool);
parsePath(req.uri, path_segments);
parseHeaders(req.raw_headers, headers);
Response response = processRequest(path_segments, headers);
sendResponse(response);
} // request_pool destroyed → all allocations freed
Pattern 3: Per-worker pool in thread pool
class Worker {
std::pmr::monotonic_buffer_resource worker_pool_;
std::pmr::vector<Task> local_queue_{&worker_pool_};
public:
Worker() : worker_pool_(std::pmr::new_delete_resource()) {}
void process(Task t) {
worker_pool_.release();
local_queue_.clear();
// Process t using local_queue_ allocations
}
};
Pattern 4: Hierarchical resources (upstream)
std::pmr::synchronized_pool_resource global_pool;
std::pmr::monotonic_buffer_resource thread_pool{&global_pool};
std::pmr::monotonic_buffer_resource frame_pool{&thread_pool};
std::pmr::vector<int> frame_data(&frame_pool);
// frame_pool exhausted → thread_pool → global_pool
Pattern 5: Implementation checklist
- Pool lifetime exceeds all containers using it
- Clear release() timing for monotonic (frame/request end)
- No copy/assignment between pmr containers using different pools
- Generous stack buffer size when used
- pool_options match actual usage patterns
- Use
synchronized_pool_resourceor per-thread pools for multithreading
12. Summary
| Topic | Summary |
|---|---|
| Memory pools | Allocate large block once, distribute slots — reduces allocation count & fragmentation |
| std::pmr | memory_resource + polymorphic_allocator for injecting pools into containers |
| monotonic | Sequential allocation, reset only — perfect for frame/request scopes |
| pool_resource | Fixed-size block reuse — suitable for general object pools |
| Custom resource | Inherit memory_resource for logging, statistics, thread-local pools |
| Core principles: |
- Pool lifetime > container lifetime
- monotonic frees only via
release() - Beware copy/assignment between containers from different pools
- Apply after profiling confirms actual benefits
Related Articles
- C++ Modern Memory Management: Custom Allocators & std::pmr Guide
- C++ Cache-Efficient Code: Data-Oriented Design Guide
- C++ std::chrono Complete Guide | duration, time_point, clocks, time measurement
- C++ SIMD & Parallelization: std::execution & Intrinsics Guide
- C++ Memory Management Complete Guide | Allocators, Pools, Arenas, Production Patterns [#55-5]
Practical Checklist
Items to verify when applying these concepts in production.
Before writing code
- Is this technique the best solution for the current problem?
- Can team members understand and maintain this code?
- Does it meet performance requirements?
While writing code
- Have all compiler warnings been resolved?
- Have edge cases been considered?
- Is error handling appropriate?
During code review
- Is the code’s intent clear?
- Are test cases sufficient?
- Is it documented? Use this checklist to reduce mistakes and improve code quality.
Keywords
std::pmr, polymorphic memory resource, memory pool, monotonic_buffer_resource, pool_resource, polymorphic_allocator, pmr containers, performance optimization
Frequently Asked Questions (FAQ)
Q. When do I use this in production?
A. When profiling shows malloc/free dominating execution time, when allocating many short-lived objects per frame/request (games, HTTP servers), or when node-based data structures repeatedly allocate fixed-size blocks.
Q. monotonic vs pool_resource?
A. Use monotonic for “allocate many, free all at once” patterns (frames/requests). Use pool_resource for repeated individual alloc/free patterns with fixed sizes.
Q. What should I read first?
A. Check the C++ Series Index for the complete flow. Reading #39-1 Cache & Data-Oriented Design first is recommended.
Q. How to study deeper?
A. Refer to cppreference std::pmr and P0339R6 specification. One-line summary: std::pmr enables control over memory pools and fragmentation. Next, read SIMD & Intrinsics (#39-3). Previous: High-Performance C++ #39-1: Cache & Data-Oriented Design Next: High-Performance C++ #39-3: SIMD & Parallelization
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Master C++ std::pmr to achieve 10x memory allocation performance.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ 현대적 메모리 관리: 커스텀 알로케이터 제작과 std::pmr 가이드
- C++ 메모리 풀 완벽 가이드 | 객체 풀·슬랩·아레나·std::pmr 실전 [#32-2]
- C++ 커스텀 메모리 할당자(Memory Pool) 제작기 [#48-3]
Keywords Covered in This Article (Related Search Terms)
This article covers C++, memory management, std::pmr, memory pool, memory_resource, polymorphic_allocator, monotonic_buffer_resource, performance optimization.