g[0]1g[1]2g[2]3g[3]4g[4]5g[5]6g[6]7g[7]8g[8]9g[9]10g[10]11g[11]12g[12]13g[13]14g[14]15g[15]16misses 0 · hits 0slot 0—slot 1—1for (int r = 0; r < N; ++r)for (int c = 0; c < N; ++c)2 for (int c = 0; c < N; ++c) for (int r = 0; r < N; ++r)3 sum += g[r * N + c];sum 01
Violet card: the loop nest; rose block: the CPU and its running sumSlate cells: the grid in main memory; khaki block: the cache, two line slotsYellow ring: the int being read; four boxes: a cache line on the moveAt the back: main memory, the 16 ints of a 4 × 4 grid stored row by row, grouped into cache lines of 4. In the middle: a cache with room for two lines. In front: the loop nest and the CPU. In row order one miss brings a whole line and the next three reads hit: 4 misses for the grid. With the loops swapped each read lands in a different line: 16 misses, for the same sum. The figure's cache is a toy: 2 lines of 4 ints, throwing out the least recently used line. Real caches hold hundreds of 64-byte lines in several levels (L1, L2, L3), but a real grid is also far bigger: for N = 4096 one column touches 4096 lines (256 KiB), more than an L1 data cache holds.

Build and measure

  1. This loop nest adds up every number in a grid g of N × N ints. Swap the two for lines and the sum is exactly the same. Yet on a big grid one order runs much faster. This lesson shows why.

Memory and the cache

Making it fast

What happens

;; step 1/9 · same-sum
1  for (int r = 0; r < N; ++r)
2    for (int c = 0; c < N; ++c)
3      sum += g[r * N + c];

g    a std::vector<int> of N * N numbers
r    the row, c the column
swap lines 1 and 2: same sum, different speed

The figure's cache is a toy: 2 lines of 4 ints, throwing out the least recently used line. Real caches hold hundreds of 64-byte lines in several levels (L1, L2, L3), but a real grid is also far bigger: for N = 4096 one column touches 4096 lines (256 KiB), more than an L1 data cache holds. Cache lines are 64 bytes on current Intel and AMD x86-64 chips; some Arm chips, such as Apple's M-series, use 128 bytes. C++17's std::hardware_destructive_interference_size gives the compiler's hint for your target.