Expand description
§nexus-pool
High-performance object pools for latency-sensitive applications.
nexus-pool provides two pool implementations optimized for different
threading models, both designed to eliminate allocation on hot paths.
§Design Philosophy
This crate follows the principle of predictability over generality:
- SPSC over MPMC: Single-writer patterns avoid lock contention entirely
- Pre-allocation over dynamic growth: Bounded pools have deterministic behavior
- Specialized over general: Each pool type is optimized for its specific access pattern
§Modules
§local - Single-threaded pools
For use within a single thread. Zero synchronization overhead.
local::BoundedPool- Fixed capacity, pre-allocated objects (RAII only)local::Pool- Growable, creates objects on demand via factory- RAII:
acquire()/try_acquire()→ auto-return on drop - Manual:
take()/try_take()→put()to return
- RAII:
Performance: ~26 cycles acquire, ~26-28 cycles release (p50)
§sync - Thread-safe pools
For cross-thread object transfer with single-acquirer semantics.
sync::Pool- One thread acquires, any thread can return
Performance: ~42 cycles acquire, ~68 cycles release (p50)
§Why Single-Acquirer?
You might ask: why not allow any thread to both acquire and return?
The short answer: MPMC pools require solving the ABA problem, which adds significant overhead (generation counters, hazard pointers, or epoch-based reclamation). For most high-performance use cases, MPMC is also a design smell.
The architectural answer: If multiple threads need to acquire from the same pool, you’re violating the single-writer principle. This creates contention and unpredictable latency—exactly what you’re trying to avoid by using a pool.
Better alternatives:
- Per-thread pools: Each thread owns its own
local::Pool - Sharded pools: Hash to a pool based on thread ID
- Message passing: Send pre-allocated buffers via channels
If you truly need MPMC semantics, consider crossbeam::ArrayQueue or
crossbeam::SegQueue which are well-optimized for that use case.
§Use Cases
§Trading Systems / Market Data
use nexus_pool::sync::Pool;
// Hot path thread owns the pool
let pool = Pool::new(
1000,
|| Vec::<u8>::with_capacity(4096), // Pre-sized for typical message
|v| v.clear(), // Reset for reuse
);
// Acquire buffer, fill with market data
let mut buf = pool.try_acquire().expect("pool exhausted");
buf.extend_from_slice(b"market data...");
// Send to worker thread for processing
// Buffer automatically returns to pool when worker drops it
std::thread::spawn(move || {
process(&buf);
// buf drops here, returns to pool
});§Single-Threaded Event Loops
use nexus_pool::local::BoundedPool;
let pool = BoundedPool::new(
100,
|| Box::new([0u8; 1024]), // Fixed-size buffers
|b| b.fill(0), // Zero on return
);
// Event loop - no allocation after startup
for _ in 0..1000 {
if let Some(mut buf) = pool.try_acquire() {
// Use buffer...
buf[0] = 42;
}
// buf returns to pool automatically
}§Performance Characteristics
Measured on Intel Core i9 @ 3.1 GHz (cycles, lower is better):
| Pool | Acquire p50 | Release p50 | Release p99 |
|---|---|---|---|
local::BoundedPool | 26 | 26 | 58 |
local::Pool (reuse) | 26 | 26 | 58 |
local::Pool (factory) | 32 | 26 | 58 |
sync::Pool | 42 | 68 | 86 |
The sync pool is ~1.6x slower on acquire due to atomic operations, but still sub-100 cycles for both operations. Release p99 remains stable even under concurrent return from multiple threads.
§Safety
The RAII pool types use guards (local::Pooled, sync::Pooled) that
automatically return objects to the pool on drop. If the pool is dropped
before all guards, the guards keep the internal storage alive via a
strong reference; values flow back into the orphaned storage on guard
drop, and the last guard to exit drops every retained value in one
shot — no panic, no leak, no use-after-free. See docs/caveats.md §2.
local::Pool also supports manual take() /
put() for cases where RAII lifetime doesn’t fit
(e.g., storing values in structs, passing through pipelines).