# Associativity Structures 如同 Hash map 一樣,我們希望某個 cache index 對到的空間可以放兩個或多個 block 的資料,這種技巧叫做 Associativity Structures。 <iframe src="https://drive.google.com/file/d/1BLpOmtVQMWgFU1-_CUItnjOWahuMANVj/preview" height="580"></iframe> - 1-Way Assoc - 就是之前的 Direct Mapped,一個 Cache 地址只能放一個 block - 2-Way Assoc - 如同其名,一個 Cache 地址可以放兩個 block - 所以可以自行延伸出 N-Way Assoc,一個 Cache 地址可以放 N 個 block - Fully Assoc - 整個 cache 就只有一個 set,這樣就不用算 Cache index 了 - 因此全部的資料都放在同一個 set 裡面 - 另一個別名叫做 Full Mapped :::warning 要注意一件事情,通常進行不同 Assoc 進行比較的時候,**是要固定 Cache 大小的**。 因此如果從 1-Way 變成 2-Way,要注意 set 數會只剩一半。 ::: ## 比較 這裡來比較 1-Way Assoc、2-Way Assoc 跟 Fully Assoc 的差異。 假設現在 Cache **只有 4 個 Block 的容量**,現在要從記憶體中讀取資料,並且用以下的「Block Address」序列來依序讀取資料: - 0 8 0 6 8 > 別忘記我們是將記憶體中的「Block Address」進行模除運算後才找到對應的 Cache index - 1-Way Assoc:要模除 4,因為一個 set 只能放 1 個 block - 模除後的 Index:0 0 0 2 0 - 全部的操作都是 Miss,包含讀取新資料也是 Miss - 例如第 3 個 0,他是要讀取 Block Address 0 的資料,可是在那之前就被 Block Address 8 給覆蓋掉了 - 2-Way Assoc:要模除 2,因為一個 set 可以放 2 個 block 所以 index 砍半 - 模除後的 Index:0 0 0 0 0 - 只有一個 1 Hit:第三個 0 讀取的時候沒有 miss - 因為這裡採用了 2-Way,所以就不會被前一個 Block Address 8 的資料給覆蓋掉 - 可是後來因為讀取了 Block Address 6 的資料,**所以會把最先讀進來的 Block Address 8 的資料給覆蓋掉** - 因此導致最後一次的讀取是 Miss - Fully Assoc,要模除 1,因為一個 set 可以放 4 個 block,Index 剩 1 個 - 模除後的 Index:0 0 0 0 0 (其實也不用模除就是了...) - 因為一個 set 可以放 4 個 Block 的資料,除了第一次放入是 miss,後續讀取都是 hit - 所以總共有兩個 Hit :::warning 要注意 2-Way 中的**取代原則**,這裡是採用 **LRU / Least Recently Used**,因為 Block Address 0 的有被使用一次,而 Block Address 8 則沒有,因此他會比較優先被替換掉。 LRU 的話會有特別的硬體負責相關的紀錄;還有其他的原則,像是 **FIFO** 或是 **Random**,這部分也是一門學問。 ::: ## 電路結構 下面以 4-Way 的電路作介紹: <iframe src="https://drive.google.com/file/d/1CAiTdoza84F72HXHj_RpUO6s0uDeeV5u/preview" height="580"></iframe> - 這是個 256 Sets 的 Cache,所以 Cache index 是 8 個 bit - 每個 Block 是 4 Bytes 寬,所以 Block Offset 是 2 個 bit 可以看到跟上周一樣的電路結構,不過現在一個 Set 裡面有 4 個 Block 的資料。 順序上是先將該 Set 的資料全部讀出來,然後用 Cache tag 進行檢驗的結果作為 MUX 的 Selection bit,來決定要輸出誰。 # Tag Size 情境: - Cache size 為 4K 個 Blocks 的大小,一個 block 有 4 個 word(16 bytes),32 bit 的 Address: - 所以 Bytes offset 是 4 bit。 - 可以知道 tag + index 必定有 32 - 4 = 28 個 bits 接著我們來探討不同 Assoc 對 Tag size 的影響。 - 1-Way Assoc - Cache size 有 4K 個 Blocks,所以總共有 4K 個 Sets,所以 index 需要 12 個 bits - 因此 tag 有 16 個 bits - 所以 tag 總共佔了 16 × 4K = 64K bits - 2-Way Assoc - Cache size 有 4K 個 Blocks,所以總共有 2K 個 Sets,所以 index 需要 11 個 bits - 因此 tag 有 17 個 bits - 所以 tag 總共佔了 17 × 2 × 2K = 68K bits - Fully Assoc - Cache size 有 4K 個 Blocks,總共只有 1 個 Set,所以 index 要 0 個 bit - 因此 tag 有 28 個 bits - 所以 tag 總共佔了 28 × 4K × 1 = 112K bits # Cache Control 講了這麼多跟 Cache 有關的內容,現在要講如何處理 Cache 的控制電路。 下面是三個層級之間的概念架構圖: <iframe src="https://drive.google.com/file/d/1uBhwZIKy2EVRB8UA869FUrfChy_A7BIn/preview" height="580"></iframe> >老師說 low end CPU 通常會共用 write 跟 read 的 bus 這裡的情境是: - Direct-mapped, write-back, write allocate - write-back 就是只寫到 Cache,並且要設置 Dirty bit - write allocate 就是發生 write miss 時會把資料拿進 Cache 裡面 - 可以複習上一周的內容 - Block size:4 words (16 bytes) - Cache size:16 KB (1024 blocks) - 32-bit address - Valid bit and dirty bit per block - Blocking cache - CPU 會等到存取完成才做下一步 設計 Cache Control 的方法就是先畫出 FSM,也就是狀態圖: <iframe src="https://drive.google.com/file/d/11Iw6LiXUc7EEO05QHMKKt9EIG-bbw4DS/preview" height="580"></iframe> - Idle → Compare Tag - 當 CPU 發送一個 Valid request 時 - Idle ← Compare Tag - 有兩種 request 情形 - 如果是 read ,並且資料是 Valid 並且是 Hit - 設置 Valid Bit 跟 Cache Tag - 如果是 write,沒有發生 write miss - 設置 Dirty bit - 滿足條件的話就標示 Cache ready 以及 Cache Hit - Compare Tag → Write-Back - 如果 Request 是 read,但是發生了 read miss,並且資料是 dirty 的 - 需要先將資料寫回記憶體 - 並且要等到資料寫好,所以會有個 Memory not ready 的指向自己箭頭 - Memory Ready 的話就可以前往 Allocate 狀態了 - Compare Tag → Allocate - 如果 Request 是 read,但是發生了 read miss,不過資料是 clean 的 - 那麼就可以直接去拿資料了 - 並且也會有個 Memory not ready 的指向自己箭頭 - 別忘記由於這裡是 Write-Allocate,所以 write miss 跟 read miss 一樣,遇到 dirty 要先寫回 上面狀態圖中的 Cache Ready 跟 Memory Ready 就是概念架構圖中的 Ready。 有了 FSM 後就可以轉成電路圖了,例如下圖: <iframe src="https://drive.google.com/file/d/1MQITf5pKNbrhRTJ5V806Nnyq5BemvS3o/preview" height="580"></iframe> 通常這個轉換的工作都有軟體可以幫忙;而最後電路可以透過 PLA 或是 ROMs 的方式做出來。 總之概念就是只要畫出 FSM 後就可以弄出電路來。 # L2 Cache 現在的電腦就我們所知,都會有 2 到 3 層的 Cache,尤其近年來 L3 快取挺普遍的。 會需要 L2 快取的原因是根據下面的公式: $$ \begin{align} \text{Total CPI} &= \frac{\text{CPU execution clock cycles + Memory stall clock cycles}}{\text{Total Instructions}}\\ &= \text{CPI} + \frac{\text{Memory stall clock cycles}}{\text{Total Instructions}}\\ &= \text{CPI} + \frac{\text{# Memory accesses × Miss rate × Miss penalty}}{\text{Total Instructions}}\\ &= \text{CPI} + \text{Miss rate × Miss penalty}\\ \end{align} $$ :::info 這裡是混合式的 Cache,所以每個 Instructions 都是一筆 Memory Access。 ::: 可知如果只有 L1 快取,一旦 Miss 就會動到 Memory 的 Penalty,那可是很貴的。 聰明的架構師就想到,既然一層不夠,那就兩層。 ## 情境模擬 - Base CPI = 1.0 - L1 Cache miss rate = 2% - Clock Rate = 5GHz = 0.2 ns per cycle - Memory Access time = 100 ns,包含 miss handling - L1 Miss Penalty **to main memory** = 100 / 0.2 = 500 cycle 如果今天只有 L1 快取,那麼 Total CPI 就會是 1 + 2% × 500 = 11.0。 :::warning CPU cycle 又稱 **hit time** ::: 但是當我們引進 L2 快取: - 5 ns access time - L1 Miss Penalty **to L2 Cache**= 5 / 0.2 = 25 cycle - ==global miss rate== = 0.5% 那麼 Total CPI 就會是 1 + 2% × 25 + ==0.5%== × 500 = 4.0。 :::warning 上面的公式其實可以改寫成: 1 + 2% × 25 + 0.5% × 500 = 1 + 2%(25 + 25% × 500) - 25% 叫做 **L1 Local miss rate in L2** - 乘開來後的 0.5% 叫做 **CPU global miss rate in L2** - 所以 **CPU global miss rate in L1** 就是一開始給的 2% ::: ## 設計目的 如果要避免 Miss,那為何不將 L1 快取弄得很大?原因是因為 L1 快取負責跟 CPU 接洽,所以他的 **Hit Time** 也是很重要的。 如果為了降低 Miss 而將 L1 弄得很大,結果反而讓 ==Hit time== 變很長,那麼也不是我們想要的,所以才有了 L2 快取的產生。 而 L2 快取的設計目的跟 L1 相比之下, ==Hit time== 就沒有那麼重要,會著重處理 Miss,所以可以: - 有更大的 Cache 大小 - 更大的 associativity - 更大的 Block Size 這裡跟上周提到的 [Combined 跟 Split Cache](https://hackmd.io/xnZnH9ZXT1yJfCaXqwflHw?view#Split-cache-vs-Combined-cache) 一樣: - L1 追求的快還要更快:==低 Hit Time==。 - L2 則是追求低 Miss Rate:==高 Hit Time==。 # Cache Friendly 老師說 Programmer 會分為以下等級: - 會寫程式:基礎 - 進行時間優化的:進階 - 會寫 Cache Friendly 的:專家 所謂的 Cache Friendly,就是寫程式的時候要**想到 Locality,以及善用 Locality**。 例如下面的 Radix Sort 跟 Quick Sort,雖然 Radix Sort 在數量大的時候有著較低的 Instruction 量(左圖),但是實際跑出來的結果卻有較多的 Clock Cycle(右圖)。 <iframe src="https://drive.google.com/file/d/1tiELh0axcLuvrNi8Os7ilx4cODeLAtR-/preview" height="380"></iframe> 而原因就是在於 Radix Sort 的 Cache Misses 比 Quick Sort 大: <iframe src="https://drive.google.com/file/d/1gy5x52uPq_NSJZIH1rssKkV-5t55zz0X/preview" height="480"></iframe> 也就是說在該實驗中的 Radix Sort 比較不具有 Cache Friendly 的特性。 # Software Optimization via Blocking / DGEMM 在計算矩陣乘法時可以使用所謂的 Blocking 技巧,善用 Locality 做出 Cache Friendly 的 Code。 下面是在計算兩個方陣 A 跟 B 的矩陣乘法 ```c= // 假設 C 矩陣都先初始化為 0 for (int i = 0; i < n; ++i){ for (int j = 0; j < n; ++j){ double cij = C[i+j*n]; /* cij = C[i][j] */ for( int k = 0; k < n; k++ ) cij += A[i+k*n] * B[k+j*n]; /* cij += A[i][k]*B[k][j] */ C[i+j*n] = cij; /* C[i][j] = cij */ } } ``` 在這樣的算法中,Cache 存取的次數是 $2N^3+N^2$,也就是內層的 `cij += A[i+k*n] * B[k+j*n]` 部分跟 `cij = C[i+j*n]` 的部分。 在最糟的情形,每次的存取都是 Miss,所以 Miss 次數就是 $2N^3+N^2$。 - 首先 `cij = C[i+j*n]` 的部分確定是拿取寫入只會發生一次,所以 Miss 次數確定有 $N^2$ 次 - 至於 `cij += A[i+k*n] * B[k+j*n]` 的部分 - 假設 `A[i+k*n]` 大小很大,雖然第一次讀取時拿進 Cache 裡面,但是它佔滿了 Cache - 並且 Cache 又剛好只放的下他 - 因此換到下一個 `A[i+k*n]` 就會被替換掉。 但其實最糟的情形不一定會發生,因為通常記憶體中一個 Block 可能可以放入多個 `A[i+k*n]`,而且不同的 `A[i+k*n]` 也不一定會在同個 Block,藉由 Block Address 算出的 Cache index 可能也會不同。 總之為了避免發生被替換掉,聰明的設計師們想到說,那就確保計算某個區域內的資料量大小剛好是 Cache 的大小,這樣就不會發生替換這件事了。 ```c= void do_block(int n, int si, int sj, int sk,double *A, double *B, double *C){ for (int i = si; i < si + BLOCKSIZE; i ++) { for (int j = sj; j < sj + BLOCKSIZE; j++) { double cij = C[i+j*n]; /* cij = C[i][j] */ for (int k = sk; k < sk + BLOCKSIZE; k++) { cij += A[i+k*n] * B[k+j*n]; /* cij += A[i][k]*B[k][j] */ } //C[i+j*n] = cij; /* C[i][j] = cij */ C[i+j*n] += cij; // 應該是 += 不是 = } } } void dgemm(size_t n, double *A, double *B, double *C){ for (int sj = 0; sj < n; sj += BLOCKSIZE) { for (int si = 0; si < n; si += BLOCKSIZE) { for (int sk = 0; sk < n; sk += BLOCKSIZE) { do_block(n, si, sj, sk, A, B, C); } } } } ``` 這份程式碼可以看到的特點是,我們一次只取 A 跟 B 當中的 `BLOCKSIZE × BLOCKSIZE` 的區域進行計算,也就是說可以確保要經過每一個 `BLOCKSIZE` 才會發生 Miss。 因此 Miss 次數就可以從 $2N^3+N^2$ 降到 $\frac{2N^3}{BLOCKSIZE}+N^2$ 次。 > 對 A 跟 B 兩個方陣來說,原本是 $N^3$ 次存取都會發生 Miss,現在則是要經過一個 Block 大小的次數後才會發生 Miss,因此次數從 $N^3$ 變成 $\frac{N^3}{BLOCKSIZE}$,而因為有 A 跟 B 兩個方陣,所以次數要乘以 2 --- ## 回顧 - Associativity - L2 Cache & formula