Grab one big slab of memory up front, then hand out fixed-size blocks from a free list so every alloc and free is just a pointer move.
Asking the operating system for memory is slow, so an allocator does it rarely. Instead of one syscall per request, it grabs a big slab once and carves it into equal, fixed-size blocks.
A free list remembers which blocks are available. alloc() pops the block at the head of the list; free() pushes a block back onto the head. Both are O(1) — no scanning, no searching, just a pointer move.
Fixed-size blocks mean a request never leaves an odd-sized hole, so the slab has no external fragmentation. The waste is internal: a 40-byte request still consumes a whole 64-byte block.
At init the allocator carves the slab into N blocks and threads them into a singly-linked free list: each free block stores the index of the next free one, and a free_head points at the first. The list lives inside the free blocks themselves, so it costs no extra memory.
BLOCK_SIZE = 64
N = 14
def init(slab):
# Link every block into the free list: 0 -> 1 -> 2 -> ... -> N-1 -> NONE
for i in range(N - 1):
slab.next[i] = i + 1
slab.next[N - 1] = NONE
slab.free_head = 0 # first free block
def alloc(slab):
i = slab.free_head # head of the free list
if i == NONE:
return NONE # slab exhausted — no free blocks
slab.free_head = slab.next[i] # advance head to the next free block (O(1) pop)
return i # caller now owns block i
def free(slab, i):
slab.next[i] = slab.free_head # block i now points at the old head
slab.free_head = i # block i becomes the new head (O(1) push)
Both operations are pure pointer moves — a LIFO stack of free blocks. alloc pops the head; free pushes onto the head. That is why a freed block is the very next one handed back out.
| Property | Slab + free list | General allocator |
|---|---|---|
alloc | O(1) — pop the head | O(n) or O(log n) — search a size class / tree |
free | O(1) — push the head | O(1)–O(n) — may coalesce neighbours |
| External fragmentation | None — every block is identical | Possible — variable sizes leave odd holes |
| Internal fragmentation | Wasted bytes when request < block size | Lower — block fits the request more tightly |
| Sizes served | One size class per slab | Any size |
| Bookkeeping | One next index per block + a head | Headers, footers, size bins, free trees |
The classic move: run many slabs, one per size class (32 B, 64 B, 128 B, …), and route each request to the slab whose block size is the smallest fit. This is the core of the Linux kernel slab allocator and of object pools generally.
alloc can hand the same block to two owners. Mark blocks as in-use and reject a free on an already-free block.free_head == NONE and alloc must fail or grow a new slab. Callers must handle a null return, not assume alloc always succeeds.A server keeps a pool of connection objects on a slab so it never mallocs on the hot path. Block size matches one Connection struct; free_head starts at block 0.
conn_a = alloc() # pops head 0 -> free_head now 1 (conn_a = block 0)
conn_b = alloc() # pops head 1 -> free_head now 2 (conn_b = block 1)
conn_c = alloc() # pops head 2 -> free_head now 3 (conn_c = block 2)
conn_d = alloc() # pops head 3 -> free_head now 4 (conn_d = block 3)
free(conn_b) # block 1 pushed -> next[1]=4, free_head now 1
free(conn_d) # block 3 pushed -> next[3]=1, free_head now 3
# free list head is now: 3 -> 1 -> 4 -> 5 -> ...
conn_e = alloc() # pops head 3 -> free_head now 1 (conn_e REUSES block 3)
Notice conn_e reuses block 3 — the most recently freed slot — because the free list is a LIFO stack. No OS call happened after the first slab grab; the four allocs, two frees, and the reuse were all pointer moves.
You free block 7, then immediately call alloc. Which block does alloc return?
Your slab uses 128-byte blocks but most objects are 24 bytes. What problem is that, and is it fixable on one slab?