obj-pool
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:
- 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.
Highlights
- 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.
Usage
[]
= "0.7"
# with serde:
= { = "0.7", = ["serde_support"] }
Single-threaded pool
use ;
let mut pool: = new;
let a: ObjId = pool.insert;
let b: ObjId = pool.insert;
println!;
pool.remove;
// Slot `a` is reused for the next insert.
let c: ObjId = pool.insert;
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 ParObjPool;
use Arc;
let pool: = new;
let id = pool.insert;
// Blocking read — returns a mapped read-guard.
assert_eq!;
// Non-blocking — returns None if the shard lock is contended.
assert_eq!;
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.