std::pmr::unsynchronized_pool_resource Bucket Management
This article explains how the C++ polymorphic memory resource
std::pmr::unsynchronized_pool_resource manages fixed-size
memory buckets to accelerate allocations and mitigate fragmentation. It
covers the internal structure of memory pools, how allocation requests
are routed to specific size classes, how chunk replenishment works via
upstream resources, and how deallocations are processed without
thread-synchronization overhead.
The Pool and Bucket Architecture
std::pmr::unsynchronized_pool_resource organizes memory
into a collection of distinct pools, commonly referred to as "buckets."
Each bucket is dedicated to serving allocation requests for a specific,
fixed block size.
When initialized, the resource configures a geometric or stepped progression of block sizes across these buckets (for example: 8, 16, 32, 64, 128, and up to a designated maximum block size). Rather than allocating individual blocks directly from the system heap, each bucket manages memory in large contiguous buffers called chunks.
Routing Allocations to Buckets
When allocate(bytes, alignment) is called, the resource
determines how to fulfill the request based on the requested size:
- Size-Class Matching: The resource searches its array of buckets to find the smallest bucket whose block size can satisfy both the requested byte count and alignment constraint.
- Bucket Allocation: If the request size is less than
or equal to
largest_required_pool_block, it is assigned to that bucket's free list. - Oversized Requests: If the requested size exceeds
the capacity of the largest bucket, the resource bypasses the bucket
architecture entirely and forwards the allocation directly to the
upstream memory resource (such as
std::pmr::new_delete_resource()).
Chunk Allocation and Free Lists
Each bucket maintains an internal free list of unused blocks using an intrusive linked-list design:
- Intrusive Storage: Because an unallocated block is unused memory, the bucket stores metadata—such as pointers to the next free block—directly inside the payload area of the idle block itself, eliminating separate per-block allocation overhead.
- Serving a Request: Serving a block from an existing chunk is an \(O(1)\) operation that simply pops the head element off the bucket's intrusive free list.
- Replenishing Empty Buckets: When a bucket's free
list is exhausted, the resource requests a new, larger chunk of
contiguous memory from its configured upstream resource. The size of
these chunks typically increases geometrically up to
max_blocks_per_chunkto amortize upstream allocation costs over time. Once fetched, the new chunk is subdivided into uniform blocks of that bucket's designated size, and those blocks are chained onto the bucket's free list.
Deallocation Workflow
When memory is released via
deallocate(ptr, bytes, alignment):
- Bucket Retrieval: The resource identifies which
bucket the pointer belongs to based on the size passed to
deallocate. - Pushing to Free List: The block is pushed back to the head of that bucket's intrusive free list in an \(O(1)\) operation.
- Deferred Upstream Release: Memory is generally not
returned to the upstream allocator immediately upon individual block
deallocation. Instead, chunks remain allocated to their respective
buckets for fast reuse in future allocations. The entire accumulated
memory is released back to the upstream resource only when
release()is called or when theunsynchronized_pool_resourcedestructor executes.
Customization with
std::pmr::pool_options
Developers can fine-tune bucket allocation behavior by passing a
std::pmr::pool_options structure during construction:
max_blocks_per_chunk: Limits the maximum number of blocks a single chunk can hold, preventing individual pools from claiming disproportionately large blocks of contiguous memory from the upstream allocator.largest_required_pool_block: Sets the upper boundary for bucket sizes. Any allocation request exceeding this value will bypass the pooled buckets and go straight to the upstream allocator.
The Unsynchronized Advantage
Unlike std::pmr::synchronized_pool_resource, the
unsynchronized variant does not use mutexes, atomics, or thread-locking
mechanisms to guard bucket structures. Free list traversal, chunk
acquisition, and block carving occur with zero synchronization overhead,
making this bucket management strategy ideal for thread-confined
workloads, local stack allocators, and single-threaded
performance-critical paths.