快取友善程式碼與記憶體山 (Cache-Friendly Code & the Memory Mountain)
Overview Table
| 主題 | 核心結論 | 書頁 |
|---|---|---|
| 快取參數取捨 (block size / E / write strategy) | 每個參數都是 hit time vs. miss rate vs. miss penalty 的權衡 | p.669 |
| Block / Line / Set 術語辨析 | block 是資料封包、line 是容器(block+tag+valid)、set 是 line 的集合 | p.670 |
| 快取友善程式碼兩原則 | 1. 讓 common case 快(專注核心函式的內層迴圈);2. 最小化內層迴圈的 cache miss | p.670 |
| stride-k miss 公式 | 每次迭代平均 min(1, (wordsize × k) / B) 次 miss;stride-1 最佳 |
p.671 |
| 列優先 vs. 行優先掃描 | sumarrayrows 比 sumarraycols 快 25 倍(大陣列時後者全 miss) |
p.671–672 |
| Read throughput(讀取吞吐量) | 讀 n bytes 花 s 秒 → n/s MB/s;衡量記憶體系統效能 |
p.675 |
| Memory Mountain(記憶體山) | throughput 是 size(時間局部性)× stride(空間局部性) 的二維函數;高低差超過一個數量級 | p.675–679 |
| 矩陣乘法六版本 | 三等價類 AB / AC / BC;miss rate 比總存取次數更能預測效能;最快比最慢快近 40 倍 | p.679–682 |
| Blocking(分塊) | 提升時間局部性的技巧;Core i7 上因 prefetch 硬體而無效 | p.683 |
| 撰寫程式的三建議 | 專注內層迴圈、stride-1 讀取(空間)、資料一旦讀入就盡量重複使用(時間) | p.683 |
快取參數對效能的影響(6.4 收尾,p.669)
快取設計是多維權衡:總容量
| 參數 | 加大的好處 | 加大的代價 |
|---|---|---|
| Block size (B) | 利用空間局部性提高 hit rate | line 數變少 → 傷害時間局部性;傳輸時間變長 → miss penalty 上升 |
| Associativity (E) | 減少 conflict miss / thrashing | tag bits、LRU 狀態位、控制邏輯更多 → hit time 與選 victim 的 miss penalty 上升 |
| Write-back(vs. write-through) | 傳輸次數少 → 留更多頻寬給 DMA I/O;越往下層越划算 | 實作較複雜(write-through 較簡單、可搭配 write buffer,且 read miss 不需觸發記憶體寫入) |
- 實例:Core i7 的 block 為 64 bytes;L1、L2 為 8-way,L3 為 16-way(miss penalty 越高的下層用越高 associativity)。
- 傳統上追求時脈的高效能系統在 L1 用較低 associativity(miss penalty 只有幾個 cycle),下層用較高者。
- Block:在快取與下層記憶體間搬動的固定大小資料封包。
- Line:快取內存放一個 block 的容器,還含 valid bit 與 tag bits。
- Set:一個或多個 line 的集合。direct-mapped 時 set = line;associative 快取中兩者不可混用。
- 業界說的「line size」其實是 block size——line 永遠只裝一個 block,故常互換使用。
6.5 撰寫快取友善程式碼 (Writing Cache-Friendly Code, p.669–675)
局部性好的程式 miss rate 低,miss rate 低的程式跑得快。快取友善 (cache friendly) = 具有良好局部性的程式碼。基本方法只有兩條:
- 讓 common case 快:程式大部分時間花在少數核心函式的少數迴圈 → 只需專注核心函式的內層迴圈,忽略其他。
- 最小化每個內層迴圈的 cache miss 數:在 load/store 總數等其他條件相同下,miss rate 較低的迴圈較快。
stride-k 存取的 miss 公式
若 block 大小為 B bytes,stride-k(以 word 為單位)的參照模式,每次迴圈迭代平均產生
- k = 1 時最小 → stride-1 是空間局部性的黃金模式。
- 例:word 4 bytes、block 4 words(B=16)、cold cache,掃描
v[0..7]的 hit/miss 模式為m h h h m h h h→ 每 4 次存取 1 miss(miss rate 1/4),cold cache 下已是最佳。
兩個關鍵教訓(以 sumvec 為例)
- 對區域變數的重複參照是好的:編譯器會把
i、sum放進 register file(階層最頂層)→ 時間局部性。 - stride-1 參照模式是好的:所有層級的快取都以連續 block 存放資料 → 空間局部性。
二維陣列:迴圈順序決定生死
C 以 row-major(列優先) 存放陣列。sumarrayrows 內層迴圈沿列掃描(stride-1);sumarraycols 交換迴圈後變成沿行掃描(stride-N)。
row-major 記憶體佈局: a[0][0] a[0][1] a[0][2] a[0][3] | a[1][0] a[1][1] ...
sumarrayrows(i 外 j 內)存取順序 →→→→ 沿記憶體連續前進(stride-1)
hit/miss: m h h h m h h h miss rate = 1/4
sumarraycols(j 外 i 內)存取順序 ↓↓↓↓ 每步跳 N 個元素(stride-N)
hit/miss: m m m m m m m m 陣列大於快取時 → 每次都 miss!
- 實測:大陣列時
sumarrayrows比sumarraycols快 25 倍(p.672)。
sumarraycols 也能享有同樣的 1/4 miss rate(p.672)。「行優先掃描必定全 miss」只在工作集 > 快取容量時成立;同理 Practice Problem 6.19 顯示快取加倍(裝得下整個陣列)後 hit rate 從 50% 升到 75%。
- 6.17(transpose):src/dst 兩陣列的同一列映到相同 cache line 時互相驅逐(thrashing);快取加倍(32 bytes)後兩陣列同時容納,只剩 cold miss。
- 6.18(分兩趟各讀 x、y):依原書解答,存取呈 m,h,m,h… 模式 → 2,048 reads、1,024 misses,miss rate 50%。
- 6.19(column-wise 同時讀 x,y):快取只裝得下半個陣列,掃第二半驅逐第一半 → hit rate 50%;快取加倍 → 75%。
- 6.20(row-wise 同時讀 x,y):stride-1,只有 cold miss → hit rate 75%;快取再大也不變(cold miss 無法避免)。
6.6 快取對程式效能的整體影響 (p.675–683)
6.6.1 The Memory Mountain(記憶體山, p.675–679)
Read throughput(read bandwidth):程式讀取 n bytes 花 s 秒,則吞吐量為
量測程式 mountain.c(Figure 6.40):
test(elems, stride):以stride掃描陣列前elems個元素,用 4×4 loop unrolling(4 個累加器acc0..acc3)增加內層迴圈的平行度(參見 05-Program-Optimization/03-Loop-Unrolling-and-Parallelism)。run(size, stride, Mhz):先呼叫一次test暖快取 (warm up the cache),再用fcyc2估計 cycle 數,換算MB/s = (size/stride) / (cycles/Mhz)。注意size單位是 bytes、elems單位是元素個數。- size ↓ → 工作集小 → 時間局部性好;stride ↓ → 空間局部性好。對不同 (size, stride) 重複量測即得二維函數 = memory mountain。每台電腦都有一座獨一無二的記憶體山,刻畫其記憶體系統的能力。
Read throughput (MB/s) Core i7 Haswell 2.1 GHz
^ 32KB L1d / 256KB L2 / 8MB L3 / 64B block
14000 | ____
| /L1 \____ <- ridges of temporal locality(垂直 size 軸)
||ridge \ L2 \____ 工作集分別塞進 L1 / L2 / L3 / Mem
|| \ridge\ L3\____
|| \ \ridge\ Mem ridge
900 ||_________\_____\_____\______
+--------------------------------> size: 16K ........ 128M
每條 ridge 沿 stride 方向下坡 = slopes of spatial locality(stride 越大越低)
stride=1 有一條平坦高稜線(~12 GB/s)= 硬體 prefetch 的功勞
山的地理結構(Core i7 Haswell,size 16 KB–128 MB、stride 1–12 元素、元素為 8-byte long):
- Ridges of temporal locality(垂直 size 軸的四條稜):工作集完全落在 L1 / L2 / L3 / main memory 的區域。L1 稜峰 >14 GB/s vs. 主記憶體稜谷 900 MB/s,相差超過一個數量級。
- Slopes of spatial locality(沿 stride 軸的下坡):stride 增大 → 空間局部性下降 → throughput 下滑。即使工作集塞不進任何快取,主記憶體稜上最高點仍比最低點高 8 倍 → 時間局部性差時,空間局部性仍能救援。
- stride-1 平坦稜線(~12 GB/s):即使工作集超過 L1、L2 容量,Core i7 的硬體 prefetching 會自動辨識循序 stride-1 參照模式並在存取前抓取 block。prefetch 演算法對小 stride 最有效 → 又一個偏好 stride-1 循序存取的理由。
兩種切片分析:
| 切片 | 固定 | 觀察 | 重點 |
|---|---|---|---|
| Figure 6.42 | stride = 8 | 時間局部性 / 快取容量 | ≤32 KB 由 L1 供應(~12 GB/s);≤256 KB 由 L2;≤8 MB 由 L3;更大主要由主記憶體供應。在 256 KB、8 MB(恰等於 L2/L3 容量)處有凹陷 (dips),推測是與其他 code/data line 的 conflict(確認需 cache simulation) |
| Figure 6.43 | size = 4 MB(L3 稜:塞得進 L3、塞不進 L2) | 空間局部性 | stride 1→8 throughput 遞減:L2 read miss 由 L3 補一個 block、後續在 L2 命中,stride 越大 L2 miss/hit 比越高;stride ≥ 8 words(8×8 = 64 bytes = block size)後每次讀取都在 L2 miss,throughput 固定為 L3→L2 的 block 傳輸速率 |
6.6.2 重排迴圈以提升空間局部性(矩陣乘法, p.679–683)
分析假設:sizeof(double) = 8、單一快取 B = 32(一個 block 裝 4 個 double)、n 大到一列塞不進 L1、區域變數放暫存器(不產生 load/store)。
6 版本依內層迴圈實際觸碰的兩個矩陣分成三等價類(Figure 6.45):
| 版本(類別) | 內層迴圈存取模式 | Loads | Stores | A misses | B misses | C misses | 總 misses/iter |
|---|---|---|---|---|---|---|---|
| ijk & jik(AB) | A 沿列 stride-1;B 沿行 stride-n | 2 | 0 | 0.25 | 1.00 | 0.00 | 1.25 |
| jki & kji(AC) | A、C 都沿行 stride-n | 2 | 1 | 1.00 | 0.00 | 1.00 | 2.00 |
| kij & ikj(BC) | B、C 都沿列 stride-1 | 2 | 1 | 0.00 | 0.25 | 0.25 | 0.50 |
class AB (ijk): sum += A[i][k]*B[k][j] class BC (kij): C[i][j] += r*B[k][j]
A: ─k→ 沿列 stride-1 (0.25 miss) r = A[i][k] 放暫存器 (0 miss)
B: ↓k 沿行 stride-n (每次 miss) B: ─j→ stride-1 (0.25)
C: ─j→ stride-1 (0.25) ← 最佳
class AC (jki): C[i][j] += A[i][k]*r
A: ↓i 沿行 stride-n (1.0) C: ↓i 沿行 stride-n (1.0) ← 最糟
Core i7 實測(Figure 6.46, cycles per inner-loop iteration vs. n)四個要點:
- 大 n 時最快版本比最慢版本快近 40 倍,儘管浮點運算次數完全相同。
- memory reference 數與 miss 數相同的版本對,實測效能幾乎一致(等價類成立)。
- 記憶體行為最差的兩個版本(AC 類)明顯慢於其他四個。
- miss rate 比總記憶體存取次數更能預測效能:class BC(0.5 miss/iter,2 load + 1 store)大勝 class AB(1.25 miss/iter,只有 2 load)。
- 大 n 時最快的 kij / ikj 效能保持恆定——即使陣列遠大於任何 SRAM 快取,prefetching 硬體認得 stride-1 模式且跟得上內層迴圈的存取速度 → 更加鼓勵寫出好空間局部性的程式。
6.6.3 在程式中利用局部性(p.683)
記憶體階層使程式存取記憶體的有效速率不是單一數字,而是隨程式局部性劇烈變動的函數(即記憶體山),差距可達數個數量級。不論底層記憶體系統的具體組織為何,建議三招:
- 專注內層迴圈——大部分計算與記憶體存取都發生在那裡。
- 最大化空間局部性——按資料在記憶體中的存放順序、以 stride-1 循序讀取。
- 最大化時間局部性——資料物件一旦從記憶體讀入,就盡量多次使用。
6.7 本章總結 (Summary, p.684)
- 基本儲存技術:RAM(SRAM 快而貴 → 快取;DRAM 慢而廉 → 主記憶體與 graphics frame buffer)、ROM(斷電保留資料 → 存 firmware)、旋轉磁碟(機械式非揮發、容量大、每 bit 便宜,但存取時間遠大於 DRAM)、SSD(以非揮發 flash 為基礎,逐漸取代旋轉磁碟)。詳見 06-Memory-Hierarchy/01-Storage-Technologies。
- 通則:越快的技術每 bit 越貴、容量越小;各技術的價格/效能以極不同的速率演進,DRAM 與磁碟存取時間跟 CPU cycle 的差距持續擴大。
- 系統以記憶體階層弭平差距:上小快、下大慢。因為寫得好的程式有局部性,大多數資料由高層供應 → 整體效果是「以高層的速度運作、以低層的成本與容量儲存」。
- 程式設計師寫出好的空間與時間局部性即可大幅縮短執行時間;利用 SRAM 快取尤其關鍵——主要從快取取資料的程式遠快於主要從記憶體取資料者。
Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
|---|---|
| stride-k 每次迭代平均 miss 數? | min(1, (wordsize × k)/B);k=1 最佳 |
| word 4B、block 16B、cold cache、stride-1 掃陣列的 miss rate? | 1/4(模式 m h h h m h h h),cold cache 下已是最佳 |
為何 sumarrayrows 遠快於 sumarraycols? |
C 是 row-major;行優先掃描為 stride-N,陣列大於快取時每次存取都 miss(實測差 25 倍) |
| 陣列塞得進快取時,行優先掃描的 miss rate? | 與列優先相同(1/4)——「全 miss」只在工作集 > 快取容量時成立 |
| read throughput 定義與單位? | n bytes / s 秒 = n/s;MB/s 以 10^6 bytes 計(非 2^20) |
| memory mountain 的兩個軸? | size(工作集大小 → 時間局部性)與 stride(→ 空間局部性) |
| 垂直 size 軸的稜線代表? | ridges of temporal locality:工作集分別完全塞進 L1 / L2 / L3 / Mem 的四個區域 |
| 沿 stride 軸的下坡代表? | slopes of spatial locality:stride 越大 miss 比例越高、throughput 越低 |
| stride-1 上的平坦高稜線成因? | 硬體 prefetching 自動辨識循序 stride-1 模式並預先抓取 block;對小 stride 最有效 |
| stride 到達幾個 word 後 throughput 落底(64B block、8B word)? | 8 words = block size;之後每次讀取都在 L2 miss,速率由 L3→L2 block 傳輸率決定 |
| 固定 stride 切片在 size = 256 KB / 8 MB 處的凹陷? | 工作集恰等於 L2/L3 容量,疑似與其他 code/data line 的 conflict(需 cache simulation 確認) |
| 矩陣乘法哪一類最快?為何? | class BC(kij / ikj):B、C 都 stride-1,僅 0.5 misses/iter;最慢 AC 類為 2.0 |
| 「總記憶體存取次數」vs.「miss rate」哪個更能預測效能? | miss rate(BC 類有 3 個 memory ops 仍勝只有 2 個的 AB 類) |
| 大 n 時 kij/ikj 效能不隨 n 惡化的原因? | prefetch 硬體認得 stride-1 且跟得上內層迴圈,即使工作集遠超所有 SRAM 快取 |
| blocking 在 Core i7 上對矩陣乘法無效的原因? | 精密的 prefetching 硬體已涵蓋其效益;在無 prefetch 的系統上才有大增益 |
| 由山估 L1 讀取 cycle 數(6.21 型) | cycles ≈ (時脈 MHz ÷ 吞吐 MB/s) × 每次讀取 bytes;例:2100/12000 × 16 ≈ 2.8 ≈ 3 cycles |
| 兩陣列同列映到相同 cache line、交錯讀寫(6.17 型) | 互相驅逐 → thrashing;快取加倍能同時容納兩陣列 → 只剩 cold miss |
| stride-1 掃描下快取加倍 hit rate 不變(6.20 型) | 原本就只有 cold miss;cold miss 無法靠加大容量消除 |
Related Notes
- 06-Memory-Hierarchy/02-Locality-and-Memory-Hierarchy
- 06-Memory-Hierarchy/03-Cache-Memories
- 06-Memory-Hierarchy/01-Storage-Technologies
- 06-Memory-Hierarchy/Practice-Memory-Hierarchy
- 05-Program-Optimization/03-Loop-Unrolling-and-Parallelism
- 05-Program-Optimization/04-Limiting-Factors-and-Memory-Performance
- 02-Information-Representation/04-Floating-Point