2018q1 Homework1 (phonebook)
===
contributed by <`piggyking1421`>
### Reviewed by `nashi5566`
* cache 分成 L1i L1d 的理由可以思考一下 instruction 和 data 之間被使用時有什麼行為差異 (eg. read or write)
### Reviewed by `0922james0922`
* 應該確實將改正過程跟結果放上去,不該只有想法跟理念,確實實做吧!!
## 系統環境
```
$ uname -a
Linux pc 4.10.0-28-generic #32~16.04.2-Ubuntu SMP Thu Jul 20 10:19:48 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
$lscpu
Architecture: x86_64
CPU 作業模式: 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
每核心執行緒數: 2
每通訊端核心數: 4
Socket(s): 1
NUMA 節點: 1
供應商識別號: GenuineIntel
CPU 家族: 6
型號: 58
Model name: Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz
製程: 9
CPU MHz: 2136.828
CPU max MHz: 3900.0000
CPU min MHz: 1600.0000
BogoMIPS: 6784.41
虛擬: VT-x
L1d 快取: 32K
L1i 快取: 32K
L2 快取: 256K
L3 快取: 8192K
NUMA node0 CPU(s): 0-7
```
## 前置作業
* 順利安裝 Lubuntu 雙系統,不過一兩週後因為某些原因要換台電腦使用,希望不會有什麼意外
* 由於對 git 和 linux 指令以及 vim 等工具都很不熟,剛開始完全不知道要怎麼下手,感謝[`ryanpatiency`](https://hackmd.io/s/B1SSiTM_G#)的重點整理讓我能有個大概,但許多教學看了卻都還沒練習,只知道了比較基本的操作,剩下的要邊做邊讀了 (感覺一週16小時遠遠不夠 CS:APP 也還沒讀完呢
## 基本練習
先以原始版本的檔案練習
先清空 cache
$ echo 1 | sudo tee /proc/sys/vm/drop_caches
$ make run
其中一筆執行時間及資料大小
```
size of entry : 136 bytes
execution time of append() : 0.067350 sec
execution time of findName() : 0.005744 sec
```
$ make cache-test
剛開始執行時因為權限無法執行
```
Consider tweaking /proc/sys/kernel/perf_event_paranoid,
which controls use of the performance events system by
unprivileged users (without CAP_SYS_ADMIN).
The current value is 3:
-1: Allow use of (almost) all events by all users
>= 0: Disallow raw tracepoint access by users without CAP_IOC_LOCK
>= 1: Disallow CPU event access by users without CAP_SYS_ADMIN
>= 2: Disallow kernel profiling by users without CAP_SYS_ADMIN
Makefile:33: recipe for target 'cache-test' failed
make: *** [cache-test] Error 255
```
剛開始直接用 sudo 去 make,後來參照[`team6612
`](https://hackmd.io/Yuo-57rAQ3q79A-20oVLTg?view#)改好權限即可直接用user執行
```
Performance counter stats for './phonebook_orig' (100 runs):
2,055,227 cache-misses # 94.373 % of all cache refs ( +- 0.08% )
2,177,780 cache-references ( +- 0.13% )
261,722,381 instructions # 1.31 insn per cycle ( +- 0.02% )
200,287,265 cycles ( +- 0.24% )
0.056767669 seconds time elapsed ( +- 1.97% )
```
可看到 chche-misses 機率約為94%
$ make plot
![](https://i.imgur.com/a5Ijp2f.png)
## 作業要求
* 降低 findname( ) 的 cache miss
* 縮短 append( ) 時間
* 引入多項效能改進方式並進行比較
* 用其他資料結構改寫 e.g. binary search tree
* 使用 hash function 加速查詢
* 嘗試字串壓縮
## Cache
改善效能前發現對cache印象有點模糊,看了[ Cache 原理和實際影響](https://hackmd.io/s/HkW3Dr1Rb#)和[ Cache 原理和實驗](https://hackmd.io/s/S14L26-_l#)中的一些參考資料釐清了一些基本觀念
* L1 Cache 分成 L1i 和 L1d , L1i ( i for instruction )用來放指令, L1d ( d for data )用來放資料,暫時想不到這樣分有哪些好處
* CPU 執行運算時會先到 L1i 看接下來要做的指令,再依照指令到 L1d 中尋找資料,若找不到便向 MMU (記憶體管理單元)請求資料,MMU 會到TLB (存放虛擬記憶體位址到實體記憶體位址的分頁表快取),若在 TLB 中找到 (TLB hit),便依序到L2 , L3 , 記憶體請求資料,若是 TLB 中沒有要找的虛擬地址(TLB miss),MMU 便會去 實體記憶體中的分頁表找,並且存入TLB。
* 看文章的意思似乎是 L1 中放的是虛擬地址,而其餘的是實體位址,為何?
* ==cache line== 是 cpu cache 中的最小單位,多為 64 Bytes
* 我的電腦 L1i 和 L1d 皆為 32k,也就是各有500個 cache line
* 我的 CPU i7-3770 是 Ivy Bridge 架構, cache line是64 Bytes ,使用的 mapping function 是 8-way associative
## Makefile
* 在看 phonebook 的 makefile 時候覺得有點難懂,所以紀錄一下。
* 看[ Makefile 語法和示範](https://hackmd.io/c/rJKbX1pFZ/https%3A%2F%2Fhackmd.io%2Fs%2FSySTMXPvl)之後還是很模糊,可能我的背景知識不太夠,或是因為我可憐的語文理解能力,之後另外查了[ GNU Make info](https://www.gnu.org/prep/standards/),但是發現實在是太多了,決定對照其他資料,遇到問題在看這個info文件
* [ Makefile 語法和示範](https://hackmd.io/c/rJKbX1pFZ/https%3A%2F%2Fhackmd.io%2Fs%2FSySTMXPvl)裡面的參考資料的==詳盡語法==好像失連了?
* Makefile 基本語法
* 最主要大概就是
```=
target : 相依檔案
command
```
* ==target==通常是要產生的文件名稱或是一個動作的名稱
* 當具備 target 需要的相依檔案,就會去執行command,並產生target檔案
* 相依檔案也會有target,像是
```=
test : a.o b.o
command
a.o : a.c
gcc a.c -o a.o
b.o : b.c
gcc b.c -o b.o
```
* a.o 和 b.o 就是產生test的相依檔案的過程target,當 test 沒有 a.o 和 b.o 時,便會去執行 a.o 和 b.o 兩個 target,而test是最終需要的target
* 也能使用 make a.o 單獨執行其中一個target,以 phonebook的makefile舉例
```=
CC ?= gcc
CFLAGS_common ?= -Wall -std=gnu99
CFLAGS_orig = -O0
CFLAGS_opt = -O0
EXEC = phonebook_orig phonebook_opt
GIT_HOOKS := .git/hooks/applied
.PHONY: all
all: $(GIT_HOOKS) $(EXEC)
$(GIT_HOOKS):
@scripts/install-git-hooks
@echo
SRCS_common = main.c
phonebook_orig: $(SRCS_common) phonebook_orig.c phonebook_orig.h
$(CC) $(CFLAGS_common) $(CFLAGS_orig) \
-DIMPL="\"$@.h\"" -o $@ \
$(SRCS_common) $@.c
phonebook_opt: $(SRCS_common) phonebook_opt.c phonebook_opt.h
$(CC) $(CFLAGS_common) $(CFLAGS_opt) \
-DIMPL="\"$@.h\"" -o $@ \
$(SRCS_common) $@.c
run: $(EXEC)
echo 3 | sudo tee /proc/sys/vm/drop_caches
watch -d -t "./phonebook_orig && echo 3 | sudo tee /proc/sys/vm/drop_caches"
cache-test: $(EXEC)
perf stat --repeat 100 \
-e cache-misses,cache-references,instructions,cycles \
./phonebook_orig
perf stat --repeat 100 \
-e cache-misses,cache-references,instructions,cycles \
./phonebook_opt
output.txt: cache-test calculate
./calculate
plot: output.txt
gnuplot scripts/runtime.gp
calculate: calculate.c
$(CC) $(CFLAGS_common) $^ -o $@
.PHONY: clean
clean:
$(RM) $(EXEC) *.o perf.* \
calculate orig.txt opt.txt output.txt runtime.png
~
```
* 若是只 make clean (第49行),因為沒有相依檔案,便只會執行 clean 中的 command
* 因為並沒有叫做 clean 的檔案,因此使用 ==.PHONY==可以使 clean 變成一個"假的" target,當 make clean 的時候會不管是否有 clean 這個檔案,都會執行 clean 這個 target。可以避免有同樣叫做 clean 的檔案存在,導致 clean 不執行。
* ==.PHONY==也可以讓make一次默認執行多個 target,第9~10行便是讓 all 變成假的 target,不會產生 all 檔案,可是 all 又和三個檔案有相依關係,所以會執行3個 target。
* 執行 target 時除了相依檔案不存在時會影響要執行的部份,makefile 也會自行判斷 target 與相依檔案之間的新舊程度去決定是否要執行相依檔案的 target。若是 target 比 相依檔案都來的新,那就不會再次執行那個target。可以減少不必要的編譯量。
* 而若是 make plot 便會去找 output.txt,找不到就會執行 output.txt 這個 target,且還會去找 calculate 和cache-test 存不存在,以此類推直到所有相依檔案都存在或是相依檔案比target舊為止。
* 1~8行都是在宣告變數和指定值,使用時要以$(變數名稱)的方式使用,有以下幾種
* =
一般的給予變數初始值,會在整個 makefile 展開後才決定變數的值,也就是 makefile 中最後被指定的值
* :=
在 makefile 展開途中就決定變數的值
``` =
x = abc
y = $(x) bar
x = xyz
# 這時 y 的值為 xyz bar
```
但若是使用了:=
``` =
x := abc
y := $(x) bar
x := xyz
# 這時 y 的值為 abc bar
```
也就是說變數的值會和其被指定值的位置有關係
* ?=
若變數尚未被指定值,則指定其值,若已有值則忽視此行
* +=
將值加在現在變數已有的值的後方
* **自動化變數**,用來省略檔名,makefile 中大量使用,剛開始看的時候讓我眼花撩亂,有以下幾種
* $@
當下那個 target 的檔名
* $<
當下那個 target 的第一個相依檔名
* $^
當下那個 target 中所有相依檔的檔名,會以空格分開
* $*
最主要的 target 檔名
* **反斜線**
* \
* **換行符**,雖然 makefile 沒有限制命令的長度,但為了可以不用一直向右滾螢幕才看得到命令,使用 \ 可以把指令打在下一行,但實際上仍然會在同一行,像是第18~26行。
* **轉義字元** 在一些有特殊功能的符號前面加上反斜線可以使其失去其意義,並顯示出來,像是#原本是用來表示該行是註釋,而打 ==\\#== 即可打出#
* **萬用字元**
* %
可以取代任何字元,像是%.c便可以代表所有.c檔
* **特殊字元**
* ==@==
一般make執行的command會顯示出來,而在command前面加上@就可以不顯示執行的指令
* ==-==
一般make只要遇到錯誤就會中斷執行,如果有不影響make的指令在前方加上 - 即可使指令即使錯誤仍然可以繼續make
* 之前看影片老師說要記得安裝 git.hook,這次作業上卻寫說執行 make 會自動安裝 hook ,看到 Phonebook 的 makefile 第8和12~14行才知道是在 makefile 中利用 scripts 去安裝,了解了 makefile 寫好可以省很多事,不過 scripts 還看不懂,之後應該還要看 git.hook 的 info 和一些 shell 的寫法。
* 第20和25行的 -D 是 gcc 的 定義巨集的語法,[GCC Command Options](https://gcc.gnu.org/onlinedocs/gcc-2.95.2/gcc_2.html),原本我還以為是makefile的。
* 第20行和25行還看不懂,知道是定義IMPL但是看不太懂那些雙引號和反斜線
* 還有許多高深的用法,但是在沒有練習的情況下感覺記不住,上面的內容能夠大概看懂 phonebook 的 makefile 了
## 實驗
### 方法一 改動entry size
* 在原本的phonebook_orig中
```clike=
typedef struct __PHONE_BOOK_ENTRY {
char lastName[MAX_LAST_NAME_SIZE];
char firstName[16];
char email[16];
char phone[10];
char cell[10];
char addr1[16];
char addr2[16];
char city[16];
char state[2];
char zip[5];
struct __PHONE_BOOK_ENTRY *pNext;
} entry;
```
* 資料大小直接相加應該會為 131 bytes,但使用sizeof 得到的卻是 136 bytes,去查了是因為記憶體對齊 (alignment)的關係,我的電腦是64位元,因此會以8 bytes 當作一個 word,因為每一筆資料大小不相同,而為了不讓一筆資料( e.g. int )在記憶體分配的時候橫跨超出一個word,導致搜尋一個資料要存取記憶體兩次(存取兩個 word size),記憶體對齊會加上 padding byte 使資料不會橫跨 word ,因此這裡才會是 136 bytes,在之後打程式的時候也許能盡量分配資料量,減少padding bytes的出現
* 我的電腦是 L1大小是 32 KB,cache line 是 64 bytes,使用的是 8-way associative,一筆 entry 是 136bytes,也就是說一筆entry會佔用3個cache line
卡住了 我再想想
## 待查問題
* L1 cache 分成 L1i 和 L1d 有哪些好處?
我的猜測是因為 instruction 和 data 的更新頻率會相差很多,instruction的重複性比較高,替換頻率比較低,之後再詳細查閱
## Reference
[CPU Cache 原理探討](https://hackmd.io/s/H1U6NgK3Z#)
[Makefile 心得](https://wwssllabcd.github.io/blog/2016/10/03/how-to-write-make-file/)
[跟我一起写Makefile:书写规则](http://wiki.ubuntu.org.cn/%E8%B7%9F%E6%88%91%E4%B8%80%E8%B5%B7%E5%86%99Makefile:%E4%B9%A6%E5%86%99%E8%A7%84%E5%88%99)
[Data structure alignment](https://en.wikipedia.org/wiki/Data_structure_alignment)