← Chapter 30: Why GPU programs actually slow down Chapter 32: When NOT to use a GPU — and the reduction tree →
date 17 / jul / 26
mtwtfss
SCALE & PERFORMANCE — Chapter 31 index home

Why matrix add scales but a linked list dies

*worked out from first principles*

Take two tasks that look almost identical on paper. Adding two big matrices together, and walking down a linked list. Both are just memory reads and simple operations. On a GPU, one of them flies at full speed and the other crawls so badly it barely beats a phone. And the gap between them teaches you the single most important thing about what a GPU actually is.

↳ A GPU is not a general brain that speeds up any code. It's a machine that runs one specific shape of problem beautifully, and chokes on the opposite shape. Matrix addition is that shape. A linked list is its nightmare.

The Dream Shape

Start with why matrix addition is the dream. It satisfies three properties at once.

First, one element, one thread. Each thread computes its own position from a simple formula and goes straight to its data, no searching. Second, zero communication. The thread handling one cell never waits on the thread handling the neighbor, because each result depends only on its own two inputs. Run them in any order, no coordination needed. Third, the reads line up perfectly, so 32 neighboring threads pull 32 neighboring values in a single clean memory fetch.

Independent work, predictable addresses, tidy memory. This is the exact shape the hardware was built for, and it scales almost perfectly, every core busy at once.

The Nightmare Chain

Now the linked list, and watch every one of those properties collapse. A linked list is a chain, each node holding the location of the next. And here is the killer: you cannot reach the hundredth node without first walking through all ninety-nine before it, because the only way to learn where a node lives is to read the one pointing to it.

So the whole traversal is forced into single file. One thread walks the chain while thousands of cores sit stone cold. There is no parallelism to hand out, because step one hundred literally cannot begin until step ninety-nine has finished. The work is serial by its very nature.

The Failed Rescue

And the obvious rescue attempt fails. What if you launch a thread per node and let each follow its own pointer? It doesn't work, because thread one wants the second node but has no idea where it is until thread zero has read the first node's pointer. The threads are chained by a dependency, each unable to start until the previous reveals the next address. The pointer itself is the barrier.

The Memory Scatter

Now it gets worse, on two fronts that are specific to how a GPU is built. First, memory. The nodes of a linked list were allocated whenever and wherever, so they are scattered randomly across memory. One node here, the next one far away, the one after somewhere else entirely.

So even when threads do read nodes, each read lands in a completely different region, and the hardware, which likes to deliver neighboring data in one efficient sweep, is instead forced into a separate fetch for every single node, most of each fetch wasted. The tidy one-sweep delivery that made matrix addition fast is impossible when the data is sprinkled all over.

The Fatal Stall

And the second front is the one that truly kills it. A GPU survives long memory waits by having other work ready. When one group of threads stalls waiting for memory, the scheduler instantly swaps in another group whose data is already available, so the cores stay busy and the wait is hidden.

But in a linked list walk, there is no other ready group, because everything is waiting on the same next pointer to be revealed. Every thread is stuck on the same dependency, so there is nothing to switch to. The scheduler looks for a backup and finds none. The cores sit fully exposed to the memory delay, idling through every single hundreds-of-cycles wait with nothing to fill it. The GPU's whole survival trick depends on having independent work lying around, and a pointer chase provides exactly none.

↳ And notice why the GPU can't even see the problem coming. It cannot fetch the next node early, because it doesn't know where the next node is until it finishes reading the current one. The address is hidden inside the data you haven't loaded yet. So there is no guessing ahead, no running start. Every hop is a fresh cold wait.

Flattening the Data

So are graphs and trees and sparse networks simply hopeless on a GPU, given they are all pointers and links by nature? No, but the fix is not to make the hardware chase pointers faster. It's to get rid of the pointers entirely by changing how the data is stored.

Instead of nodes scattered across memory linked by addresses, you flatten the whole structure into a few flat arrays laid out back to back. One array holds all the values in order, another records where each group of connections begins, so you can jump straight to any row's data by simple arithmetic instead of following a trail. The pointer chasing vanishes. Now different threads can process different rows independently, their reads fall neighboring in memory so they coalesce into clean fetches, and there is plenty of independent work for the scheduler to hide latency behind. The exact same graph, stored two ways, and only the flattened version fits the machine.

↳ The General Takeaway

So the whole picture. Matrix addition wins because it is independent work with predictable, neighboring addresses, the native shape of the hardware.

A linked list loses because it is a forced serial chain of data-dependent, scattered, unpredictable reads, which defeats parallelism, defeats efficient memory delivery, and defeats latency hiding, all three at once.

The deepest truth underneath is that a GPU is not a general-purpose computer, it's a wide array that thrives on regular, flat, independent data and struggles with irregular, pointer-linked, serial data. The CPU is the one built for pointer-heavy tangled structures.

The winning move on a GPU is rarely to write cleverer code.
It's to reshape your data.
Make it stop looking like a chain of pointers and start looking like a flat grid the hardware already loves.