CPU Cache

Wilkes called it a slave memory in 1965; the word cache arrives with the IBM System/360 Model 85 three years later. Everybody has been told that walking an array down its columns is slower than walking along its rows. The arithmetic underneath that is three lines long and almost nobody is shown it, so here it is with the knobs left on: a read does not fetch a value, it fetches a line, and how much of that line you use before throwing it away is the entire difference. Nothing here is timed. Every number is counted.

New to caches? Start here

Memory is slow and processors are fast, by a wide margin. A cache is a small pocket of fast memory that keeps recently used data close, so most reads never reach the slow part at all.

The detail that decides everything on this page: a cache does not fetch the byte you asked for. It fetches a whole cache line, typically sixty-four bytes, because fetching neighbours is nearly free once the trip has been made. So the first read of a line is expensive and the next sixty-three are not -- which means the ORDER you touch memory in changes how many trips happen, without changing how much data you touched.

That is the whole subject. Same arithmetic, same number of values, different order, different cost.

Two loops over the same array, in a different order

1 Every element, row by row

An array of doubles laid out row by row, which is what a C multidimensional array does and what numerical array libraries do by default. Walking along a row walks straight through memory.

The first eight reads walking along a row: which element, its byte address, which line it falls in, and whether it hit
elementaddresslineoutcome
[0][0]0line 0miss
[0][1]8line 0hit
[0][2]16line 0hit
[0][3]24line 0hit
[0][4]32line 0hit
[0][5]40line 0hit
[0][6]48line 0hit
[0][7]56line 0hit

2 The same elements, column by column

The same array and the same elements, taken down the columns instead. Consecutive elements of a column are a whole row apart in memory.

The first eight reads walking down a column: which element, its byte address, which line it falls in, and whether it hit
elementaddresslineoutcome
[0][0]0line 0miss
[1][0]512line 8miss
[2][0]1,024line 16miss
[3][0]1,536line 24miss
[4][0]2,048line 32miss
[5][0]2,560line 40miss
[6][0]3,072line 48miss
[7][0]3,584line 56miss
the stride
512 bytes between one element of a column and the next, against a line of 64

That stride is at least a whole line, so every read down a column lands in a line nothing else will use.

3 The line, which is what a read actually fetches

A read fetches a whole line. Make the line longer and each miss brings back more of the row you are about to want; it does nothing at all for the column.

does the array fit in the cache
no, the array is 4.0 times the size of the cache

4 Why the second one costs an order of magnitude more

elements read
4,096 either way
misses, row by row
512
misses, column by column
4,096
the difference
8.0 times as many

Same 4,096 elements, same addresses, different order, 3,584 more misses. A read does not fetch a value, it fetches 8 of them, and walking down a column throws away 7 of every 8.

the simulation and the arithmetic agree: 512 and 4,096

What this page checked when it loaded.

Each claim, whether it held, and the values behind it
claimheldmeasured
both walks read exactly the same number of elementsyes4096 reads each
the simulated row-major misses match the arithmeticyes512 simulated, 512 predicted
and so do the column-major missesyes4096 simulated, 4096 predicted
one miss per line along a row, not one per elementyes8 elements to a line
the column walk misses several times more oftenyes512 against 4096

All 5 checks held when this page loaded.

What is real here, and what is not

Nothing on this page is timed, and that is deliberate

There is no clock here. Every number is a count of simulated misses, exact and derived from the access pattern. Timing anything from JavaScript in a browser would measure the engine, the garbage collector, the operating system and whatever else is running, and would produce a number that changes on every reload. The one thing this page therefore does NOT demonstrate is the step from misses to wall-clock time; it shows the mechanism and asks you to take the last inch on trust.

The cache is direct mapped, and associativity would matter more than this used to say

A block goes in exactly one slot, chosen by a division you can do in your head. Real caches are set associative, typically eight ways, which changes the eviction behaviour. This entry used to end by saying a column walk gets no reuse under any associativity because there is nothing to reuse, and that is wrong: columns 0 to 7 of one row share a single line, so the neighbouring columns WILL want the bytes already fetched. What stops them here is that the stride maps too many of those lines onto the same slots and the direct-mapped cache evicts them before the walk comes back. That is a conflict miss, not an absence of reuse, and a sufficiently associative cache of this size keeps the lines and closes most of the gap. Direct-mapped caches are not extinct either; they are unusual as the main cache of a high-performance processor and ordinary in simpler and specialised parts.

One level, where a modern processor commonly has several

There is no L1, L2 and L3 here, no prefetcher, no write buffer and no store queue. A modern processor would also spot a constant stride and start fetching ahead of the loop, which recovers a good deal of what the column walk loses. That prefetcher is the reason the measured penalty on a modern machine is often smaller than the miss counts here suggest, and it is fair to say the page overstates the gap for that reason.

The array fitting in the cache is the case usually left out

Shrink the array below the cache size and the two orders cost exactly the same, because every line is fetched once whichever way you walk. That is not a quirk of this model: it is why a benchmark on a small array shows nothing, and why the demonstration in a textbook always uses an array chosen to be too large. The knobs are here so you can watch the effect turn off.

Row-major is a property of this layout, not of every language

This page used to say the layout was “what C, Python and JavaScript all do”, and that was an overclaim an outside reader caught. A C multidimensional array really is contiguous and row-major. A Python list of lists is an array of pointers to separate objects, and Python’s own buffer protocol distinguishes C-contiguous from Fortran-contiguous precisely because neither is assumed. ECMAScript specifies arrays as objects with indexed properties and promises nothing about their arrangement in memory; a typed array is contiguous, an ordinary one need not be. The machine models a row-major array of doubles because that is the layout the effect depends on, and saying which layout is the honest version of saying which languages.

Sources