| Taskflow: A General-purpose Task-parallel Programming System

Loading...
Searching...
No Matches
Public Member Functions | List of all members
tf::ObjectPool< T, H, LogSize > Class Template Reference

sharded fixed-size object allocator with a lock-free hot path More...

`#include <taskflow/utility/object_pool.hpp>`

## Public Member Functions
| ObjectPool ()=default
| constructs the allocator with `2^LogSize` empty shards
| ObjectPool (const ObjectPool &)=delete
| disabled copy constructor
ObjectPool & | operator= (const ObjectPool &)=delete
| disabled copy assignment operator
| ~ObjectPool ()=default
| destroys the allocator and releases all backing memory to upstream
template<typename... Args>
T * | animate (Args &&... args)
| constructs an object of type `T` in the pool and returns a pointer
void | recycle (T *obj)
| destructs the object and returns its storage to the pool
void | release ()
| returns all recycled blocks and backing memory to the system allocator

## Detailed Description

template<typename T, typename H = TaggedHead128, size_t LogSize = 5>
class tf::ObjectPool< T, H, LogSize >

sharded fixed-size object allocator with a lock-free hot path

Template Parameters

  T | object type to allocate
  H | tagged-pointer policy that controls the free-stack head representation. The type must provide: `pointer_type` — typedef for the address representation `tag_type` — typedef for the ABA counter representation `get_ptr()` — returns the stored address as `pointer_type` `get_tag()` — returns the ABA counter as `tag_type` `H(pointer_type, tag_type)` — two-argument constructor Built-in choices: tf::TaggedHead128 (default) — 16-byte head, 64-bit counter; lock-free on x86-64 and ARMv8.1+, mutex-based elsewhere. tf::TaggedHead64<> — 8-byte head, 16-bit counter; lock-free on all 64-bit platforms; requires pointers to fit in 48 bits.
  LogSize | log2 of the number of shards (default `5`, giving 32 shards); must be in [1, 15] to fit the shard index in a `uint16_t`

ObjectPool is a high-performance allocator for a single fixed-size type `T`, designed for concurrent task-parallel workloads where objects are frequently created and destroyed across many threads.

Internally, allocations are distributed across `2^LogSize` independent shards. Each shard maintains two independent components (separated by cache lines to prevent false sharing):

Hot Path (99% of operations): A lock-free Treiber stack of recycled blocks. When tf::ObjectPool::animate is called, it tries to pop a recycled block from this stack with a single atomic CAS. On success, the block is reused with no mutex acquisition. Blocks returned by tf::ObjectPool::recycle are pushed back onto this stack without acquiring any mutex.

Cold Path (1% of operations): A `std::pmr::synchronized_pool_resource` as backing storage for fresh block allocations. This mutex-protected pool is only touched when the shard's hot-path stack is empty. When accessed, it allocates a whole chunk (configured to hold up to 1024 blocks via `max_blocks_per_chunk` = 1024), amortizing the synchronization cost: one mutex acquisition yields ~1024 blocks for the hot path.

The tagged-pointer policy `H` attaches a version counter to each free-stack head to prevent the ABA problem. This counter increments on every push and pop, making ABA wrap-around effectively impossible under realistic workloads. Shards are aligned to the cache line size to eliminate false sharing between concurrent threads accessing different shards' hot-path stacks.

// default: TaggedHead128, 32 shards
tf::ObjectPool<MyTask> pool;
// lock-free on all 64-bit platforms (default PtrBits=48), 32 shards
tf::ObjectPool<MyTask, 5, tf::TaggedHead64<>> pool64;
// construct a MyTask in the pool, forwarding constructor arguments
MyTask* t = pool.animate(arg1, arg2);
// ... use t ...
// destruct and return storage to the pool for reuse
pool.recycle(t);
tf::ObjectPool
sharded fixed-size object allocator with a lock-free hot path
Definition object_pool.hpp:408
tf::ObjectPool::animate
T * animate(Args &&... args)
constructs an object of type T in the pool and returns a pointer
Definition object_pool.hpp:579
tf::ObjectPool::recycle
void recycle(T *obj)
destructs the object and returns its storage to the pool
Definition object_pool.hpp:621

Note
  All pointers returned by tf::ObjectPool::animate must be passed to tf::ObjectPool::recycle before the allocator is destroyed. Destroying the allocator with live objects is undefined behavior.

Two-Level Freelist Design

The combination of lock-free and mutex-protected freelists is deliberate: recycled blocks remain on the lock-free stack indefinitely, avoiding mutex costs on every allocation. The backing pool's internal freelist is rarely used directly because blocks do not call `deallocate()` in the normal hot path — they stay on the lock-free stack for immediate reuse. This design trades chunk-level memory reuse efficiency for atomic-fast allocation on the hot path, which is the right trade-off for task-parallel workloads where the hot path is hit millions of times.

Chunk Amortization

When the hot-path stack is empty, a single `std::pmr::synchronized_pool_resource::allocate` call acquires a mutex and either reuses a chunk or allocates a new one from the system allocator. With `max_blocks_per_chunk` = 1024, one mutex acquisition amortizes to ~1024 subsequent lock-free pops, yielding negligible mutex overhead (roughly 0.001 mutex cost per allocation).

## Constructor & Destructor Documentation

