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:

  1. 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.
  2. 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.
  3. 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_chunk to 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 the unsynchronized_pool_resource destructor 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.