Skip to content
 
 

Repository files navigation

obj-pool

crates.io docs.rs License

A typed object pool with compact 32-bit IDs, optional serde support, and a sharded parallel variant for concurrent workloads.

Why obj-pool?

When building self-referential data structures (graphs, trees, linked lists) in Rust you have a few options:

  1. Unsafe pointer manipulation.
  2. Rc<RefCell<T>> — safe but verbose and allocation-heavy.
  3. A plain Vec<T> with index-based access.

ObjPool<T> is a polished version of option 3. It handles slot reuse, provides a typed ObjId handle instead of a raw usize, and catches bugs in debug builds.

Highlights

  • Compact IDsObjId wraps NonZeroU32, so Option<ObjId> is 4 bytes with no extra space (niche optimization). A slab key is a full usize (8 bytes on 64-bit).
  • Debug-mode safety — in debug builds every pool mixes a random pool-specific offset into each ObjId it issues, so accidental cross-pool access is rejected (get returns None, indexing panics) instead of silently returning wrong data. Release builds skip the masking entirely.
  • Parallel poolParObjPool<T, S> shards S inner pools behind RwLocks for concurrent insert/remove/lookup without a global lock.
  • Optional serde — enable the serde_support feature to serialize/deserialize ObjId.

Usage

[dependencies]
obj-pool = "0.7"

# with serde:
obj-pool = { version = "0.7", features = ["serde_support"] }

Single-threaded pool

use obj_pool::{ObjPool, ObjId};

let mut pool: ObjPool<String> = ObjPool::new();

let a: ObjId = pool.insert("hello".to_string());
let b: ObjId = pool.insert("world".to_string());

println!("{} {}", pool[a], pool[b]);

pool.remove(a);

// Slot `a` is reused for the next insert.
let c: ObjId = pool.insert("reused".to_string());

Parallel pool

ParObjPool<T, S> distributes objects across S shards. ObjIds are self-contained — the shard index is encoded in the upper bits, so callers need no knowledge of the sharding. The number of bits reserved for the shard index is the smallest able to represent S shards, so each shard can hold up to 2^(32 - ceil(log2(S))) - 1 objects (e.g. ~268M per shard for S = 16).

use obj_pool::ParObjPool;
use std::sync::Arc;

let pool: Arc<ParObjPool<u64, 16>> = Arc::new(ParObjPool::new());

let id = pool.insert(42);

// Blocking read — returns a mapped read-guard.
assert_eq!(*pool.get(id).unwrap(), 42);

// Non-blocking — returns None if the shard lock is contended.
assert_eq!(pool.try_get(id).map(|v| *v), Some(42));

Comparison with slab

slab is the most commonly used arena crate in the Rust ecosystem. The table below summarises the differences, followed by benchmark numbers.

Feature comparison

obj-pool slab
Key type ObjId (NonZeroU32, 4 bytes) usize (8 bytes)
Option<Key> size 4 bytes (niche) 16 bytes
Cross-pool debug check yes no
Concurrent variant ParObjPool no
Serde optional feature no

Benchmarks

Measured on Apple M-series (aarch64), optimized build (cargo bench). Each cell shows the median time for the operation over the full collection.

Insert (pre-allocated capacity)

N obj-pool slab winner
100 122 ns 194 ns obj-pool −37%
1 000 1.12 µs 2.02 µs obj-pool −45%
10 000 10.3 µs 20.8 µs obj-pool −50%
100 000 95.5 µs 206 µs obj-pool −54%

obj-pool's Vacant links are u32 (4 bytes) rather than usize (8 bytes), so the free-list nodes fit in half the space, halving cache pressure during sequential inserts.

Get (read all occupied slots)

N obj-pool slab winner
100 48 ns 44 ns tie
1 000 435 ns 427 ns tie
10 000 4.71 µs 4.68 µs tie
100 000 46.5 µs 50.3 µs obj-pool −8%

Effectively identical at all sizes; both resolve to the same machine code after inlining.

Remove + reinsert (free-list / slot reuse)

N obj-pool slab winner
100 132 ns 254 ns obj-pool −48%
1 000 1.05 µs 2.21 µs obj-pool −52%
10 000 9.27 µs 16.5 µs obj-pool −44%
100 000 147 µs 237 µs obj-pool −38%

Same cause as insert: the compact u32 free-list outperforms slab's usize-based bookkeeping.

Iterate occupied slots (~33% gaps)

N obj-pool slab winner
100 52 ns 50 ns tie
1 000 479 ns 485 ns tie
10 000 4.66 µs 4.72 µs tie
100 000 47.3 µs 47.0 µs tie

Identical after fixing ObjId::from_index to use new_unchecked in release, which allows the compiler to dead-code-eliminate key construction when the caller discards it with _.

License

Licensed under either of Apache License 2.0 or MIT License at your option.

About

A simple object arena

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages