# 2017q1 Homework1 (phonebook) contributed by < `etc276` > ###### tags: `etc276` ## Reviewed by `yanang` * 在 append() 中,當發生相同 hash value ,以 linked list 串連心增加的 data ,若考慮將資料重最前端插入,則可以減少尋找尾端的時間。 ```c= entry *pHead = (entry *) malloc (sizeof(entry)); pHead->pNext = e[hash]; e[hash] = pHead; strcpy(pHead->lastName,lastName); ``` * 不同的 hash function 和不同的 table size 都會影響實驗結果,可以參考 [how to choose size of hash table](http://stackoverflow.com/questions/22741966/how-to-choose-size-of-hash-table) * 針對你的實驗結果,就我的實驗結果和各位同學的共筆展示,使用 hash table 來搜尋資料應是非常迅速。 * time of finName() = 0.002348 是完全不符合預期的,是否在繪圖時使用了錯誤的資料來源? ![](https://i.imgur.com/s2aEMwG.png) ## 開發環境 - Ubuntu 16.10 (64bit) ``` Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 Thread(s) per core: 2 Core(s) per socket: 2 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 61 Model name: Intel(R) Core(TM) i7-5500U CPU @ 2.40GHz Stepping: 4 CPU MHz: 2400.878 CPU max MHz: 3000.0000 CPU min MHz: 500.0000 BogoMIPS: 4788.90 Virtualization: VT-x L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 4096K NUMA node0 CPU(s): 0-3 ``` ## 相關連結 - [2017 年春季作業說明](https://hackmd.io/s/Bk-1zqIte#) - [2017q1 Homework1 (作業區)](https://hackmd.io/MYJjEYGZITgWgOwCMAmw4BYYwAxyQQKxwBmwGOkAHMDeEgGxA===?view) - [B01: phonebook作業要求](https://hackmd.io/s/rJYD4UPKe#b01-phonebook) - [phonebook Github連結 ( etc276 ) ](https://github.com/????????) - [作業解說 video](https://www.youtube.com/watch?v=ZICRLKf_bVw) - [課程進度與開放資源 Wiki](http://wiki.csie.ncku.edu.tw/sysprog/schedule) ## 開發紀錄 ### Original ```c== 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; ``` * 自 GitHub 取得作業程式碼 (先要 fork 到自己帳號在 clone ) ``` $ git clone https://github.com/etc276 $ cd phonebook ``` * 執行程式,並用 perf 檢查效能,可以發現 cache miss 高達 81.73% ```clike Performance counter stats for './phonebook_orig' (100 runs): 3,430,005 cache-misses # 81.729 % of all cache refs ( +- 0.03% ) 4,196,826 cache-references ( +- 0.19% ) 261,712,845 instructions # 1.34 insn per cycle ( +- 0.02% ) 194,998,572 cycles ( +- 0.26% ) 0.066929775 seconds time elapsed ( +- 0.31% ) ``` * 透過 gnuplot 建立執行時間的圖表 ![](https://i.imgur.com/pDoy32p.png) ### Optimization 1 (By struct) ```c== typedef struct __PHONE_BOOK_ENTRY { char lastName[MAX_LAST_NAME_SIZE]; struct __PHONE_BOOK_ENTRY *pNext; struct __PHONE_BOOK_DETAIL *pDetail; } entry; typedef struct __PHONE_BOOK_DETAIL { 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]; } detail; ``` 目前我對 cache miss 對效能影響的理解為,由於記憶體使用上並不連續,造成 latency 變長。所以先從 struct 下手,讓會用到的資料盡量連續,而方法為將 lastname 以外的資料存在別的地方,減少單一 entry 的大小。 如下方所示,cache miss下降到 69.64%,執行時間也稍有下降。 ```c Performance counter stats for './phonebook_opt' (100 runs): 1,210,125 cache-misses # 69.643 % of all cache refs ( +- 0.19% ) 1,737,624 cache-references ( +- 0.58% ) 243,060,991 instructions # 1.75 insn per cycle ( +- 0.02% ) 139,089,184 cycles ( +- 0.78% ) 0.048515702 seconds time elapsed ( +- 0.93% ) ``` ![](https://i.imgur.com/EsxPXV5.png) ### Optimization 2 (By hash) ```c== entry *findName(char lastName[], entry *e[]) { int hash = BKDRHash(lastName)%TABLE_SIZE; entry *tmp = e[hash]; while (tmp) { if (!strcasecmp(lastName, tmp->lastName)) return tmp; tmp = tmp->pNext; } return NULL; } void append(char lastName[], entry *e[]) { int hash = BKDRHash(lastName)%TABLE_SIZE; entry *tmp = e[hash]; while (tmp->pNext) { tmp = tmp->pNext; } entry *node = (entry *) malloc(sizeof(entry)); tmp->pNext = node; strcpy(node->lastName, lastName); } ``` 減少 cache miss 之後,下一步想辦法從演算法減少時間。 original 的版本就是從頭到尾跑一遍查詢,這在資料量龐大時效能會有明顯影響,所以就想用 O(1) 的 hash 去實現。而在參考完 [Section 8 哈希表(Hash Table)](http://pisces.ck.tp.edu.tw/~peng/index.php?action=showfile&file=f3443505c4cd3d8598eee689618327ef8af0f8af7),決定使用 BKDR 加上"拉鍊法"處理 hash 和碰撞的問題。 而實際上 coding 才想到自己其實不知道要怎麼選 table size,參考完 [为什么hash table的大小最好取一个不接近2^p的素数](http://thuhak.blog.51cto.com/2891595/1352903) 還是不知道要選用什麼數量級的質數,再繼續看其他人的開發紀錄,發現有人[實驗](https://hackmd.io/s/Hyr9PhFYx)過,最後決定選用 20k 附近且有在[這篇](https://hackmd.io/s/HkO2ZpIYe)中出現的 21911 作為 table size. cache miss 下降到 36.93% ```clike Performance counter stats for './phonebook_opt' (100 runs): 15,649 cache-misses # 36.930 % of all cache refs ( +- 1.25% ) 42,374 cache-references ( +- 0.61% ) 691,447 instructions # 0.59 insn per cycle ( +- 0.20% ) 1,169,857 cycles ( +- 1.09% ) 0.121802201 seconds time elapsed ( +- 1.69% ) ``` ![](https://i.imgur.com/s2aEMwG.png) :::danger 所以你還是沒搞懂,快去「找教科書」(不要茫茫網海亂找) 來讀,我等你的報告 --jserv ::: ### 問題思考 * 有代表性嗎?跟真實英文姓氏差異大嗎? * 我們先查看各字母使用的頻率,可以發現差異相當大 ![](https://i.imgur.com/hqUqfCP.png) 圖片來源 : https://zh.wikipedia.org/wiki/%E5%AD%97%E6%AF%8D%E9%A2%91%E7%8E%87 * 然後再查看英文單詞中,首字母的頻率 * 再查看 work.txt 以及 [illusion030 的共筆]("https://hackmd.io/s/S1mqcITtl#")可得知此 dataset 的 真實姓名不多 >> words.txt 中有大量沒有意義的單字(ex.aaaaa),寫一個比較的程式發現,words.txt 裡總共 349900 筆資料,但是只有 7426 筆資料有在 all-names.txt 裡出現,所以 words.txt 對 phonebook 來說代表性很低。 * 而且真實情況中, lastName 一定會發生重複的情況 ``` 首字母 單詞頻率 t 16.671% a 11.602% s 7.755% h 7.232% w 6.753% i 6.286% o 6.264% b 4.702% m 4.374% f 3.779% c 3.511% l 2.705% d 2.670% p 2.545% n 2.365% e 2.007% g 1.950% r 1.653% y 1.620% u 1.487% v 0.649% j 0.597% k 0.590% q 0.173% x 0.037% z 0.034% ``` * 資料難道「數大就是美」嗎? 寫程式是要給其他人使用的,所以數據的選擇也要盡量真實。以此 dataset 為例,就算增加 work.txt 裡的 "偽" 數據,也無法"美"。例如 Michael 在英文名字中出現的頻率遠比 Zuzzolo 高,如果我們單純地用 lastName 去做 hash 處理,會發現前者的 link list 過長,而後者就只有一個 entry 的情況出現。 * 如果我們要建立符合真實電話簿情境的輸入,該怎麼作? 要先確定這本電話簿的使用情況,如年代、地區等等,然後去戶政事務所或是相關網站找出一部份的姓名,並試圖增加重複性或抽樣等方法當作母群體,去實際執行並抓出 95% 的信賴區間,以符合真實情況。 ## 相關知識 ### cache 目前電腦的架構為處理器(CPU)+記憶體(RAM),但由於近 40 年來兩者的速度差異越來越大,所以就利用 cache 去減少效能上的差異。 cache 是 CPU 外層的記憶體,其容量相當小,藉由將 RAM 的一部分資料先放在上面,減少兩者溝通的時間。而因為 cache 的容量遠小於 RAM,所以如果 CPU 要用到的下一個資料不在 cache 上,就會發生 **cahe miss**,反之則為 **cache hit**。 cache 的原理為:「實務上會使用到的記憶體常為連續性的」。所以才有辦法藉由這種結構去彌補兩者之間的速度差(可以先把資料放上 cache ),因此第一個優化方法就是先盡量將資料連續化。 ###### 參考資料 ### perf perf 是一種效能分析工具。他會幫你計算 cache miss, instructions 等等,去分析程式的效能瓶頸。相關資料請參閱 [perf 原理和實務 ](https://hackmd.io/s/B11109rdg) ### gunplot * gunplot 是一種繪圖工具,可以批次提供圖表。 * gnuplot 啟動後,需要做一些必要設定,例如設定圖片名稱,XY 軸的資訊等等: ```c > set title 'my plot' @ 設定圖片名稱 > set xlabel 'x axis' @ 設定XY軸座標名稱 > set ylabel 'y axis' > set terminal png @ 設定輸出格式為 .png > set output 'output_plot.png' @ 設定輸出檔名 > plot [1:10][0:1] sin(x) @ 畫出 sin(x) 函式 @ x軸座標範圍 1-10 @ y軸座標範圍 0-1 ``` * 繪圖 $ gnuplot [Script] * 查看圖片 $ eog [jpg/png...] * 更多使用方法請點 [這裡](https://hackmd.io/s/Skwp-alOg) ### astyle 使用相同的 coding style 有助於多人開發(commit style也是),而這次的規範為: ``` $ astyle --style=kr --indent=spaces=4 --indent-switches --suffix=none *.[ch] ``` - ```--style=kr``` 用 K&R 的風格進行編程 - ```--indent=spaces=4``` 四個空格 - ```--indent-switches``` 關於 Case 的縮排 > Indent 'switch' blocks, so that the inner 'case XXX:' headers are indented in relation to the switch block. - ```--suffix=none``` 不保存原始文件 - ``` *.[ch]``` 所有的.c或.h檔 ## 進度與小結 :::danger 「預計」就不要寫出來,Show me the code! HackMD 會幫你記錄每筆紀錄的變更和具體時間,把時間省下來發現並解決問題 --jserv ::: <s> * 2/27 以前在安裝 ubuntu 和閱讀相關資烙 * 2/28 開始寫開發紀綠(一些自己的認知和 original 的測試結果) * 3/01 第一次 commit (Opt by struct) * 3/02 (預計) 完成 Opt by hash </s> ## 心得 在寫 hash 的時候,產生一堆 bug ,時間詭異,圖也沒有出現。後來才知道是 main.c 錯太多東西,導致像 orig.txt 等文件並沒有產生成功,索性刪掉重新 clone 。之後先仔細閱讀 Makefile 裡各 source code 的相依性,才知道要先從 opt.h 修改起,像是我修改 function 的 prototye,這時 main.c 就需要去利用 #ifdef OPT 做修改,最後才是 opt.c 的小 bug .