# 2017q1 Homework1 (phonebook) contributed by < `henry0929016816` > ### Reviewed by `illusion030` * 可以嘗試使用不同的 hash function 來實作並比較 * 沒有回答作業要求中的問題 ### Reviewed by `petermouse` * 需考量不同 hash table 大小對搜尋造成的影響 * 可以考慮使用樹狀結構 ( binary search tree , red–black tree 等) 來實作,並與其他方法一同分析比較 * Git commit [b9e5c2d](https://github.com/henry0929016816/phonebook/commit/b9e5c2dad108e0956d91d4121fe14653a15172e9) 標題不需指出檔名,標題首字需大寫 * 要考慮到 `malloc()` 失敗的情形 ### Reviewed by `SeanCCC` * 為何make cache-test需樣sudo權限? ## 安裝 ubuntu 16.04 [acer windows8.1 安裝雙系統](https://hackmd.io/BwNgDAhgnJILQCYDMATMcAsBGBAjOuSAZgKxxLZ7BYoxIDGQA===#) ## 開發環境 ``` os: ubuntu 16.04 LTS Architecture: x86_64 CPU 作業模式: 32-bit, 64-bit Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 每核心執行緒數:2 每通訊端核心數:2 Socket(s): 1 NUMA 節點: 1 供應商識別號: GenuineIntel CPU 家族: 6 型號: 60 Model name: Intel(R) Core(TM) i5-4210M CPU @ 2.60GHz 製程: 3 CPU MHz: 799.963 CPU max MHz: 3200.0000 CPU min MHz: 800.0000 BogoMIPS: 5188.12 虛擬: VT-x L1d 快取: 32K L1i 快取: 32K L2 快取: 256K L3 快取: 3072K NUMA node0 CPU(s): 0-3 ``` 利用 ==lscpu== 指令取得電腦內部的資訊 >請列出硬體相關資訊,如:cache, memory >[color=red][name=課程助教] >抱歉給的資訊太少,已補上相關資訊[name=henry0929016816] ## 預備知識 - [ ]perf 的使用方式 - [ ]gnuplot 畫圖 - [ ]cache ## 效能分析-優化前 * 執行 phonebook_orig ``` ./phonebook_orig ``` 得到 findName() 跟 append() 所需的執行時間 ``` size of entry : 136 bytes execution time of append() : 0.040664 sec execution time of findName() : 0.005231 sec ``` * 找出 cache-misses 率 ``` sudo make cache-test ``` 發現 cache-misses 率高達==84.546%== ``` Performance counter stats for './phonebook_orig' (100 runs): 970,646 cache-misses # 84.546 % of all cache refs ( +- 0.34% ) 1,148,065 cache-references ( +- 0.27% ) 262,101,748 instructions # 1.44 insn per cycle ( +- 0.02% ) 181,954,743 cycles ( +- 0.18% ) 0.063477558 seconds time elapsed ( +- 0.65% ) ``` ## 優化方式 ### 方法一: 改變 struct 結構 * #### 結構太大造成的瓶頸 認真檢視 phonebook_orig.c 的程式碼 ```c= entry *findName(char lastName[], entry *pHead) { while (pHead != NULL) { if (strcasecmp(lastName, pHead->lastName) == 0) return pHead; pHead = pHead->pNext; } return NULL; } entry *append(char lastName[], entry *e) { /* allocate memory for the new entry and put lastName */ e->pNext = (entry *) malloc(sizeof(entry)); e = e->pNext; strcpy(e->lastName, lastName); e->pNext = NULL; return e; } ``` 將會發現 append() 跟 findName() 函式使用到的資料只有 __PHONE_BOOK_ENTRY 裡的 lastName,其他的資訊幾乎都沒有用到,卻佔了一大部分的空間。我的電腦 leve 1 cache 有32 kbyte,而 __PHONE_BOOK_ENTRY 結構的大小為 136 bytes,==( 32 * 1024 ) / ( 136 ) = 240.941==。只考慮當下只有這支程式在跑的情況下,將所有的 __PHONE_BOOK_ENTRY 結構都丟進 findName() 裡去執行 ,我的level 1 cache 也只能儲存 240.941 個 entry,查找的資料一但過多,勢必會發生頻繁的 cache miss。 * #### 突破瓶頸 修改 phonebook_opt.h ```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_detail; //只存放lastName,讓struct的大小降低 typedef struct __LAST_NAME_ENTRY{ char lastName[MAX_LAST_NAME_SIZE]; entry_detail *detail; struct __LAST_NAME_ENTRY *pNext; }entry; ``` 新增一個資料結構 __LAST_NAME_ENTRY 用來儲存 lastname ,至於其他的資訊,等到有需要再 ==malloc== entry_detail 去儲存。使得需要儲存的結構降為 32 byte , 照著剛才的算法 ==( 32 * 1024 ) / ( 32 ) = 1024==,按照同樣的假設條件,只有這支程式再跑,我們可以發現比剛才還要多的 entry,期許能降低 cache-miss。 * #### 實驗結果 * 利用==sudo make cache-test== 觀看兩支程式的cache miss * phonebook_orig ``` Performance counter stats for './phonebook_orig' (100 runs): 628,561 cache-misses # 74.442 % of all cache refs ( +- 2.65% ) 844,360 cache-references ( +- 1.67% ) 262,444,912 instructions # 1.21 insn per cycle ( +- 0.02% ) 216,675,317 cycles ( +- 1.36% ) 0.135132961 seconds time elapsed ( +- 1.88% ) ``` * phonebook_opt ``` Performance counter stats for './phonebook_opt' (100 runs): 112,042 cache-misses # 47.039 % of all cache refs ( +- 0.61% ) 238,189 cache-references ( +- 2.55% ) 244,613,756 instructions # 1.39 insn per cycle ( +- 0.02% ) 175,806,100 cycles ( +- 1.67% ) 0.107832504 seconds time elapsed ( +- 2.03% ) ``` 發現 cache-miss 的確是有降低的 * #### 附圖說明 ![](https://i.imgur.com/fl66SIJ.png) cache-miss 率的降低,影響了執行時間,可推測 cache-miss 較低的程式,執行速度比較快。 * 由於電腦不是高效能模式,執行速度有點慢,透過指令調整為高效能模式 * ==cpupower frequency-set --governor performance== * 得到新的執行時間圖 ![](https://i.imgur.com/ILTAbeZ.png) ### 方法二: 建立 hash function * #### 問題:建立的資料表,無法快速查到資料 以下為原本的 append 函式 ```c= entry *append(char lastName[], entry *e) { /* allocate memory for the new entry and put lastName */ e->pNext = (entry *) malloc(sizeof(entry)); e = e->pNext; strcpy(e->lastName, lastName); e->pNext = NULL; return e; } ``` 建立的資料表為 singly-linked list,因此每次搜尋資料都必須從 linked list 的頭開始往下尋找,如果想要查的資料不幸位在 linked list 末端,那將會浪費許多時間在走訪無關的節點。 * #### 想法 既然建立的資料表,無法快速查到想要的資訊,那我們就重新建表,使得資料以特定的原則==分門別類==,查詢資料時,只要知道資料在哪個類別,就能以較快的方式尋得資料。 * #### 分類的方式 : hash function 參考[hash table 之 bkdrhash算法解析及擴展](http://blog.csdn.net/wanglx_/article/details/40400693),我們將字串依照數學運算,求得值,當作是資料的==類別==,兩個字串如果擁有相同的值,則用 linked list 串接在一起,方便搜尋資料時,只要求得字串的值,就能知道該往哪個類別搜尋資料,不用在從頭到尾辛辛苦苦找資料了。 * #### 實作 網路上有很多 hash function 的實作方式,我選擇使用 bkdrhash 。以下為實作函式。 `phonebook_hash.c` ```c= unsigned int bkdr_hash(char* key) { char* str = key; unsigned int seed=31; unsigned int hash=0; while(*str!='\0') { hash=hash * seed +(*str++); } /*PRIME_NUMBER=4793*/ hash%=PRIME_NUMBER; return hash; } ``` 假設將字串 abc 丟進此函式,參照 ascii code a 為 97,得到這個資料我們可以計算,==(97~a~ * 31^2^ + 98~b~ * 31^2^ + 99~c~ * 31^0^) % 4793 = 20.10==,得到的值則當作分類用的 index。我們可以期許透過這個方式,加速 findName() 的執行時間 * #### 實際結果 - cache miss降低 透過hash function 我們得到了比第一個優化方式還要少的 cache miss。以下為兩者的 cache miss 資料 改變 struct 結構 ``` Performance counter stats for './phonebook_opt' (100 runs): 127,322 cache-misses # 45.177 % of all cache refs ( +- 0.90% ) 281,831 cache-references ( +- 0.89% ) 244,515,172 instructions # 1.95 insn per cycle ( +- 0.02% ) 125,228,080 cycles ( +- 0.30% ) 0.043359649 seconds time elapsed ( +- 1.24% ) ``` hash function ``` Performance counter stats for './phonebook_hash' (100 runs): 187,364 cache-misses # 36.710 % of all cache refs ( +- 1.23% ) 510,394 cache-references ( +- 0.56% ) 256,299,139 instructions # 1.53 insn per cycle ( +- 0.02% ) 167,345,086 cycles ( +- 0.29% ) 0.057692378 seconds time elapsed ( +- 1.06% ) ``` * #### 附圖說明 ![](https://i.imgur.com/APU9xFV.png) append() 時間的增加,是因為建構資料時多了計算 hash function 的時間,然而這犧牲的時間卻換取了 findName() 極高的效能提升。 ## 參考資料 * [ktvexe 的 hackmd](https://hackmd.io/MYZgnCwGbALAtGArCADPWAjAhgRnpgOwAmY8UAbMJqsLgBxb2ZA=#) * [jkrvivian 的 hackmd](https://hackmd.io/s/SyOQgOQa#) * [perf 原理和實務](https://hackmd.io/s/B11109rdg#) * [Programming Small](https://hackmd.io/KYYwzAjAHATAJgVgLQEMQDMxICwIcJATgHZhikowE4YA2AI2xnqgSA==#) * [cache 原理和實驗](https://hackmd.io/s/S14L26-_l#) * [Linux 系統下如何查看 CPU 型號、核心數量、頻率和溫度](https://magiclen.org/linux-view-cpu/) * [高效能電腦的程序優化](http://linux.vbird.org/linux_enterprise/cputune.php) * [釋放與清除 cache memory](http://gwokae.mewggle.com/wordpress/2010/06/%E9%87%8B%E6%94%BE%E8%88%87%E6%B8%85%E9%99%A4-linux%E8%A8%98%E6%86%B6%E9%AB%94%E4%B8%AD%E7%9A%84cache-memory/) * [hash table 之 bkdrhash算法解析及擴展 ](http://blog.csdn.net/wanglx_/article/details/40400693) * [gnuplot 設定長條圖並標記數值](http://gmd20.blog.163.com/blog/static/1684392320146163444900/)