# 2016q3 Homework2 (phonebook-concurrent)
contributed by <`LanKuDot`>
## 閱讀筆記
### [The free lunch is over](http://www.gotw.ca/publications/concurrency-ddj.htm)
* Free (performance) lunch 指的是程式設計的效能可以透過 CPU 時脈的進步而得到改善。會說 over 是因為 CPU 的時脈無法得到更進一步的增加,由於耗電和散熱的問題,所以程式設計師必須要修改程式才能改善效能
* 文章中 `Figure 1`,可以看到 CPU 時脈並沒有隨著電晶體數量而增加,反而是趨緩了
* 以前 CPU 如何增進效能
* Clock speed
* Execution Optimization
* Pipelining、Branch prediction
* Out-of-order execution 還要注意不能讓原本程式崩潰 (read/write reorder)
* Cache:盡量減少存取 Main memory 的機會
* 現在 CPU 如何增進效能
* Hyperthreading:在 single CPU 上同時執行多個 thread,但是共用 ALU、FPU
* Multicore:多個 CPU
* 迷思:2 x 3GHz < 6GHz
* Cache
* 軟體開發革命 (Software development revolution) 是源自於存在一陣子的技術 (擁有成熟的銷售商和工具支援),而非新技術一出現即發生。
#### Concurrency
* 兩個會使用 Concurrency 的理由:分離可獨立運作的工作;為了效能,像是使用多核新的 CPU
* 難點:並非所有的程式都合適平行化;程式要設計可以實行 Concurrency 是困難的
* 如果程式的相依性太高或是太常取用共同資源則會拖慢 Concurrency 的效率
* 作者提出 Concurrency 對我們的影響
1. 想要完全使用到 CPU 所有的資源
2. 程式越來越有機會造成 CPU-bound。雖然主要還是 IO-bound 等,但如果 CPU 時脈無法增加,而其他存取方式速度變快,最後會發生 CPU-bound
3. 效能優化將會越來越重要
4. 程式語言強迫必須好好處理 concurrency
### [Concurrency is not parallelism](https://blog.golang.org/concurrency-is-not-parallelism)
* **Concurrency** 是指程式架構,將程式拆開成多個可獨立運作的工作。eg:drivers,都可以獨立運作,但不需要平行化。
* 拆開多個的工作不一定要同時運行
* 多個工作在單核心 CPU 上運行
* **Parallelism** 是指程式執行,同時執行多個程式。Concurrency 可能會用到 parallelism,但不一定要用 parallelism 才能實現 concurrency。eg:Vector dot product
* 程式會同時執行 (例如:分支後,同時執行,再收集結果)
* 一個工作在多核心 CPU 上運行
### Concurrecncy 系列
* A **happends-before** B:在 B 開始執行之前,A 的執行效果必須可見。
* A is **Sequenced-Before** B:在**同一個 thread** 下,A 比 B 先求值。
* A is **synchronized-with** B:A 在這一個 thread 對記憶體的操作,在**另一個 thread** 的 B 必須可見。
* Sequential consistency:無論處理器實際怎麼執行,確保結果跟照程式順序執行一樣
* Read before Write:在新值寫入前讀取到舊值
* Write after Write:在正確值寫入後被舊值複寫 (有先後)
### Process && Thread && Coroutine
[[Source](http://learn-gevent-socketio.readthedocs.io/en/latest/general_concepts.html)]
* Process:一個完整的執行程式,擁有自己的 memory space、執行檔的鏡像、系統配給的存取資源的 descriptor 等
* Thread:輕量的 process,在 process 中被創造,**與同一個 process 中的其他 thread 共用 process 的資源**。
* Coroutine:合作函式 (Cooperative **functions**)。允許函式執行到一半離開 (`yield`),去執行其他函式,再次呼叫這個函式時會從上次離開點開始執行。
#### [用 macro 實現 coroutine](http://blog.linux.org.tw/~jserv/archives/001848.html)
* 將 Jserv 老師給的用 macro 實現 coroutine 的程式只經過 Preprocessor 處理,會比較容易理解運作,以下程式碼是整理過後的
```c=
static int condition = 1;
static void user_thread_1()
{
static int __s = 0;
switch (__s) {
case 0:;
for (;;) {
printf("Run %s\n", __func__);
{ __s = 21;
usleep(THREAD_INTERVAL);
return;
case 21: ;
};
}
} __s = 0;;
}
static void user_thread_2()
{
static int __s = 0;
switch (__s) {
case 0:;
for (;;) {
if (condition) {
printf("Run %s - (1)\n" , __func__);
{ __s = 32;
usleep(THREAD_INTERVAL);
return;
case 32: ;
};
}
printf("Run %s - (2)\n", __func__);
condition = !condition;
{ __s = 36;
usleep(THREAD_INTERVAL);
return;
case 36: ;
};
}
} __s = 0;;
}
```
```
Output:
Run user_thread_1
Run user_thread_2 - (1)
Run user_thread_1
Run user_thread_2 - (2)
Run user_thread_1
Run user_thread_2 - (2)
Run user_thread_1
Run user_thread_2 - (1)
Run user_thread_1
Run user_thread_2 - (2)
Run user_thread_1
Run user_thread_2 - (2)
...
```
* 比較令我在意的是那個 case in for loop,所以我寫了小程式驗證
```c=
static void caseInForLoop()
{
static int __s = 0;
switch (__s) {
case 0:
for (;;) {
printf("Run to case 0.\n");
__s = 1;
return;
case 1:
printf("Run to case 1.\n");
}
}
}
int main(void)
{
caseInForLoop();
caseInForLoop();
return 0;
}
```
```
Output:
Run to case 0.
Run to case 1.
Run to case 0.
```
* ~~驚呆了~~,可以發現到 `caseInForLoop()` 只有呼叫兩次,但實際輸出卻有三次!
* 分析:
* 第一次執行沒有問題,一開始 `__s` 的值為 0,所以會印出 case 0 的訊息,然後值被改為 1,之後回傳。
* 然而第二次執行時,`__s` 值為 1,從輸出可以知道他跳到 case 1 開始執行,之後又回到迴圈頭重新執行,所以又會再輸出一次 case 0 的訊息
* 我現在才知道 switch-case 的 `case` 是類似 `goto`,以 `switch` statement 裡面的值決定要從 switch block 裡面的那一個點開始執行,而 `break` 是跟程式說執行到這裡就好了
* 回到 Jserv 老師的程式,`user_thread_1` 比較好理解,與測試程式一樣,只有第一次進入時 `__s` 值為 0,之後在無限迴圈中值都會保持在 21,從 `case 21` 開始執行,在無限迴圈中印完訊息後,delay、回傳。
* 至於 `user_thread_2`,
* 第一次進入時 `__s` 為 0,`condition` 為 1,所以會印出 `(1)`,`__s` 變為 32
* 再來從 `case 32` 開始,印出 `(2)`,`condition` 為 0,`__s` 變為 36
* 然後下次進來從 `case 36` 開始
* 這樣應該就會發現**每次再次呼叫函式時,都會從上次的離開點的下一行開始執行**,這正是 coroutine 中 `yield` 的功能。再檢視一次 `cr_yield` macro
```c
#define cr_yield \
{ __s = __LINE__; \ // 紀錄下一次進來的起始點
usleep(THREAD_INTERVAL); \
return; \ // 離開了
case __LINE__: ; \ // 再回來就從這裡開始
}
// 用 __LINE__ macro 作為 case 的好處在於一個函式中
// 可能會使用兩次以上的 yield,所以必須要區分不同 yield
// 的重新進入點,比起手動編碼,不如用行號來區分
```
### Switch-Case
* `switch` statement 會從輸入的變數值判斷要從 `switch` statment block 中的哪一個部分開始執行。而 `case` 就是一個標籤,告訴 `switch` statment 可以從哪裡開始,`break` 則是到哪裡結束。
* 注意 `case` 之間的 scope 除非特別指定 (用 `{}`),否則是相通的。見下例:
```clike
switch (foo) {
case 1:
int z = 1;
printf("%d\n", z);
break;
case 2:
int z = 2;
printf("%d\n", z);
break;
}
```
* 因為 case 之間的 scope 是相通的,會發現在同一個 switch scope 下出現兩個 `z` 的宣告 (把 `case` 無視掉可以比較容易理解),編譯就會出現錯誤訊息。
```clike
switch (foo) {
// case 1:
int z = 1;
printf("%d\n", z);
break;
// case 2:
int z = 2;
printf("%d\n", z);
break;
}
```
* 所以要避免這個情況的話,就要特別指定 `z` 的 scope,讓他只適用在一個 `case` statement 下:
```clike
switch (foo) {
case 1: {
int z = 1;
printf("%d\n", z);
break;
}
case 2: {
int z = 2;
printf("%d\n", z);
break;
}
}
```
* 或
```clike
switch (foo) {
case 1: {
int z = 1;
printf("%d\n", z);
}
break;
case 2: {
int z = 2;
printf("%d\n", z);
}
break;
}
```
### Refactoring
[[Source 1](http://teddy-chen-tw.blogspot.tw/2014/04/refactoring.html)] [[Source 2](http://www.ithome.com.tw/node/79813)]
* 「在不改變軟體的外在行為之下,**改善**既有軟體的內部設計」
* 對於既有的 context (codebase,原始碼),找到需要重構的對象 form。form 中有東西因為無法平衡 force 而發出壞味道 (像 long method 就會受到 explanation、sharing、choosing 的 force),讓整個 form 也有壞味道。
* Refactoring 可以調和不平衡的 force,讓 form 更合適這個 context (例如:提昇可讀性),但不一定能完全消除壞味道。
* Refactoring 隨時在進行 (為了增加功能、除錯、程式碼審閱而觸發 refactoring)
### SuperMalloc
* 原本不同時候配置的記憶體會分散在記憶體的各處。SuperMalloc 針對每種不同的 object 大小給予一塊連續的記憶體,例如:bin 0 專屬於 8 bytes 的 objects,bin 1 專屬於 10 bytes 的 objects 以此類推。`size_2_bin` 負責運算要配置的物件需要從那一塊記憶體去配置。
# 作業
## 程式碼解析
### `file_align.c`
* 目的在於將檔案中每一行的字串長度貼到指定長度
* `stat`:display file or file system status
* 用 `$ man 2 stat` 可以看到其函式的用法
* `int stat(const char *pathname, struct stat *buf);`
* `struct stat`:是用來儲存檔案資訊的資料結構,包含檔案大小、類型、修改日期等資訊
* `strncpy`:複製 source 的前 n 個 byte 到 destination
* 如果 source 的程度小於 n 則會用 0 補滿
* 經過轉換之後的字串會是:`<originText>(會有newline)+<serial of \0>`
* 用 `size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);` 會取來自 `*ptr` 直接寫入 `size * nmemb` bytes 到檔案中,不會因為有 null character 而中斷寫入
* 所以用 vim 開啟 aligned 後的檔案會看到 `^@`,這是 null character
#### 程式問題
1. 第一步先改檔名XD,我覺得用 `text_align.c` 或 `line_align.c` 更能描述這個程式的功能
2. `wbuf` 是用 `malloc` 配置的,程式結束時沒有使用 `free`
3. 當讀入的字串長度"不等於"要 padding 到的長度,`rbuf` 的內容才會被複製到 `wbuf`,問題有二:
a. 當讀入字串長度等於 padding 的長度時 (`!= 0`不成立),沒有任何東西被寫到 `wbuf`,會造成這筆資料遺失
b. 當字串資料過長時,會被截短。
* 我這邊選擇的修正是無論來源字串多長,就是會複製要 padding 的 bytes,但是對於過長的來源字串會輸出警告訊息給使用者
```c=
suffix = pad - strlen(rbuf);
// The length of the input text is longer than the length to padding to.
// Warn the user, and still write to the output file but with only
// first "PadToLen" bytes.
if (suffix < 0)
printf("Warning:"
" The length of %s is longer than %d.\n", rbuf, pad);
strncpy(wbuf, rbuf, strlen(rbuf));
```
4. `file.c` 與 `file_align.c` 很相似,查看兩者用途發現 `file.c` 是模組化的版本
* 留下模組化的版本,建立 header file,讓主程式及測試程式都可以直接使用。模組化的好處是相容性高,而且只要維護一組程式碼就好
### `debug.h`
* 提供 macro `dprintf`,與 `printf` 的用法一樣,只是會在前面增加 `DEBUG:` 的字樣,透過定義 `DEBUG` macro 啟用。
* 與 c library 提供的 `dprintf` 功能不一樣,c library 的為將輸出寫入指定的 file descriptor
### `phonebook_opt.c`
* 第一眼看到函式跟變數名稱不知道用途((淚
* `append_a`:主要是用在傳遞給 thread 的訊息之資料結構,以下是修改過後的,更能辨別用途
```c
typedef struct _thread_argument {
char *data_begin;
char *data_end;
int threadID;
int numOfThread;
entry *lEentryPool_begin;
entry *lEntry_head;
entry *lEntry_tail;
} thread_arg;
```
* 其對應產生函式 `new_append_a()` 改為 `createThread_arg()`
* kl
### 主程式
1. 將檔案的內容映射到 memory 上,讓每個 thread 可以同時存取。
* `mmap`:將指定檔案的資料映射到 process 的 virtual memory space 上,可以防止 blocking I/O 的發生。
> 關於 `mmap` 詳細用法可以參考[林哲亘的共筆](https://hackmd.io/s/rJPYFYRa)
>
* 根據 manual,有提供 `munmap` system call 來刪除配給的區塊,不過在程式結束時,這個區塊會自動被 delete 掉。關閉對應的 file descriptor 不會刪除配給的記憶體
2. 一次建立足夠的空 entry 給所有 thread 使用,避免因為 `malloc` system call 所造成的 waiting。
3. `pthread_setconcurrency`:Setting the concurrency level allows the application to give the system a hint as to the number of kernel-scheduling entities that should be provided for efficient execution of the application,還不太懂用途
4. 準備每個 thread 所需要的資訊,例如:讀取資料區間的開頭與結尾、寫入資料區間的開頭與結尾等
5. 創造 thread,開始讓各自的 thread 工作
6. 等待所有 thread 完成之後將每一段 thread 所屬的 entry list 組合起來
#### 程式問題
* 開啟 aligned 之後的檔案但是沒有關閉,透過 `open` 開啟檔案會配置資源給對應的 file descriptor,要用 `close` 來釋放資源
* 搭配 `valgrind` 檢查有無 memory leak 的問題。可以發現 malloc 28次,卻只有 13 次 free
```
==4768== HEAP SUMMARY:
==4768== in use at exit: 2,199 bytes in 15 blocks
==4768== total heap usage: 28 allocs, 13 frees, 8,410,205 bytes allocated
==4768==
==4768== LEAK SUMMARY:
==4768== definitely lost: 585 bytes in 11 blocks
==4768== indirectly lost: 0 bytes in 0 blocks
==4768== possibly lost: 0 bytes in 0 blocks
==4768== still reachable: 1,614 bytes in 4 blocks
==4768== suppressed: 0 bytes in 0 blocks
==4768== Rerun with --leak-check=full to see details of leaked memory
```
* 處理 memory leak
* 既然 `THREAD_NUM` 是在 compilation 時期指定,則隨 `THREAD_NUM` 變動的 array 長度可以不用使用 `malloc`
* `thread_args` 的元素也是透過 `malloc` 取得,要 free 掉
* 在 `findName()` 中會給找到的 entry 的 `lastname` 和 `detail` 配給記憶體,但是沒有處理已經被搜尋過的 entry 不需要再配置記憶體的情況,像在主程式中搜尋 `zyxel` 就有三次,造成前兩次配置的記憶體遺失。
* 解決方案:在 `mmap` 多加一個 `PROT_WRITE` 的 flag,直接在映射的記憶體上處理 `\n`,而不配置記憶體給他。另外在 `append` 中取用一個新的 entry 時也會同時初始化 `dtl` 為 `NULL`,讓 `findName()` 判斷要不要配置新記憶體給他。另一個好處是在釋放 entry 的記憶體時不需要檢查 `dtl` 是否為 `NULL`,根據 [manual](http://man7.org/linux/man-pages/man3/malloc.3.html) 如果 `free()` 收到 `NULL` 並不會做任何事情。
> 不過我在修改這裡時發生神奇的事情,原先我只加 `PROT_WRITE`,會在第一個 assertion (問你有沒有實做 findName 那個) 失敗。將 `MAP_SHARE` 改成 `MAP_PRIVATE` 才正常運作。是因為沒有 sync? (同一個程序去修改需要 sync 嗎?)
>
* 但還是有 memory leak,所以我透過 `valgrind --tool=memcheck --leak-check=yes <executable>` 可以更詳細知道哪裡有配置記憶體但沒有 free 的程式碼,還包含 call stack。一個是 `pHead` 在 main 中被移動到下一個,造成原版的頭遺失。另一個是在 `findName()` 中配置的 detail 記憶體沒有被一併 free 掉。
```
==6392== 24 bytes in 1 blocks are definitely lost in loss record 1 of 7
==6392== at 0x4C2BBA0: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==6392== by 0x400F79: main (main.c:61)
==6392==
==6392== 107 bytes in 1 blocks are definitely lost in loss record 4 of 7
==6392== at 0x4C2BBA0: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==6392== by 0x401535: findName (phonebook_opt.c:18)
==6392== by 0x4012A6: main (main.c:148)
```
* 修正完還是有 5 個 block lost 掉,但是不知道在哪裡
>> 這時候就要出動 [LeakSanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizerLeakSanitizer),可一併對照 Mozilla Taiwan 的網誌: [Address-Sanitizer(ASAN): 一個 C/C++ 記憶體偵錯的工具](http://tech.mozilla.com.tw/posts/4282) [name=jserv]
>> 我使用 ASAN 去偵測,但是程式很安靜的結束了,不過我檢查 valgrind 的報表,他標示 5 個 block 是 still reachable,然後查 [StackOverflow](http://stackoverflow.com/questions/3840582/still-reachable-leak-detected-by-valgrind),提到可以不必擔心這一區塊的 memory,整理到下面。[name=LanKuDot]
* 另外這次的 refactoring 中,主程式花最多時間修改。我感受到為了讓優化過的版本可以運作,所以在 code 中這邊貼一塊,那邊補一塊 (原始版到處都是 `#ifndef, #else, #endif`)。就好像是要重新粉刷牆面的某一些地方,沒有照著原本牆面的粉刷方向去刷,整面牆就會有各種方向的刷痕。
* 整理過後讓 free processing、build entry、release the memory 這三個部份都只有一組 `OPT` 的 condition macro,增加可讀性。
### [ASAN(Addres-Sanitizer)](http://tech.mozilla.com.tw/posts/4282)
* 偵測 Memory corruption,LLVM 3.1 及 GCC 4.8 後內建 ASAN
* 可以偵測下列問題
* Use after free (Dangling pointer dereference)
* Heap buffer overflow
* Stack buffer overflow
* Global buffer overflow
* Use after return
* 編譯時加入 `-fsanitize=address`,編譯器會插入除錯程式碼,再加上 `-fno-omit-frame-pointer` 可以讓錯誤訊息回報正確的 call stack
* ASAN 是以 **continue-after-error** mode 執行,所以就算遇到第一個錯誤,程式還是可以繼續執行
* 示範一個 use after free 的程式
```c
#include <stdlib.h>
int main(void)
{
char *x = (char *)malloc(10 * sizeof(char));
free(x);
return x[5]; // Use after free
}
Compile: gcc -fsanitize=address -fno-omit-frame-pointer useAfterFree.c
```
```
Execution: ./a.out
=================================================================
==4547==ERROR: AddressSanitizer: heap-use-after-free on address 0x60200000eff5 at pc 0x4007f4 bp 0x7ffca341e6a0 sp 0x7ffca341e690
READ of size 1 at 0x60200000eff5 thread T0
#0 0x4007f3 in main
#1 0x7f4484687a3f in __libc_start_main
#2 0x4006c8 in _start
```
### 關於 Valgrind 回報的 still reachable
根據[這篇](http://stackoverflow.com/questions/3840582/still-reachable-leak-detected-by-valgrind)回答:memory leak 有兩種定義
* 配置的記憶體在程式結束後**沒有**被 free
* 配置的記憶體**無法**被 free,因為沒有有效指標指向他
只有第二種才是真正的 memory leak。然而如果 `valgrind` 回報的區塊是分類在 still reachable 的話 (由於 program 尚在追蹤指向這些區塊的指標),代表這些區塊雖然還沒被 free,但是之後還是可以被系統取回。所以不必擔心被分類在 still reachable 的區塊
```
==4443== HEAP SUMMARY:
==4443== in use at exit: 7,780 bytes in 5 blocks
==4443== total heap usage: 20 allocs, 15 frees, 8,409,647 bytes allocated
==4443==
==4443== LEAK SUMMARY:
==4443== definitely lost: 0 bytes in 0 blocks
==4443== indirectly lost: 0 bytes in 0 blocks
==4443== possibly lost: 0 bytes in 0 blocks
==4443== still reachable: 7,780 bytes in 5 blocks
==4443== suppressed: 0 bytes in 0 blocks
```
### 進一步優化 append()
原本的版本中 `append()` 還有兩個地方可以改進:
1. C 中 memory 是以 row-major 的方式儲存,但原本程式碼取址的方式 (包含 `data` 跟 `entry`) 卻以 column-major 去存取。這樣會造成大量 cache-miss 和 cache-reference。
* Thread 0: 0, 3, 6, 9, ...
Thread 1: 1, 4, 7, 10, ...
Thread 2: 2, 5, 8, 11, ...
2. 原版程式中的 local linked list 第一個是空資料,而且在連接的時候,會拋棄第一個 entry。所以如果配給剛好數量的 entry,然後都從第二個 entry 開始填資料,一定會有一筆資料沒有地方填。
所以我做了以下修正:
1. 每個 thread 取得的 entry 跟 data 會是連續的,以 row-major 的方式取址
* Thread 0: 0, 1, 2, 3, ...
Thread 1: 10, 11, 12, 13, ...
Thread 2: 20, 21, 22, 23, ...
* 要注意每個 thread 的範圍:我讓每個 thread 分到 `num_of_data / THREAD_NUM + 1` 的資料去處理,+1 是為了避免餘數集中在最後一個 thread。最後一個 thread 的資料結尾則直接指定 `data_pool` 的結尾。
>> 想問這邊 ```num_of_data / THREAD_NUM + 1``` 如果無法整除的話該如何處理??
>> 我嘗試過這樣做,但是如果在 words.txt 裡新增一筆資料後會有問題...
[name=littlewhiteYA]
2. 處理第二個問題,必須要先讓第一個 entry 填入資料,才能進入迴圈從第二個 entry 開始處理 (因為第一個 entry 沒有前面可以 append),也能避免少掉幾個資料的問題。
#### Cache 分析及執行時間
改變取址的方式得到了大量的改進,~~起飛拉~~
* 改進前:
* append time:0.019036 sec
```
Performance counter stats for './phonebook_opt':
254,263 cache-misses # 6.179 % of all cache refs [76.70%]
4,115,040 cache-references [49.89%]
251,354,400 instructions # 1.12 insns per cycle [76.12%]
224,681,630 cycles [74.70%]
```
* 改進後:得到近一倍的改善
* append time:0.009584 sec (49.6% improved)
* cache misses 62.9% improved
* cache references 41.7% improved
```
Performance counter stats for './phonebook_opt':
94,171 cache-misses # 3.922 % of all cache refs [78.01%]
2,400,810 cache-references [45.85%]
264,389,689 instructions # 1.41 insns per cycle [76.93%]
187,534,122 cycles [78.00%]
```
## 效能分析
[append() time], [findname() time] in seconds
* Baseline:0.145418, 0.022165
* Optimized_Origin:0.022656, 0.012122
* Handle memory leak:0.019036, 0.011447
* Use row-major `append()`:0.009584(!!!) 0.010219
