# 2016q3 Homework 1 (phonebook) contributed by <``yanzijun``> ## 作業環境 + OS: Ubuntu 16.04.1 LTS + CPU: Intel(R) Core(TM) i5-3337U CPU @ 1.80GHz + Memory: 8G + cache: + L1d cache: 32K + L1i cache: 32K + L2 cache: 256K + L3 cache: 3072K ## 前置作業 因為看到 [@aweimeow](https://hackmd.io/EwFgZghgDAjA7AVgLQA44QJxJAEwMwypwxRJR4ZxgBGcsUEKQA==) 與 [@louielu](https://hackmd.io/CYJgZgHA7AnADAFgLQGMCmaCGSEGYCsyMARjPkgIwhS4gT4VwogJA===#) 共筆有使用lstopo套件,因此也決安裝一下,以便獲取更多詳細的cache資料,方便後面的分析。 ### 安裝lstopo ```shell # 參考aweimeow提供的安裝方法 $ wget https://www.open-mpi.org/software/hwloc/v1.11/downloads/hwloc-1.11.4.tar.gz $ tar zxvf hwloc-1.11.4.tar.gz $ cd hwloc-1.11.4 $ cat README # 不過他的 README 沒有說明要怎麼安裝 Orz $ ./configure $ make $ sudo make install ``` 非常順利的安裝完成了,沒有噴任何錯誤.. 那就開啟lstopo吧! 因為我只要看 cache 資料,所以透過 --no-io 參數,使其不輸出I/O 裝置等資料 ```shell $ lstopo --no-io ``` 輸出 ``` Machine (7870MB) + Package L#0 + L3 L#0 (3072KB) L2 L#0 (256KB) + L1d L#0 (32KB) + L1i L#0 (32KB) + Core L#0 PU L#0 (P#0) PU L#1 (P#2) L2 L#1 (256KB) + L1d L#1 (32KB) + L1i L#1 (32KB) + Core L#1 PU L#2 (P#1) PU L#3 (P#3) ``` ### 安裝perf 先看Kernel config 有沒有開啟 Perf , PC安裝一般 Linux distro ,預設應該有開。 ```shell $ cat "/boot/config-`uname -r`" | grep "PERF_EVENT" ``` 如有開啟其值應該會是y ```shell CONFIG_PERF_EVENTS_INTEL_UNCORE=y CONFIG_HAVE_PERF_EVENTS=y CONFIG_PERF_EVENTS=y CONFIG_HAVE_PERF_EVENTS_NMI=y ``` 開始安裝perf ```shell $ sudo apt-get install linux-tools-common linux-tools-generic ``` 接著輸入perf top 查看系統目前各個函式再Event上的效能 ```shell $ perf top ``` 應該會出現下面這種情形 ![image alt](http://wiki.csie.ncku.edu.tw/embedded/perf/perf_top_error.png) 透過sudo執行,即可看到perf top的分析 ![測試](https://i.imgur.com/RuJcbjW.png) 但可透過kernel.perf_event_paranoid 來決定你在沒有 root 權限下 (Normal User) 使用 perf 時,你可以取得哪些 event data。預設值是 1 ,可以輸入 ```shell $ cat /proc/sys/kernel/perf_event_paranoid ``` 來查看權限值。一共有四種權限值: ``2`` : 不允許任何量測。但部份用來查看或分析已存在的紀錄的指令仍可使用,如 perf ls、perf report、perf timechart、 perf trace。 ``1`` : 不允許 CPU events data。但可以使用 perf stat、perf record 並取得 Kernel profiling data。 ``0`` : 不允許 raw tracepoint access。但可以使用 perf stat、perf record 並取得 CPU events data。 ``-1``: 權限全開。 最後如果要檢測 cache miss event ,需要先取消 kernel pointer 的禁用。 ```shell $ sudo sh -c " echo 0 > /proc/sys/kernel/kptr_restrict" ``` ### 安裝 gnuplot 製圖 ```shell $ sudo apt-get install gnuplot ``` ### astyle + Git 自動程式碼縮排檢查 安裝astyle ```shell $ sudo apt-get install astyle ``` 調整作業風格要求如下 ```shell $ astyle --style=kr --indent=spaces=4 --indent-switches --suffix=none *.[ch] ``` 安裝 Git pre-commit hook 可在每次 git commit 時,檢查 C/C++ 原始程式碼的風格是否一致: ```shell $ ln -sf ../../scripts/pre-commit.hook .git/hooks/pre-commit ``` ## 實作 phonebook 案例 先針對未優化過版本進行 cache-miss, cache-references, instructions, cycles 執行100次的分析 ```shell perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./phonebook_orig ``` 輸出結果 ``` Performance counter stats for './phonebook_orig' (100 runs): 2,088,386 cache-misses # 95.686 % of all cache refs ( +- 0.17% ) 2,182,530 cache-references ( +- 0.17% ) 260,717,761 instructions # 1.44 insns per cycle ( +- 0.02% ) 180,625,105 cycles ( +- 0.12% ) 0.071431807 seconds time elapsed ( +- 0.90% ) ``` 其cach-misses 高達 95.686% 查看執行效率 ```shell $ ./phonebook_orig ``` 輸出 ``` size of entry : 136 bytes execution time of append() : 0.091677 sec execution time of findName() : 0.005124 sec # cache-misses 95.686 % ``` ### 如何優化? + **更改資料結構** 我們先看一下 ``phonebook_orgi.h`` 中的code ```c /* original version */ 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; ``` 再找電話簿資料中,一般來說我們是以找姓名或住址等一項進行索引,找到符合後才去看相關資料如:電話、住址、Email、姓名等。這裡的程是以找lastName為主,但結構中卻有一大堆其他的資料,好比資料庫搜尋姓名卻把所有欄位都列入搜尋。 因此我們先把一些資料放在另一結構中,等有需要的時候再讀取 ```c typedef struct __PHONE_BOOK_DATA { char firstName[16]; char email[16]; char phone[10]; char cell[10]; char addr1[16]; char addr2[16]; char state[2]; char zip[5]; } data; typedef struct __PHONE_BOOK_ENTRY { char lastName[MAX_LAST_NAME_SIZE]; struct data *data; struct __PHONE_BOOK_ENTRY *pNext; } entry; ``` 此處我要澄清一件事,就是我沒寫過C語言... 所以關於struct這邊的寫法是參照 [aweimeow](/s/ryCTJfIp) 的寫法,關於struct以前高中練演算法常見也知道是什麼,但畢竟對於C語言沒有開發經驗跟研讀,所以語法都是邊查邊問,如果有錯或不好之處請告知。 修改完畢後,``cache-misses``已經下降至 ``73.224%`` ``` Performance counter stats for './phonebook_opt' (100 runs): 376,674 cache-misses # 73.224 % of all cache refs ( +- 0.19% ) 514,414 cache-references ( +- 0.31% ) 243,955,824 instructions # 1.82 insns per cycle ( +- 0.02% ) 134,089,503 cycles ( +- 0.19% ) 0.052669400 seconds time elapsed ( +- 1.06% ) ``` ![gnuplot runtime](https://i.imgur.com/SFA2tcW.png) ``` size of entry : 32 bytes execution time of append() : 0.096749 sec execution time of findName() : 0.004163 sec ``` 可以發現size of entry 僅剩下 ``32 bytes`` 與原先的 ``136bytes`` 差了100多 bytes, ``append`` 與 ``findName``的時間也都有下降。 L1 cache: 32K / Phonebook size: 136 Bytes => 約30筆 entry L1 cache: 32K / Phonebook size: 32 Bytes => 約128筆 entry 能存放的筆數已經是原本約4倍,但35萬筆之料當中,其存放的量仍然相當的少,代表我們還有很大的進步空間。 + **使用Hash Table** - Hash方法 : BKDRHash - Table大小 : 42737 (質數,減少碰撞) 新增``main_hash.c`` 、 ``phonebook_hash.c`` 、 ``phonebook_hash.h`` 3個檔案 修改``calculate.c`` 、 ``Makefile`` 2個檔案 + **新增 Hash 結構 ``phonebook_hash.h``複製 ``phonebook_opt.h`` 來修改** 定義TABLE_SIZE 之後建立Hash Table大小用的到 ```C #define TABLE_SIZE 42737 ``` ```C typedef struct __PHONE_BOOK_HASH{ entry **list; } hashTable; ``` 此處使用pointer to pointer 使為了串接碰撞資料(這裡有點不懂,需再提問) **參考資料:**[TempoJiJi 學長共筆](https://hackmd.io/s/S1I5-CzT#實做hash降低findname時間) 修改函數 ``*findName`` 函數,第二個參數改用hashTable去做,而不再用``pHead``串列去找,大幅下降``findName``時間 ``append`` 函數,第二個參數也是用hashTable,但回傳型態修改為void,原本會再回傳``entry``,這裡直接用HashTable,所以不用回傳。 重新宣告函數,並新增 ``createHashTable`` 與 ``hash`` 函數 ```C entry *findName(char lastName[], hashTable *tb); void append(char lastName[], hashTable *tb); hashTable *createHashTable(int tablesize); unsigned int hash(char *str); ``` + **複製 ``phonebook_opt.c`` 為 ``phonebook_hash.c`` 進行修改** 新增 hash 函數,採用``BKDR Hash`` 法 **參考資料:**[各項Hash比對](https://www.byvoid.com/zht/blog/string-hash-compare) ```C unsigned int hash(char *str) { unsigned int hash_value = 0; unsigned int seed = 131; while(*str) hash_value = hash_value * seed + (*str++); return (hash_value % TABLE_SIZE); } ``` 有關為何要乘上一個seed係數 **參考資料:**[Programming Small](https://hackmd.io/EYYwTOCGDMCsC0AzSBGALPN0BsL6UQHYAOeABi0LGkmDIE5o0g==#hash-function) 新增 ``createHashTable`` 製作Hash Table ```C hashTable *createHashTable(int tableSize){ int i; hashTable *tb; tb = (hashTable*)malloc(sizeof(hashTable)); tb->list = (entry **)malloc(sizeof(entry*)*tableSize); for(i = 0;i < tableSize;i++){ tb->list[i] = NULL; } return tb; } ``` 重新修改 findName 方式,採用HashTable能下降速度及減少cache-miss ```C entry *findName(char lastName[], hashTable *tb){ entry *e; unsigned int index = hash(lastName); for(e = tb->list[index]; e != NULL; e = e->pNext){ if (strcasecmp(lastName, e->lastName) == 0) return e; } return NULL; } ``` 修改 append 方式,改用無回傳值直接併入Hash Table ```C void append(char lastName[], hashTable *tb) { /* allocate memory for the new entry and put lastName */ unsigned int index = hash(lastName); entry *e; e = (entry*)malloc(sizeof(entry)); strcpy(e->lastName, lastName); e->pNext = tb->list[index]; tb->list[index] = e; } ``` + **複製 ``main.c`` 為 ``main_hash.c``來做修改** 建立Hash Table,呼叫 ``createHashTable`` 函數 ```C hashTable *tb = createHashTable(TABLE_SIZE); ``` 修改 ``append`` 方式,原本為 ```C e = append(line, e); ``` 因為改用Hash Table不用回傳,修改為 ```C append(line, tb); ``` 找到 ```C assert(findName(input, e) && "Did you implement findName() in " IMPL "?"); assert(0 == strcmp(findName(input, e)->lastName, "zyxel")); ``` 將 ``findName`` 第二個參數改為Hash Table ```C assert(findName(input, tb) && "Did you implement findName() in " IMPL "?"); assert(0 == strcmp(findName(input, tb)->lastName, "zyxel")); ``` + **修改``Makefile``內相關設定** EXEC 新增 phonebook_hash 新增一個共同編譯的main_hash.c ```Makefile SRCS_common_hash = main_hash.c ``` ```Makefile phonebook_hash: $(SRCS_common_hash) phonebook_hash.c phonebook_hash.h $(CC) $(CFLAGS_common) $(CFLAGS_hash) \ -DIMPL="\"$@.h\"" -o $@ \ $(SRCS_common_hash) $@.c ``` catch-test 部份再加 hash的測試 ```Makefile perf stat --repeat 100 \ -e cache-misses,cache-references,instructions,cycles \ ./phonebook_hash ``` + **修改calculate.c 攸關產生數據統計圖** 其各種版本統計出的時間資料都會輸出到``output.txt``,而calculate就是負責計算這部份 添加 ```C fp = fopen("hash.txt", "r"); if (!fp) { fp = fopen("hash.txt", "r"); if (!fp) { printf("ERROR opening input file hash.txt\n"); exit(0); } } double hash_sum_a = 0.0, hash_sum_f = 0.0, hash_a, hash_f; for (i = 0; i < 100; i++) { if (feof(fp)) { printf("ERROR: You need 100 datum instead of %d\n", i); printf("run 'make run' longer to get enough information\n\n"); exit(0); } fscanf(fp, "%s %s %lf %lf\n", append, find, &hash_a, &hash_f); hash_sum_a += hash_a; hash_sum_f += hash_f; } ``` 修改輸出至 ``outout.txt`` 資料 原本僅有兩個 ``%lf`` 放 ``org i ``、``opt`` 的數據,增加一個放hash的數據 ```C fprintf(output, "append() %lf %lf %lf\n",orig_sum_a / 100.0, opt_sum_a / 100.0 , hash_sum_a / 100.0); fprintf(output, "findName() %lf %lf %lf", orig_sum_f / 100.0, opt_sum_f / 100.0 , hash_sum_f / 100.0); fclose(output); ``` 使用 Hash 後,cahce-miss已經下降至59%,findName時間也減少很多 ```C Performance counter stats for './phonebook_hash' (100 runs): 369,746 cache-misses # 59.233 % of all cache refs ( +- 0.30% ) 624,220 cache-references ( +- 0.36% ) 235,988,953 instructions # 1.64 insns per cycle ( +- 0.02% ) 143,618,916 cycles ( +- 0.24% ) 0.058912004 seconds time elapsed ( +- 1.36% ) ``` ![phonebook 效率比較](https://i.imgur.com/VP9omdp.png) ``cache-miss rate`` 已經降低,``findName`` 速度也下降很多,但 ``append`` 卻比原本還高許多,大概是opt版本的兩倍 append 時間變長,有些學長姐之前共筆有相關紀錄: + 碰撞資料太多 [c14006078 共筆](https://hackmd.io/s/BkN1JNQp#使用-hash-function) + Table Size 太大 [勃興學長 共筆](https://embedded2016.hackpad.com/2016q1-Homework-1-DdP67a0ke8Q#:h=方法二:利用hash-function) / [未知名學長 共筆](https://embedded2016.hackpad.com/ep/pad/static/9eSkToGwJvZ) =目前進度= 9/28 教師節快樂:smile: ## 參考資料 + [louielu 共筆](https://hackmd.io/CYJgZgHA7AnADAFgLQGMCmaCGSEGYCsyMARjPkgIwhS4gT4VwogJA===#) + [aweimeow 共筆](https://hackmd.io/EwFgZghgDAjA7AVgLQA44QJxJAEwMwypwxRJR4ZxgBGcsUEKQA==) + [成大wiki Linux效能分析工具Perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) + [Programming small](https://hackmd.io/EYYwTOCGDMCsC0AzSBGALPN0BsL6UQHYAOeABi0LGkmDIE5o0g==) + [TempoJiJi 學長共筆](https://hackmd.io/s/S1I5-CzT#實做hash降低findname時間) + [各項Hash比對](https://www.byvoid.com/zht/blog/string-hash-compare) + [勃興學長 共筆](https://embedded2016.hackpad.com/2016q1-Homework-1-DdP67a0ke8Q#:h=方法二:利用hash-function) + [c14006078 共筆](https://hackmd.io/s/BkN1JNQp#使用-hash-function) + [未知名學長 共筆](https://embedded2016.hackpad.com/ep/pad/static/9eSkToGwJvZ) 因為hackpad內無作者訊息,又找不知道怎麼連近來的..只好暫且稱呼未知名學長 Sorry...