## ◆ ObjectPool() [1/2]

template<typename T, typename H = TaggedHead128, size_t LogSize = 5>

tf::ObjectPool< T, H, LogSize >::ObjectPool | ( |  | ) |
| default

constructs the allocator with `2^LogSize` empty shards

Each shard is default-constructed with an empty free stack and an uninitialized backing pool. No memory is allocated from the OS until the first call to tf::ObjectPool::animate.

## ◆ ObjectPool() [2/2]

template<typename T, typename H = TaggedHead128, size_t LogSize = 5>

tf::ObjectPool< T, H, LogSize >::ObjectPool | ( | const ObjectPool< T, H, LogSize > & |  | ) |
| delete

disabled copy constructor

ObjectPool owns its shards and backing memory; copying is not meaningful. Declare the allocator as a global or long-lived member and share it by reference or pointer.

## ◆ ~ObjectPool()

template<typename T, typename H = TaggedHead128, size_t LogSize = 5>

tf::ObjectPool< T, H, LogSize >::~ObjectPool | ( |  | ) |
| default

destroys the allocator and releases all backing memory to upstream

The destructor of each shard's `std::pmr::synchronized_pool_resource` returns all allocated chunks to the system allocator, including memory backing blocks that are currently on the free stack. No per-block destructor is called; callers are responsible for recycling all live objects before destroying the allocator.

## Member Function Documentation

## ◆ animate()

template<typename T, typename H = TaggedHead128, size_t LogSize = 5>
template<typename... Args>

T * tf::ObjectPool< T, H, LogSize >::animate | ( | Args &&... | args | ) |
| inline nodiscard

constructs an object of type `T` in the pool and returns a pointer

Template Parameters

  Args | constructor argument types

Parameters

  args | arguments forwarded to the constructor of `T`

Returns
  pointer to the newly constructed `T`; never null

On the hot path, animate pops a previously recycled block from the shard's lock-free free stack and constructs `T` in it via `std::construct_at`, with no mutex acquisition. On a cache miss (empty free stack), a fresh block is carved from the shard's backing `std::pmr::synchronized_pool_resource`, which amortizes system allocation cost over chunks of up to 1024 blocks.

Allocations are distributed across shards via a per-thread round-robin counter seeded from the thread ID hash, balancing load with zero shared state after initialization.

tf::ObjectPool<MyTask> pool;
// default-construct
MyTask* t1 = pool.animate();
// construct with arguments
MyTask* t2 = pool.animate(42, "hello");
pool.recycle(t1);
pool.recycle(t2);

Note
  The returned pointer must eventually be passed to tf::ObjectPool::recycle. Discarding it without recycling leaks both the object's resources and the underlying block.

## ◆ recycle()

template<typename T, typename H = TaggedHead128, size_t LogSize = 5>

void tf::ObjectPool< T, H, LogSize >::recycle | ( | T * | obj | ) |
| inline

destructs the object and returns its storage to the pool

Parameters

  obj | pointer to a `T` previously returned by tf::ObjectPool::animate, or `nullptr` (no-op)

recycle calls the destructor of `*obj` via `std::destroy_at`, then pushes the underlying block onto its shard's lock-free free stack without acquiring any mutex. The block becomes immediately available for the next call to tf::ObjectPool::animate on any thread.

The correct shard is identified via the `pool_id` stored in the block header, so recycle may be called from any thread regardless of which thread called animate.

tf::ObjectPool<MyTask> pool;
MyTask* t = pool.animate(arg1, arg2);
// ... use t ...
pool.recycle(t); // destructor called here; memory returned to pool
t = nullptr; // pointer is now dangling; do not dereference

Note
  Passing a pointer not obtained from this allocator is undefined behavior. After recycle returns, `obj` is a dangling pointer and must not be dereferenced.

## ◆ release()

template<typename T, typename H = TaggedHead128, size_t LogSize = 5>

void tf::ObjectPool< T, H, LogSize >::release | ( |  | ) |
| inline

returns all recycled blocks and backing memory to the system allocator

release calls `std::pmr::synchronized_pool_resource::release` on each shard's backing pool, returning all chunks to the upstream system allocator in one shot, then atomically resets each shard's free stack to null. This is an O(1) operation per shard — no per-block work is performed because the backing pool owns memory at the chunk level and frees entire chunks regardless of how many individual blocks were returned to it.

After this call the allocator is in the same state as after construction: empty free stacks, no memory held from the OS.

This method is optional and is not required before destruction. It is useful for reclaiming pool memory between distinct workload phases without destroying the allocator itself.

tf::ObjectPool<MyTask> pool;
// --- phase 1 ---
for (auto& task : phase1_tasks) {
MyTask* t = pool.animate(task);
// ... run t ...
pool.recycle(t);
}
pool.release(); // return OS memory before phase 2 begins
// --- phase 2 ---
for (auto& task : phase2_tasks) {
MyTask* t = pool.animate(task);
// ... run t ...
pool.recycle(t);
}
tf::ObjectPool::release
void release()
returns all recycled blocks and backing memory to the system allocator
Definition object_pool.hpp:670

Note
  All live objects must be recycled before calling release. Calling release while objects are still alive is undefined behavior because the backing memory they reside in is freed.

The documentation for this class was generated from the following file:

- taskflow/utility/object_pool.hpp