CS:APP 3/e cachelab
===
###### tags: `sysprog2018`
[實驗說明](http://csapp.cs.cmu.edu/3e/cachelab.pdf)
## 任務:
1. 完成 csim.c 中的 cache simulator。
2. 完成 trans.c 中,可能造成 cache misses 的矩陣運算。
## 工具
* 檢查全部
``$./driver.py``
* 檢查 csim 中的模擬器的正確性
``$ ./test-csim``
* 檢查矩陣運算的正確與效能
```
$ ./test-trans -M 32 -N 32
$ ./test-trans -M 64 -N 64
$ ./test-trans -M 61 -N 67
```
* 預先提供的參考用模擬器:csim-ref
## Part A
### 要求
* 模擬一個參數如下的 cache。
![](https://pic4.zhimg.com/80/v2-0fa71edb0f43ac513a2081ab04c5f6de_hd.jpg)
以 32 bit address 為例:
![](https://static.lwn.net/images/cpumemory/cpumemory.12.png)
* -v: Optional verbose flag that displays trace info
* -s : Number of set index bits (S = 2^s is the number of sets)
* -E : Associativity (number of lines per set)
* -b : Number of block bits (B = 2^b is the block size)
* -t : Name of the valgrind trace to replay
* 對其作 `trace/***.trace` 中的記憶體操作。
```
Ex:
I 0400d7d4,8
M 0421c7f0,4
L 04f6b868,8
S 7ff0005c8,8
每行代表一個操作,格式:
[space]operation address,size
“I” 代表 instruction load,
“L” 代表 data load,
“S” 代表 data store,
“M” 代表 data modify (i.e., a data load followed by a data store)
```
* 這次實作不需要考慮 "I",只需要考慮 "L"、"S"、"M"
- "L" 表示載入(讀)、"S" 表示儲存(寫),兩者在檢查 cache 時的行為一樣,只要判斷 set、tag 跟 valid bit 即可
- "M" 代表修改,需要一次讀檔與一次寫入,因此會檢查兩次 cache
### 定義 cache 結構 & 建立 cache table
每行 cache line 的資料結構如下圖:
![cache line](https://i.imgur.com/E5StvFY.png)
* 這次並不會進行資料搬移,只需要透過比較同個組中 valid bit 跟 tag 就可以判斷 hit/miss,所以在定義資料結構時沒有加入 data。
* cache 的取代採用 least recently used,因此在每筆資料中需要一個參數紀錄該筆資料在什麼時間點被使用,我們以一個 operation 作為一個時間單位,程式一開始為 0,每進行一個操作數值加一,因此數值愈小表示愈久未被使用。
```c
typedef struct cache_data{
int valid_bit;
int tag;
int time_be_used;
} cache_data;
cache_data **cache_table;
```
* 使用者輸入的 s 表示此 cache table 會有 $2^s$ 組
* 每組底下有 E 行 cache line
* 以動態宣告二維陣列的方式產生 cache table 並進行初始化
```c
int S = 1<<s;
cache_table = malloc(S*sizeof(cache_data *));
for (int i = 0; i < S; i++) {
cache_table[i] = malloc(E*sizeof(cache_data));
}
for (int i = 0; i < S; i++) {
for (int j = 0; j < E; j++) {
cache_table[i][j].valid_bit = 0;
cache_table[i][j].tag = 0;
cache_table[i][j].time_be_used = 0;
}
}
```
### 取得使用者輸入指令、參數 (-v -s -E ...)
* 使用[getopt(3)](https://linux.die.net/man/3/optarg)取得 argv 參數
* getopt 的基礎用法為:```getopt(argc, argv, "hvs:E:b:t:")```
- 第三個參數所代表的是使用者輸入: -h -v -s 等附加指令
- 參數後的冒號(:)表示該指令需要再帶入一參數,如 "s:" 表示實際輸入應為 -s test
- 指令後所帶的參數透過 optarg 取得
```c
char c ;
while((c=getopt(argc, argv, "hvs:E:b:t:")) != -1){
switch(c){
case 'h':
break;
case 'v':
ifprint = true;
break;
case 's':
s = atoi(optarg);
break;
case 'E':
E = atoi(optarg);
break;
case 'b':
b = atoi(optarg);
break;
case 't':
file_name = optarg;
break;
}
}
```
### 讀檔 & 字串處理
* 由於檔案帶有空白符,因此必須使用```fgets```而非```scanf```
* 搭配```strtok```對字串進行切割
- 用法為:```strtok(word, "\n")``` ,第一個參數為要切割的字串,第二個參數是切割的符號,切割的方式是留下切割符號之前的字串
- 透過 ```strtok(NULL, "\n")``` 取得切割符號後的字串
- ```fgets``` 會保留 "\n" 換行符號,因此會先進行切割
- 以 "空白" 將 operation 取出
- 以 "," 將地址、記憶體大小分離
* 需要特別注意的是**將 char 轉換成 16進位整數**
- 使用 ```strtol(buf, NULL, 16)``` 進行處理
```c
char word[50];
FILE *fp = fopen(file_name, "r");
while (fgets(word, 50, fp)){
count++;
if(ifprint)
printf("%s ", strtok(word, "\n"));
/* 處理讀檔內容 */
char *instruction = strtok(word, " ");
char *addr = strtok(NULL, ",");
unsigned int addr_hex = (unsigned int)strtol(addr, NULL, 16);
/* 將16進位地址再次處理 */
parse(s,b,addr_hex);
if(ifprint)
printf(" %d %d %d ", tag, set, offset);
switch(*instruction){
case 'I':
break;
case 'L':
check_cache();
break;
case 'M':
check_cache();
check_cache();
break;
case 'S':
check_cache();
break;
}
if(ifprint)
printf("\n");
}
```
* parse function, 將16進位地址進一步處理
- 可拆解成 \[tag]\[set]\[offset] 三個部分
- offset 為 b 個 bit (使用者輸入 -b 帶的數值)
- set 為 s 個 bit (使用者輸入 -s 帶的數值)
- tag 為剩下的位元,這次一共 32 bits,所以 tag 的位元數為 32-s-t
了解各位元如何區分後以簡單的 bitwise 進行分解
```c
void parse(int s, int b, int var){
unsigned int mask_offset = (1 << b) - 1;
unsigned int mask_set = (1 << (s+b)) - ( 1 << b);
unsigned int mask_tag = ~0 - (mask_offset + mask_set);
tag = (var & mask_tag) >> (s+b);
set = (var & mask_set) >> (b);
offset = var & mask_offset;
}
```
### 檢查 cache table
cache table 檢查只需要依據 set、tag 跟 valid bit 進行判斷即可
* set、tag 皆相同且 valid bit 為 1 表示 hit ,可直接返回
* 非 hit 的情況皆為 miss
* 檢查每個組中所有行的 valid bit,若 valid bit 為 0 則直接將資料搬入該行 cache 並返回;若一組中所有 cache 的 valid bit 都為 1 則比較最近一次的使用時間,紀錄最近最少使用的
- 我們的計算方式以每一個 operation 為一個時間單位,紀錄該 cache 在第幾個時間單位被使用,因此數值愈小表是最近愈少使用。
* 將最近最少使用的 cache 取代
```c
void check_cache(){
for (int i = 0; i < E; i++) {
if(cache_table[set][i].valid_bit == 1 && cache_table[set][i].tag == tag){
cache_table[set][i].time_be_used = count;
hits += 1;
if(ifprint)
printf("hit ");
return;
}
}
misses += 1;
if(ifprint)
printf("miss ");
int n_lru = 0;
for (int i = 0; i < E; i++) {
if(cache_table[set][i].valid_bit == 0){
cache_table[set][i].valid_bit = 1;
cache_table[set][i].tag = tag;
cache_table[set][i].time_be_used = count;
return;
}
else{
if(cache_table[set][n_lru].time_be_used > cache_table[set][i].time_be_used)
n_lru = i;
}
}
evictions += 1;
if(ifprint)
printf("eviction ");
cache_table[set][n_lru].valid_bit = 1;
cache_table[set][n_lru].tag = tag;
cache_table[set][n_lru].time_be_used = count;
}
```
## Part B
### 要求
前情提要:轉置矩陣時會有 cache miss發生,但由於矩陣的記憶體空間是連續的,所以可以運用其 locality優化,降低cache miss。
* 這部分的實驗要求針對 s=5, E=1, b=5 進行優化。
* 要進行轉置的矩陣有 32x32(miss < 300), 64x64(miss < 1300), 61x67(miss < 2000) 三個尺寸。
### 轉置 32x32 矩陣
首先觀察 cache 中每個 block 的大小,我們能算出一個 block(b=5) 有 32 bytes 的空間,能裝得下 8 個 int。於是以每列(row)8 個 int 逐列轉置矩陣。
| |0~| 8~|18~|24~|
|:-:|:-:|:-:|:-:|:-:|
| 0|index1|index2|index3|index4|
| 1|...|...|
| 2|...|...|
| 3|...|||
| 4|...|||
| 5|...|||
| 6|...|||
| 7|...|...|index31|index32|
| 8|...|...|
```shell
yichung@merry:~/git/cache_lab(master 16h36m)$ ./test-trans -M 32 -N 32
Function 0 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 0 (Transpose submission): hits:1710, misses:343, evictions:311
```
但仍無法達到要求,
觀察 trace 檔,發現不斷讀出/存入的過程中,輪流使用 AB 矩陣,容易因為相同的 index (s),產生 cache miss。進一步改良,一次從 A 載入 8 個 int,再一次 8 個存入 B 矩陣。
```shell
yichung@merry:~/git/cache_lab(master 16h39m)$ ./test-trans -M 32 -N 32
Function 0 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 0 (Transpose submission): hits:1766, misses:287, evictions:255
```
成功完成目標。
### 轉置 64x64 矩陣
第一次相同的方式執行,64x64 的矩陣運算 cache miss 飆到不合理的 4611 次。
試著畫出矩陣中每 8 個 int 對應 block 的 index。
ArrayA:
| | 0~| 8~|18~|24~|32~|40~|48~|56~|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| 0|index1|index2|index3|...|...|...|index7|index8|
| 1|...||
| 2|...|||
| 3|index25|index26|...|...|...|...|index31|index32|
| 4|index1|index2|index3|...|...|...|index7|index8|
| 5|...|||
|...|
試著改為4個4個 int 操作,cache miss 次數下降為 1651。
```shell
yichung@merry:~/git/cache_lab(master 16h36m)$ ./test-trans -M 64 -N 64
Function 0 (2 total)
Step 1: Validating and generating memory traces
Step 2: Evaluating performance (s=5, E=1, b=5)
func 0 (Transpose submission): hits:1710, misses:1651, evictions:311
```
但4個4個操作卻又浪費了,block 空間,試著載入多一點的 int 並操作。但由於是進行矩陣轉置,無法同時對AB兩個矩陣進行優化,一直無法突破。
參考[马天猫Masn的 Cachelab 筆記](https://zhuanlan.zhihu.com/p/28585726)
![](https://pic2.zhimg.com/v2-85af81ad19187208673aacbb0cb42f69_r.jpg)
此方式透過時間上的 locality 降低了 cache miss。
中間步驟,A左下角讀出->B右上角讀出->B右上角寫入->B左下角寫入。連續操作B上角的空間,降低了 cache miss。
### 轉置 61x67 矩陣
可以發現這次限制不如前次來得嚴格,先以 32x32 的方式轉置,發現分區轉置能降低 cache miss。
以數種不同大小的分區轉置不規則的 61x67 矩陣,最終在 15. 16 時有最小的 cache miss。