slabCrate
Pre-allocated storage that hands you a usize key for each value you insert. Constant-time insert, lookup and remove with no hashing and no per-element allocation — the structure behind connection tables and arenas.
Overview
A Slab<T> is a Vec<T> with a free list. You hand it a value, it gives you
back a usize key; you hand the key back to read, mutate or remove. Removing
leaves a hole that the next insert fills.
use slab::Slab;
let mut slab = Slab::new();
// The slab chooses the key, not you.
let hello = slab.insert("hello");
let world = slab.insert("world");
assert_eq!(slab[hello], "hello");
assert_eq!(slab.remove(world), "world");
// The freed slot is handed out again.
assert_eq!(slab.insert("again"), world);
That inversion is the whole idea, and it is what distinguishes a slab from a
HashMap<usize, T>. You do not choose the key — the slab does. In exchange,
insert, lookup and remove are all constant time with no hashing, values live in
one contiguous allocation, and there is no per-element Box. Lookup is an index
and a tag check.
It solves a specific problem: you have many objects of one type, they come and
go, and you need a cheap handle to each. That is a connection table, a timer
wheel, a set of in-flight requests, the nodes of a graph. It lives in the Tokio
organisation, and tokio-util builds its DelayQueue on it — timer entries come
and go, and the key is the handle you cancel with.
The thing to understand before using it: keys are recycled. Nothing detects
a stale key. After remove(world), the key world is not invalid — it is
vacant, and the very next insert makes it valid again, pointing at an entirely
different value. get will cheerfully return that value, and contains will
say true.
use slab::Slab;
let mut slab = Slab::new();
let key = slab.insert("original");
slab.remove(key);
// Reusing the slot silently revives the old key.
slab.insert("different value");
assert_eq!(slab.get(key), Some(&"different value")); // <- not None
If stale handles can reach your slab — stored in a message, sent to another
task, written into a client response — that silent revival is a bug waiting to
happen, and no amount of care at the call site prevents it. The fix is a
generational arena, where each key carries a counter that invalidates on
removal: slotmap and generational-arena both do this, at the cost of a
larger key and a comparison per lookup. Reach for a slab when keys are internal
and their lifetime is something you control; reach for a generational arena when
they are not.
Two smaller caveats. Capacity does not shrink on remove — a slab that peaked
at ten thousand connections keeps room for ten thousand until you call
shrink_to_fit or compact. And keys are not stable across compact,
which is why that method takes a closure telling you about every move.
It is a safe dependency to take: no required dependencies, std behind a
default feature so no_std with alloc works by turning it off, MSRV 1.51,
optional serde support, and an API that has been stable at 0.4 for years.
When to use it
A table of live connections or sessions
The case the crate was written for.
use slab::Slab;
struct Connection {
peer: String,
bytes_sent: u64,
}
let mut connections = Slab::new();
// The returned key *is* the connection handle.
let alice = connections.insert(Connection { peer: "alice".into(), bytes_sent: 0 });
let bob = connections.insert(Connection { peer: "bob".into(), bytes_sent: 0 });
connections[alice].bytes_sent += 512;
// A disconnect is one O(1) removal; the slot is reused by the next accept.
let gone = connections.remove(bob);
assert_eq!(gone.peer, "bob");
assert_eq!(connections.len(), 1);
assert_eq!(connections[alice].bytes_sent, 512);
Why it fits: the alternative is a HashMap<u64, Connection> with a counter
you increment yourself, which costs a hash per packet and an allocation per
entry, and still leaves you inventing the ids. The slab gives you a dense,
cache-friendly table and an id that is already an array index. It is also how an
event loop maps a readiness notification back to the socket that caused it:
mio's Token is a newtype around usize, so a slab key goes
straight into one.
A graph or tree without `Rc` or raw pointers
Arena allocation, where edges are keys instead of references.
use slab::Slab;
struct Node {
value: i32,
children: Vec<usize>,
}
let mut tree = Slab::new();
let leaf_a = tree.insert(Node { value: 1, children: vec![] });
let leaf_b = tree.insert(Node { value: 2, children: vec![] });
let root = tree.insert(Node { value: 0, children: vec![leaf_a, leaf_b] });
// Walking the tree is index arithmetic, with no lifetime to thread through.
let total: i32 = tree[root].value
+ tree[root]
.children
.iter()
.map(|&k| tree[k].value)
.sum::<i32>();
assert_eq!(total, 3);
Why it fits: a tree of Box<Node> cannot have a parent pointer without
Rc<RefCell<_>>, and a graph with cycles cannot be expressed with ownership at
all. Keys sidestep the borrow checker entirely — a usize borrows nothing, so
cycles, back-edges and shared children are all just numbers. The whole structure
also drops in one go, which matters for a large graph where recursive Box drop
can overflow the stack.
Knowing a value's key before you have the value
vacant_entry reserves the slot first, which you need when the value has to
contain its own key.
use slab::Slab;
struct Task {
id: usize,
label: String,
}
let mut tasks = Slab::new();
let entry = tasks.vacant_entry();
let id = entry.key();
// The value can now record the key it is about to live at.
entry.insert(Task { id, label: format!("task-{id}") });
assert_eq!(tasks[id].label, "task-0");
assert_eq!(tasks[id].id, id);
Why it fits: without this you would insert a placeholder and patch it
afterwards, which means the field is briefly wrong and every reader has to
tolerate that. vacant_entry makes the key available before the value exists,
so the value is correct from the moment it is constructed. The same shape covers
registering a callback that needs to be able to deregister itself.
API map
The vocabulary is Vec's and HashMap's, with keys the slab issues rather than
ones you supply. Entries are grouped by what they do to the slab.
Inserting and removing4
insert
Stores a value and returns its key.
use slab::Slab;
let mut slab = Slab::new();
// Keys start at 0 and go up while the slab is only growing.
assert_eq!(slab.insert('a'), 0);
assert_eq!(slab.insert('b'), 1);
assert_eq!(slab.len(), 2);
When to use it: whenever you add a value. It is amortised O(1): it fills the
most recently freed slot if there is one, otherwise it pushes, reallocating the
backing Vec on the same schedule a Vec would. The key is meaningful only to
this slab — do not persist it, derive anything from its numeric value, or assume
it stays in insertion order once removals start.
vacant_entry and vacant_key
Gets the next key before committing a value to it.
use slab::Slab;
let mut slab: Slab<String> = Slab::new();
// A peek, with no reservation: the slab is unchanged.
assert_eq!(slab.vacant_key(), 0);
assert!(slab.is_empty());
// A reservation you can take the key from and then fill.
let entry = slab.vacant_entry();
let key = entry.key();
let value: &mut String = entry.insert(format!("slot {key}"));
value.push('!');
assert_eq!(slab[0], "slot 0!");
When to use it: vacant_entry when the value must know its own key, or when
building the value can fail and you want to decide after seeing the key —
dropping the entry without inserting leaves the slab untouched. vacant_key is
the cheap read-only version for when you only need to predict the key.
VacantEntry::insert returns &mut T, so you can finish initialising in place.
remove and try_remove
Takes a value out and frees its slot.
use slab::Slab;
let mut slab = Slab::new();
let key = slab.insert("value");
assert_eq!(slab.try_remove(key), Some("value"));
// Already gone, so the second attempt reports rather than panics.
assert_eq!(slab.try_remove(key), None);
assert!(!slab.contains(key));
When to use it: try_remove by default — it handles a double-remove without
panicking, which is the common shape when a disconnect and a timeout can both
fire for the same connection. remove panics on a vacant key and is right only
where the key provably still exists. Neither shrinks the backing storage.
retain, drain and clear
Bulk removal.
use slab::Slab;
let mut slab: Slab<u32> = (0..6).map(|n| (n as usize, n * 10)).collect();
// Keep the entries whose value and key both interest you.
slab.retain(|key, value| key % 2 == 0 && *value > 0);
assert_eq!(slab.len(), 2); // keys 2 and 4
// drain yields the values and empties the slab, keeping its capacity.
let taken: Vec<u32> = slab.drain().collect();
assert_eq!(taken, vec![20, 40]);
assert!(slab.is_empty());
When to use it: retain for a sweep — expiring idle sessions, dropping
closed sockets — since its closure sees the key as well as the value. drain
when you want the values back as you empty it; clear when you do not. Both
keep the allocation, which is usually what you want for a table that will fill
up again.
Reading4
get, get_mut and indexing
The two ways to look a key up.
use slab::Slab;
let mut slab = Slab::new();
let key = slab.insert(10u32);
// Indexing is concise and panics on a vacant key.
slab[key] += 5;
assert_eq!(slab[key], 15);
// get is the checked form.
assert_eq!(slab.get(key), Some(&15));
assert_eq!(slab.get(999), None);
if let Some(value) = slab.get_mut(key) {
*value = 0;
}
assert_eq!(slab[key], 0);
When to use it: index when the key came from this slab and has not been
removed since — inside the loop that owns the table, that is most of the time,
and it reads far better than .get(k).unwrap(). Use get at any boundary where
the key arrived from elsewhere. Remember that neither distinguishes a recycled
key from the original: get returning Some means the slot is occupied, not
that it holds what you put there.
contains
Asks whether a key is occupied.
use slab::Slab;
let mut slab = Slab::new();
let key = slab.insert("x");
assert!(slab.contains(key));
slab.remove(key);
assert!(!slab.contains(key));
When to use it: validating a key before indexing, and little else — if you
are about to read the value anyway, get tells you both things at once. Note
what it actually means: this slot is occupied now, by whatever currently lives
there.
get_disjoint_mut
Mutable references to several entries at once.
use slab::{GetDisjointMutError, Slab};
let mut slab = Slab::new();
let a = slab.insert(1u32);
let b = slab.insert(2u32);
// Two &mut into one slab, which the borrow checker cannot approve alone.
let [first, second] = slab.get_disjoint_mut([a, b]).unwrap();
std::mem::swap(first, second);
assert_eq!((slab[a], slab[b]), (2, 1));
// The same key twice is rejected rather than aliased.
assert_eq!(
slab.get_disjoint_mut([a, a]),
Err(GetDisjointMutError::OverlappingIndices)
);
When to use it: moving a value between two entries, or updating a node and
its parent together. It is the slab equivalent of split_at_mut, and it returns
an error — OverlappingIndices, IndexVacant or IndexOutOfBounds — rather
than letting you alias. get2_mut is the older two-key form, returning
Option<(&mut T, &mut T)>.
key_of
Recovers the key from a reference into the slab.
use slab::Slab;
let mut slab = Slab::new();
let key = slab.insert(String::from("value"));
let value = &slab[key];
assert_eq!(slab.key_of(value), key);
When to use it: inside a loop that holds a reference and needs the key to
record elsewhere. It is constant time — the key is pointer arithmetic against
the base of the backing Vec. It panics if the reference does not point
into this slab, and it cannot tell a foreign reference from a valid one by
value, so only pass references you obtained from this slab.
Iterating and sizing3
iter and iter_mut
Walks the occupied entries, keys included.
use slab::Slab;
let mut slab = Slab::new();
slab.insert(1u32);
let middle = slab.insert(2);
slab.insert(3);
slab.remove(middle);
// Vacant slots are skipped; keys ascend but are not contiguous.
let seen: Vec<(usize, u32)> = slab.iter().map(|(k, &v)| (k, v)).collect();
assert_eq!(seen, vec![(0, 1), (2, 3)]);
for (_key, value) in slab.iter_mut() {
*value *= 10;
}
assert_eq!(slab[0], 10);
When to use it: any sweep over the table — flushing buffers, collecting
stats, expiring entries. The item is (usize, &T), so you get the key without
tracking it yourself. Iteration is O(capacity), not O(len), because it walks the
backing Vec and skips holes — a mostly-empty slab with large capacity iterates
slowly until you compact it.
with_capacity and reserve
Allocating up front.
use slab::Slab;
// One allocation, then 64 inserts that cannot reallocate.
let mut slab: Slab<u64> = Slab::with_capacity(64);
assert!(slab.capacity() >= 64);
assert_eq!(slab.len(), 0);
for n in 0..64 {
slab.insert(n);
}
assert_eq!(slab.len(), 64);
When to use it: when you know roughly how many entries you will hold — a
connection limit, a fixed worker count. It is the main reason to prefer a slab
over a map in a hot path: pre-allocate once and inserts stop touching the
allocator. reserve and reserve_exact grow an existing slab the way their
Vec namesakes do.
compact
Shrinks the slab by moving entries down, telling you each new key.
use slab::Slab;
let mut slab = Slab::with_capacity(10);
let a = slab.insert('a');
slab.insert('b');
slab.insert('c');
slab.remove(a);
// 'c' moves from key 2 into the hole at key 0.
slab.compact(|&mut value, from, to| {
assert_eq!((value, from, to), ('c', 2, 0));
true // returning false cancels this move and stops
});
assert_eq!(slab.len(), 2);
assert!(slab.capacity() >= 2 && slab.capacity() < 10);
When to use it: after a burst has drained, when a sparse slab is both wasting
memory and slowing iteration. The closure is how you fix up every key you have
stored elsewhere, and returning false aborts a move you cannot rekey — so a
slab whose keys have escaped can still be compacted partially. If you only want
the memory back and have no holes to close, shrink_to_fit leaves keys alone.