# 2016q3 Homework1 (phonebook) contributed by <`carolc0708`> [作業說明A01: phonebook](https://hackmd.io/s/S1RVdgza) ## 開發環境 * Ubuntu 16.04.1 LTS * CPU: Intel(R) Core(TM) i5-4570 CPU @ 3.20GHz * Cache: `$ lscpu | grep cache` * L1d cache: 32K * L1i cache: 32K * L2 cache: 256K * L3 cache: 6144K >cache line issue ## Perf效能分析工具 * `phonebook_orig`的分析: ``` Performance counter stats for './phonebook_orig' (100 runs): 1,220,664 cache-misses # 85.971 % of all cache refs ( +- 0.39% ) 1,419,851 cache-references ( +- 0.26% ) 261,811,357 instructions # 1.21 insns per cycle ( +- 0.02% ) 216,828,386 cycles ( +- 0.42% ) 0.066546829 seconds time elapsed ( +- 0.56% ) ``` * `phonebook_orig`的執行時間: ``` size of entry : 136 bytes execution time of append() : 0.047410 sec execution time of findName() : 0.005756 sec ``` ## gnuplot 製圖 更改`\scripts\runtime.gp`檔中的製圖參數 ``` reset set ylabel 'time(sec)' set style fill solid set title 'perfomance comparison' set term png enhanced font 'Verdana,10' set output 'runtime.png' plot [:][:0.100]'output.txt' using 2:xtic(1) with histogram title 'original', \ '' using ($0-0.06):($2+0.001):2 with labels title ' ', \ '' using 3:xtic(1) with histogram title 'optimized' , \ '' using 4:xtic(1) with histogram title 'hash' , \ '' using ($0+0.3):($3+0.0015):3 with labels title ' ', \ '' using ($0+0.4):($4+0.0015):4 with labels title ' ' ``` 並在`calculate.c`當中增加將`hash.txt`讀入`output.txt`的部分 >[gnuplot introduction(中文版)](https://dl.dropboxusercontent.com/u/1091156/yong/text/gnuplot%20introduction.pdf) >[gnuplot manual](http://www.gnuplot.info/docs_5.0/gnuplot.pdf)[color=blue] ## 案例分析:Phonebook 在`phonebook_orig.h`中phonebook的資料結構定義如下: ```clike= typedef struct __PHONE_BOOK_ENTRY { char lastName[MAX_LAST_NAME_SIZE];//16 byte char firstName[16];//16 byte char email[16];//16 byte char phone[10];//10 byte char cell[10];//10 byte char addr1[16];//16 byte char addr2[16];//16 byte char city[16];//16 byte char state[2];//16 byte char zip[5];//5 byte struct __PHONE_BOOK_ENTRY *pNext;//8 byte } entry; ``` 可以看到一個entry總共占用了 136 byte,然而cache L1的大小為32 Kbit,若每次透過`findName()`函式進行比對,cache只能讀入32 * 1024 / 136 * 8 = 30.12,約 30 筆entry,在進行350000筆資料的比對時,會造成大量的cache miss。 ## Hint: 可能的效能改進方式 - [x] 改寫 struct __PHONE_BOOK_ENTRY 的成員,搬動到新的結構中 - [x] 使用 hash function 來加速查詢 - [ ] 既然 first name, last name, address 都是合法的英文 (可假設成立),使用[字串壓縮的演算法](http://stackoverflow.com/questions/1138345/an-efficient-compression-algorithm-for-short-text-strings),降低資料表示的成本 - [ ] 使用 binary search tree 改寫演算法 - [ ] 本例選用的 dataset (也就是輸入電話簿的姓名) 是否存在問題? 有代表性嗎?跟真實英文姓氏差異大嗎? 資料難道「數大就是美」嗎? 如果我們要建立符合真實電話簿情境的輸入,該怎麼作? ## 挑戰題 - [ ] 除了降低 `findName()` 的 cache miss 與執行成本,`append()` 也該想辦法縮減時間 * 建立 phone book 時,既然 lastName 是索引值,可以優先建立搜尋表格,其他資料可稍後再補上 * 用 PThread 建立 [manager/worker thread model](http://stackoverflow.com/questions/12282393/how-to-synchronize-manager-worker-pthreads-without-a-join) - [ ] 支援 [fuzzy search](http://www.informit.com/articles/article.aspx?p=1848528),允許搜尋 lastName 時,不用精準打出正確的寫法 * 比方說電話簿有一筆資料是 `McDonald`,但若使用者輸入 `MacDonald` 或 `McDonalds`,也一併檢索出來 * 延伸閱讀: [Levenshtein Distance (編輯距離)](https://charles620016.hackpad.com/ep/pad/static/Japi4qFyAzt) - [ ] 改善電話簿的效能分析,透過大量的重複執行,找出 95% 信賴區間,而且允許動態新增資料 (較符合現實) 和查詢 - [x] 以 [gnuplot](http://www.gnuplot.info/) 繪製效能分析圖表 ## METHOD1: 改變entry大小 在`phonebook_orig.c`中,可以發現每次進行`findName()`時,只用到`lastName`,卻須將整個entry 136 byte載入cache中。 ```clike= entry *findName(char lastName[], entry *pHead) { while (pHead != NULL) { if (strcasecmp(lastName, pHead->lastName) == 0) return pHead; pHead = pHead->pNext; } return NULL; } ``` 所以在`phonebook_opt.h`中重新設計phonebook的資料結構,將比對時不會用到的資料(即`lastName`、`*pNext`以外的資料)包裝為另一個struct, ```clike= typedef struct __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; ``` 並用一指標`*detailInfo`指向它。 ```clike= typedef struct __PHONE_BOOK_ENTRY { char lastName[MAX_LAST_NAME_SIZE]; detail *detailInfo; struct __PHONE_BOOK_ENTRY *pNext; } entry; ``` 如此一來,原本的`__PHONE_BOOK_ENTRY`只佔用了16(`lastName`) + 8(`*pNext`) + 8(`*detailInfo`) = 32 byte,cache每次能讀取32 * 1024 / 32 * 8 = 128 筆entry,可大幅減少cache miss。 >[C/C++ typedef , struct , typedef struct 差別](http://vincecc.blogspot.tw/2013/10/cc-typedef-struct-typedef-struct.html)[color=blue] >- [ ]TODO: 嘗試這部分實作並測試效能是否改善 >[課程資料](https://hackmd.io/s/S1rbwmZ6)當中提到:" 因為若在 append 過程中 malloc() 空間給 *detail,會增加很多 cache miss,嚴重影響到效能表現,經過實測,總體效能甚至比原始版本還差一點。目前想法是將 *detail 的 assign (當有需要時)交給另外一個 function 處理,畢竟我們一開始只有 last name 的資訊。 " --- * 改善後,執行時間如下: ``` size of entry : 32 bytes execution time of append() : 0.028434 sec execution time of findName() : 0.001888 sec ``` 改善後,cache miss由85%降到45%左右。 ``` Performance counter stats for './phonebook_opt' (100 runs): 197,061 cache-misses # 45.603 % of all cache refs ( +- 1.47% ) 432,126 cache-references ( +- 0.72% ) 244,209,456 instructions # 1.85 insns per cycle ( +- 0.02% ) 131,874,631 cycles ( +- 0.41% ) 0.041165375 seconds time elapsed ( +- 0 .93% ) ``` `findName()`執行時間上有減少。 * 和未優化版本比較: ![](https://i.imgur.com/0KMntkl.jpg) ## METHOD2: 使用 hash function >[複習 hash function](https://www.csie.ntu.edu.tw/~hsinmu/courses/_media/dsa_12spring/hashing.pdf) >[其他字串可用的Hash function](https://www.byvoid.com/zht/blog/string-hash-compare)[color=blue] >這邊在實作的時候程式碼亂成一團,把它用條件編譯的方式好好整理一下,參考[這裡](http://blog.sina.com.cn/s/blog_4b4b54da0100r2l6.html)。 * djb2 參考[課程資料:Programming Small](https://hackmd.io/s/S1rbwmZ6)中對於實作hash function的探討,而hash function的選擇參考 [hash function for string](http://stackoverflow.com/questions/7666509/hash-function-for-string),實作了 [djb2](http://www.cse.yorku.ca/~oz/hash.html)。 djb2 hash function給定初始hash值為5381,將key編為hash*33,再加上字串的內容。考慮phonebook資料結構的設計,輸入字串可以是`lastName`(16 byte)或結合所有的字串(127 byte),在此由於`findName()`只有用到`lastName`的比較,只輸入`lastName`去編key。 >不知道兩種效果是否差很多,可以試試看[name=Carol Chen] ```C= unsigned int DJB2_hash(char lastName[], int hash_table_size) { unsigned int hash = 5381; int c; int i = 0; while (c = lastName[i++]) hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ return hash %= hash_table_size; } ``` 在這邊實作`append()`時,有另外定義全域變數`entry* hash_table_cur[HASH_NUM_SIZE]`,用來紀錄hash table發生collision時chaining的linked list已經存到第幾個。這樣一來每次在加入新的資料的時候,就不用整條linked list從頭走一次,可以減去一些執行時間。 hash table size = 256 ``` size of entry : 32 bytes execution time of append() : 0.038605 sec execution time of findName() : 0.000015 sec ``` hash table size = 1000 ``` size of entry : 32 bytes execution time of append() : 0.038603 sec execution time of findName() : 0.000003 sec ``` hash table size = 2000 ``` size of entry : 32 bytes execution time of append() : 0.039207 sec execution time of findName() : 0.000001 sec ``` 可以發現`append()`的時間與未優化前沒有差太多,而`findName()`的執行時間則是隨著hash table的大小增加而逐漸縮減。 ``` Performance counter stats for './phonebook_hash' (100 runs): 580,489 cache-misses # 50.242 % of all cache refs ( +- 0.29% ) 1,155,387 cache-references ( +- 0.06% ) 293,285,085 instructions # 1.28 insns per cycle ( +- 0.02% ) 229,464,841 cycles ( +- 0.34% ) 0.066811857 seconds time elapsed ( +- 0.37% ) ``` cache miss由未優化前的85%降至50%左右。比縮小entry的優化版本(45%)稍微高一點。 --- * BKDR hash ```C= unsigned int BKDR_hash(char lastName[], int hash_table_size) { unsigned int seed = 131; unsigned int hash = 0; int i = 0; while(lastName[i] != '\0') { hash = hash * seed + lastName[i]; i++; } return hash %= hash_table_size; } ``` hash table size = 256 ``` size of entry : 32 bytes execution time of append() : 0.040698 sec execution time of findName() : 0.000029 sec ``` hash table size = 1000 ``` size of entry : 32 bytes execution time of append() : 0.041464 sec execution time of findName() : 0.000003 sec ``` hash table size = 2000 ``` size of entry : 32 bytes execution time of append() : 0.041498 sec execution time of findName() : 0.000001 sec ``` 可以發現`append()`的時間與未優化前沒有差太多,但約比djb2的時間長一些。而`findName()`的執行時間則是隨著hash table的大小增加而逐漸縮減。 ``` Performance counter stats for './phonebook_hash' (100 runs): 593,343 cache-misses # 50.444 % of all cache refs ( +- 0.25% ) 1,176,231 cache-references ( +- 0.07% ) 292,144,527 instructions # 1.22 insns per cycle ( +- 0.01% ) 239,491,289 cycles ( +- 0.30% ) 0.069722688 seconds time elapsed ( +- 0.33% ) ``` cache miss從未優化前的85%降到50%左右,和djb2的版本差不多。 * 和為優化版與縮減entry大小版本比較如下: ![](https://i.imgur.com/H5iIJRp.png) ## METHOD3: SMAZ字串壓縮 * [smaz字串壓縮演算法](https://github.com/antirez/smaz)