# nexus-slab
Manual memory management with SLUB-style slab allocation. 1 cycle churn
(alloc+free) at 32B, sub-cycle free. 15x faster than Box. Placement new
confirmed in assembly.
## Why
When you churn same-type objects (orders, connections, timers, nodes), the
global allocator is your bottleneck. `malloc`/`free` contend with every other
allocation in the process. A slab gives you:
- **LIFO cache locality** — free a slot, allocate again, get the same
cache-hot memory back
- **Zero fragmentation** — every slot is the same size
- **Allocator isolation** — your hot path doesn't compete with logging
or serialization
- **Placement new** — values written directly into slot memory, no copy
## Quick Start
```rust
use nexus_slab::bounded::Slab;
// SAFETY: caller accepts manual memory management contract
let slab = unsafe { Slab::with_capacity(1024) };
let mut ptr = slab.alloc(Order { id: 1, price: 100.5 });
ptr.price = 105.0; // safe Deref/DerefMut
slab.free(ptr); // consumes handle, can't use after
```
Construction is `unsafe` — you're opting into:
- **Free everything you allocate.** Unfree'd slots leak.
- **Free from the same slab.** Cross-slab free corrupts the freelist.
- **Don't share across threads.** The slab is `!Send`/`!Sync`.
Everything after construction is safe. `Slot<T>` is move-only
(`!Copy`, `!Clone`) — the compiler prevents double-free.
## API
### Typed Slabs
```rust
use nexus_slab::bounded::Slab; // fixed capacity
use nexus_slab::unbounded::Slab; // grows via chunks
// Bounded
let slab = unsafe { Slab::with_capacity(1024) };
let ptr = slab.alloc(42u64); // panics if full
let ptr = slab.try_alloc(42u64)?; // returns Err(Full(42)) if full
let value = slab.take(ptr); // extract without drop, free slot
slab.free(ptr); // drop value, free slot
// Unbounded
let slab = unsafe { Slab::with_chunk_capacity(256) };
let ptr = slab.alloc(42u64); // never fails, grows if needed
// Placement new (two-phase)
if let Some(claim) = slab.claim() {
let ptr = claim.write(Order { id: 1, price: 100.5 });
// value constructed directly in slot memory
slab.free(ptr);
}
```
### Byte Slabs (Type-Erased)
Store heterogeneous types in one slab. Any `T` fitting in `N` bytes works.
```rust
use nexus_slab::byte::bounded::Slab;
let slab: Slab<128> = unsafe { Slab::with_capacity(64) };
let p1 = slab.alloc(42u64); // 8 bytes
let p2 = slab.alloc([1.0f64; 8]); // 64 bytes
let p3 = slab.alloc(String::from("hello")); // different types, same slab
slab.free(p3);
slab.free(p2);
slab.free(p1);
```
### Slot<T>
8-byte move-only handle. Safe `Deref`/`DerefMut` access.
```rust
let mut ptr = slab.alloc(Order { id: 1, price: 100.5 });
// Safe access
ptr.price = 105.0;
let p: Pin<&mut Order> = ptr.pin_mut(); // stable address, no Unpin needed
// Raw pointer escape hatch
let raw = ptr.into_raw(); // disarms debug leak detector
let ptr = unsafe { Slot::from_raw(raw) }; // reconstruct
slab.free(ptr);
```
**Debug mode:** dropping a `Slot` without calling `free()` or `take()`
panics (leak detection). Release mode: silent leak.
### Rc Slabs (Shared Ownership)
When multiple owners need access to the same slot — e.g., a collection
holds a node and the user holds a handle for cancellation.
```rust
use nexus_slab::rc::bounded::Slab;
// SAFETY: caller accepts manual memory management contract
let slab = unsafe { Slab::<Order>::with_capacity(1024) };
// Alloc returns RcSlot<Order> with refcount 1
let h1 = slab.alloc(Order { id: 1, price: 100.0 });
// Clone is safe — increments refcount
let h2 = h1.clone(); // refcount 2
// Access through borrow guards — one borrow at a time
{
let mut order = h1.borrow_mut(); // exclusive guard
order.price = 105.0;
}
{
let order = h2.borrow(); // shared guard (still exclusive — see below)
assert_eq!(order.price, 105.0); // mutation visible across clones
}
// Every handle must be freed — slot deallocated on last free
slab.free(h2); // refcount 2 → 1, slot stays alive
slab.free(h1); // refcount 1 → 0, value dropped, slot freed
```
**Borrow rules:** More conservative than `RefCell`. Only one borrow
(shared OR exclusive) is allowed at a time. Any attempt to borrow while
another borrow is active panics. This is intentional — shared mutable
state in a low-latency system should be tightly controlled.
```rust
let h1 = slab.alloc(42u64);
let h2 = h1.clone();
let _g1 = h1.borrow();
let _g2 = h2.borrow(); // PANICS — already borrowed (even though both are shared)
```
**Pin support:** Slab memory never moves, so `Pin` is sound without
`T: Unpin`:
```rust
let mut pinned = handle.pin_mut(); // Pin<RefMut<'_, T>>
```
## Performance
See [BENCHMARKS.md](./BENCHMARKS.md) for full methodology and numbers.
Pinned to core 0. Batched timing (64 ops per rdtsc pair), 10K samples.
### Churn — alloc + deref + free (cycles p50)
| 32B | **1** | 15 | **15x** |
| 64B | **2** | 17 | **8.5x** |
| 128B | **4** | 23 | **5.8x** |
| 256B | **7** | 29 | **4.1x** |
| 512B | **14** | 44 | **3.1x** |
| 1024B | **25** | 103 | **4.1x** |
| 4096B | **78** | 249 | **3.2x** |
### Free (cycles p50)
| 32B-4096B | **0-1** | 23-26 |
Slab free is sub-cycle regardless of size — a single freelist pointer
write. Box free is constant at ~24 cycles (allocator bookkeeping).
Assembly-verified placement new: `alloc()` compiles to freelist pop +
SIMD store directly into slot memory. No intermediate copy.
## Bounded vs Unbounded
| Capacity | Fixed at init | Grows via chunks |
| Full behavior | `Err(Full)` | Always succeeds |
| Alloc latency | ~2 cycles | ~2 cycles (LIFO from current chunk) |
| Growth | Never | New chunk (~40 cycle p999) |
Use bounded when you know your capacity. Use unbounded when you need
overflow headroom without crashing.
## Architecture
### Pointer Provenance
Freelist pointers are derived from `UnsafeCell` for correct write provenance under stacked borrows. This ensures that miri accepts the freelist manipulation as valid -- pointers written into vacant slots carry the correct provenance tag from the `UnsafeCell` wrapping the union, not from a stale read-only reference.
### SlotCell (SLUB-style union)
```rust
#[repr(C)]
union SlotCell<T> {
next_free: *mut SlotCell<T>, // vacant: freelist link
value: ManuallyDrop<MaybeUninit<T>>, // occupied: user data
}
```
No tag, no metadata. Writing a value overwrites the freelist pointer.
The `Slot` handle is the proof of occupancy.
### Unbounded Builder
```rust
use nexus_slab::unbounded::Builder;
// SAFETY: caller guarantees slab contract
let slab = unsafe {
Builder::new()
.chunk_capacity(4096)
.initial_chunks(4)
.build::<Order>()
};
```
## Features
| `std` | yes | Enables `alloc` + `thread::panicking()` for debug leak detection |
| `alloc` | with `std` | `Vec`-backed storage. Required for slab operation. |
| `rc` | no | Reference-counted handles (`rc::bounded::Slab`, `rc::unbounded::Slab`) with borrow guards |
`no_std` with `alloc` is supported for embedded systems.
## License
MIT OR Apache-2.0