Lesson
The theory — Cache Locality: Why the Order of Reads Matters
A cache is a small, fast memory that keeps copies of main memory in lines, each holding one aligned block of L bytes. A read whose block is already in a line is a hit; any other read is a miss, and the whole block is loaded. In a direct-mapped cache of S lines, the block holding address a may go in one line only, line ⌊a / L⌋ mod S. A loop that reads neighbouring addresses one after another gets several reads out of each block it loads, which is called spatial locality.
What each symbol means
L- the line size in bytes: 16 here, 64 on most desktop and server processors
S- the number of lines: 8 here
- Assumes
- A direct-mapped cache that starts empty, 4-byte ints, and a matrix stored row by row, as C stores it (Fortran stores by columns, and there the fast order is the other one). Only the matrix reads are counted. Real first-level data caches are larger and set-associative, typically 8-way: a block may go in any of 8 lines of its set, which removes most of the conflicts shown here but not a shortage of space. Hardware prefetching, which fetches the next line before it is asked for, is left out.
Problem solved in full
-
How much memory traffic does the wrong order cost? 4 steps
Press 8 × 8 by columns and explain the 0 hits. Then take a 1024 × 1024 matrix of 4-byte ints and a direct-mapped 32 KiB cache with 64-byte lines. How many bytes move from memory when it is summed by rows, and how many by columns? Does padding each row by one line help?
-
On the page, a row is 8 × 4 = 32 bytes, two lines, so row i starts in line 2i mod 8 and column 0 uses lines 0, 2, 4 and 6 only. a[4][0] takes the line a[0][0] was in, and by the time the loop reaches a[0][1] its block has gone: 0 hits of 64. By rows there is one miss per 16-byte block, 16 of them, so 48 hits, 1 − 4/16 = 75%.
-
The large cache has 32,768 / 64 = 512 lines. By rows, each 64-byte line holds 16 ints and only the first read of each misses: 1,048,576 / 16 = 65,536 misses, a hit rate of 1 − 4/64 = 93.75%, and 65,536 × 64 = 4,194,304 bytes moved, the matrix exactly once.
-
By columns: a row is 4,096 bytes, 64 lines, so row i starts in line 64i mod 512 and a column uses only 8 of the 512 lines. a[8][0] throws out a[0][0]'s block, and long before the loop returns for a[0][1] it has gone. Every one of the 1,048,576 reads misses: 67,108,864 bytes, 16 times as much.
-
Padding each row with 16 ints makes it 4,160 bytes, 65 lines. 65 and 512 have no common factor, so a column now spreads over all 512 lines. It still gets no hits: a column touches 1,024 blocks and the cache holds 512, so every block is gone before the next column wants it.
Answer
4 MiB by rows, 64 MiB by columns. Padding spreads the column over the whole cache and still gets no hits: it cures conflicts, and each of this matrix's columns is bigger than the whole cache.
-
Learning path
How a computer runs a program
References (1)
- The first insight's measured case, the line size in the definition, and the 8-way first-level caches in `assumes`: Ulrich Drepper, "What Every Programmer Should Know About Memory," Red Hat, Inc., version 1.0, 21 November 2007. §3.2 (p. 15), §3.3.1 (pp. 17–18), §6.2.1 (pp. 49–50).