A typed object pool with compact 32-bit IDs, optional serde support, and a sharded parallel variant for concurrent workloads.
When building self-referential data structures (graphs, trees, linked lists) in Rust you have a few options:
- Unsafe pointer manipulation.
Rc<RefCell<T>>— safe but verbose and allocation-heavy.- 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.
- Compact IDs —
ObjIdwrapsNonZeroU32, soOption<ObjId>is 4 bytes with no extra space (niche optimization). Aslabkey is a fullusize(8 bytes on 64-bit). - Debug-mode safety — in debug builds every pool mixes a random pool-specific offset into each
ObjIdit issues, so accidental cross-pool access is rejected (getreturnsNone, indexing panics) instead of silently returning wrong data. Release builds skip the masking entirely. - Parallel pool —
ParObjPool<T, S>shardsSinner pools behindRwLocks for concurrent insert/remove/lookup without a global lock. - Optional serde — enable the
serde_supportfeature to serialize/deserializeObjId.
[dependencies]
obj-pool = "0.7"
# with serde:
obj-pool = { version = "0.7", features = ["serde_support"] }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());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));slab is the most commonly used arena crate in the Rust ecosystem. The table below summarises the differences, followed by benchmark numbers.
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 |
Measured on Apple M-series (aarch64), optimized build (cargo bench).
Each cell shows the median time for the operation over the full collection.
| 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.
| 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.
| 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.
| 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 _.
Licensed under either of Apache License 2.0 or MIT License at your option.