Linked list vs CSR adjacency
Storing adjacency is a three-way trade among mutability, compactness, and cache-locality: doubly-linked lists chase pointers across the heap but insert and delete cheaply (right for mutating OLTP graphs), while CSR streams a node's neighbors contiguously through cache but must rebuild to add an edge (right for read-mostly analytic sweeps).
The doubly-linked list mutates cheaply, but it scatters across memory — every hop is a potential cache miss. CSR packs a node's neighbors into one contiguous slice that flies through cache, at the price of never being able to cheaply add an edge. That's the storage fork; now both layouts are just bytes. The thing missing is a way to ASK questions of them without hand-coding pointer walks.
Scene 06
Linked list vs CSR adjacency
- Watch
- Try it
- Predict
- Capture
Last scene the relationship chain was a doubly-linked list because it's easy to insert and delete. But pointer-chasing across the heap is cache-hostile. Watch Alice's four friends drawn first as that scattered list — each cell at its own heap address, chained by prev/next — then drawn a second way: packed into one contiguous slice that reads straight through cache. Same four neighbors, two completely different memory layouts.
Highlighted lines are the ones running in the diagram right now.
def neighbors(v, offset, neighbor_array):start = offset[v]end = offset[v + 1]# contiguous run — streams through cachereturn neighbor_array[start:end]
def insert_list(cell, after): # doubly-linked listcell.next = after.next # rewire two pointersafter.next = cell # O(1) — splice in placedef insert_csr(v, u, offset, arr): # CSR — no free slotarr = rebuild_with_edge(v, u) # O(total edges) rebuildreturn arr
def walk_list(head): # doubly-linked relationship chaincell = headwhile cell is not None:visit(cell.id) # use this neighborcell = cell.next # pointer → some other heap address# next cell is elsewhere → likely a cache miss
def pick_layout(workload):if workload.mutates_constantly:return LINKED_LIST # splice edges in place, skip rebuildselse: # read-mostly: never insertsreturn CSR # contiguous neighbors stream through cache
Where this sits in Build a graph database (Neo4j / Dgraph-style)
Scene 06 of 16, in the The store act — Store records, index-free adjacency, the CSR trade.. Doubly-linked relationship lists are cache-hostile but mutate in place; Compressed Sparse Row packs a node's neighbors into one contiguous slice that streams through cache but rebuilds the whole array to add a single edge — the OLTP-graph vs OLAP-graph storage fork.
Up next. We can store and traverse a graph by hand, but nobody writes raw pointer-walks. We need a language where you describe the SHAPE you want — Alice's friends' friends — and the engine compiles it into a traversal. And the surprising part: for expansion, that compiler needs no join planner at all, because the pattern maps straight onto pointer-chasing.
All 16 scenes in Build a graph database (Neo4j / Dgraph-style) · Every curriculum