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.
| element | address | line | outcome |
|---|---|---|---|
| [0][0] | 0 | line 0 | miss |
| [0][1] | 8 | line 0 | hit |
| [0][2] | 16 | line 0 | hit |
| [0][3] | 24 | line 0 | hit |
| [0][4] | 32 | line 0 | hit |
| [0][5] | 40 | line 0 | hit |
| [0][6] | 48 | line 0 | hit |
| [0][7] | 56 | line 0 | hit |
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.
| element | address | line | outcome |
|---|---|---|---|
| [0][0] | 0 | line 0 | miss |
| [1][0] | 512 | line 8 | miss |
| [2][0] | 1,024 | line 16 | miss |
| [3][0] | 1,536 | line 24 | miss |
| [4][0] | 2,048 | line 32 | miss |
| [5][0] | 2,560 | line 40 | miss |
| [6][0] | 3,072 | line 48 | miss |
| [7][0] | 3,584 | line 56 | miss |
- 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.
| claim | held | measured |
|---|---|---|
| both walks read exactly the same number of elements | yes | 4096 reads each |
| the simulated row-major misses match the arithmetic | yes | 512 simulated, 512 predicted |
| and so do the column-major misses | yes | 4096 simulated, 4096 predicted |
| one miss per line along a row, not one per element | yes | 8 elements to a line |
| the column walk misses several times more often | yes | 512 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
- M. V. Wilkes, Slave Memories and Dynamic Storage Allocation, IEEE Transactions on Electronic Computers, 1965. The idea, before it was called a cache.
- J. S. Liptay, Structural Aspects of the System/360 Model 85, II: The Cache, IBM Systems Journal 7(1), 1968. Where the word is used for the hardware, and where the first published hit ratios are.
- Logical Art, the studio this belongs to.