# 2017q1 Homework1 (phonebook) contributed by <`petermouse`> ### Reviewed by <`vtim9907`> - 用 BST 優化是很好的嘗試,不過可以嘗試針對 cache line 的特性來實做 BST,可以先參考 [ cache 原理和實驗 ](https://hackmd.io/s/S14L26-_l)了解 cache line 特性。 - hash function 的部份,嘗試測試搜尋多筆資料很好,但是就如你說的,測出來的時間包含到了其他東西,所以不夠準確,可以考慮修改更精確。 - 可以嘗試比較多種 hash function 之間的效能差異。 - 可以嘗試實做 新增/刪除 資料的功能,以貼近現實需求,更符合實際情況。 - 尚未回答本作業要求回答的問題。 ### Review by <`henry0929016816`> * 是擅長發現問題的朋友呢!發現到 pHead 沒有初始化 lastName 有可能會出錯 * 如果 perf top 無法觀看到 append 跟 findName 的差異 可以嘗試使用 [gprof](https://sourceware.org/binutils/docs/gprof/) 觀察函式執行效能的差異 * 使用 shuf 產生了隨機的值去檢測 bst 跟 djb2 findName 是個不錯的方式,但是這只能看到執行時間的差異,建議可以將建表資料的分佈方式用點陣圖表示,讓大家知道 bst 跟 djb2 資料分佈的差異導致搜尋速度的落差。 ### Review by <`tina0405`> * 在hashtable上可以多嘗試各種不同的方法。 * binarytree上我覺得能直接看出findname()效能的進步,對應時間複雜度,如果能解釋數據更好。 ## 開發環境 * OS: Ubuntu 16.04 LTS * Memory: 8 GB * CPU: * Name: * Intel(R) Core(TM) i5-4200H CPU @ 2.80GHz * Cache: * L1d Cache: 32K * L1i Cache: 32K * L2 Cache: 256K * L3 Cache: 3072K L1 Cache 資訊 ( `$ sudo dmidecode` 查詢硬體資訊 ) ``` Handle 0x000A, DMI type 7, 19 bytes Cache Information Socket Designation: CPU Internal L1 Configuration: Enabled, Not Socketed, Level 1 Operational Mode: Write Back Location: Internal Installed Size: 128 kB Maximum Size: 128 kB Supported SRAM Types: Unknown Installed SRAM Type: Unknown Speed: Unknown Error Correction Type: Single-bit ECC System Type: Other Associativity: 8-way Set-associative ``` 128 kB 是因為 2 ( cores ) * 2 ( D-Cache + I-Cache ) * 32 kB 得來 ## phonebook ### 程式碼觀察 仔細觀察 `main.c`,`pHead` 是 linked list 的 header, `pHead->pNext` 才會是第一筆資料的位置,不過在程式碼中 ```clike= /* in main.c */ e = pHead; /* some statements */ findName(input, e); ``` ```clike= /* in phonebook_orig.c */ entry *findName(char lastName[], entry *pHead) { while (pHead != NULL) { if (strcasecmp(lastName, pHead->lastName) == 0) return pHead; pHead = pHead->pNext; } return NULL; } ``` 發現到 `main.c` 執行到 `findName()` 這個函式時,竟然先比較`pHead->lastName` 才接著比較 `pHead->pNext->lastName`,可是 `pHead` 並沒有初始化 `lastName` 這個 member,不過也不會造成錯誤,實際將 `pHead->lastName` printf 出來,會發現沒有輸出任何文字。 接著我在[這篇文章](http://stackoverflow.com/questions/8029584/why-does-malloc-initialize-the-values-to-0-in-gcc)看到了可能的解釋: > When you call malloc(), one of two things will happen: > 1. It recycles memory that was previous allocated and freed from the same process. > 2. It requests new page(s) from the operating system. 特別是第二點 > Memory coming from the OS will be zeroed for security reasons > When the OS gives you memory, it could have been freed from a different process. So that memory could contain sensitive information such as a password. So to prevent you reading such data, the OS will zero it before it gives it to you 也就是說,在執行 `malloc()` 且由作業系統給一個新的分頁時,作業系統為了考量到安全問題,會先把分頁給清空,所以當我們存取 `pHead->lastName` 時,馬上就遇到了`\0`,所以實際上 `pHead->lastName` 就是個空字串 這篇文章也另外提到了: > I note that the C standard says nothing about this. This is strictly an OS behavior. So this zeroing may or may not be present on systems where security is not a concern. 所以是剛好作業系統幫助了我們初始化,可能換了作業系統就不適用,不該仰賴作業系統幫助我們做初始化0的動作,或是改用 `calloc()` ### 原始版本 先 `make cache-test` 測試看看 cache miss rate ``` Performance counter stats for './phonebook_orig' (100 runs): 1,026,076 cache-misses # 85.825 % of all cache refs ( +- 0.30% ) 1,195,548 cache-references ( +- 0.18% ) 262,104,722 instructions # 1.39 insn per cycle ( +- 0.02% ) 188,310,587 cycles ( +- 0.27% ) 0.056928599 seconds time elapsed ( +- 0.54% ) ``` 執行 `calculate` 並查看 `output.txt` 平均 100 次的執行時間 (還沒有 opt 的版本 所以 opt 時間為 orig 的時間) ``` append() 0.037610 0.037610 findName() 0.005019 0.005019 ``` 參考[示範共筆](https://hackmd.io/s/BJRMImUPg#),也試著用 `perf record` 紀錄熱點,結果得不到有用的資訊,推測是程式執行太快,樣本數不夠,或者是我搞錯什麼了 試著調整 `/proc/sys/kernel/perf_event_max_sample_rate` 內的數值,結果也失敗了 ( Fsync 命令執行失敗 ) ### 改良版本(1): 改變資料結構 原始版本 cache miss 極高,是因為在當 cache miss 發生時,移入 block 的資料量極少( 1筆,struct size (136 Byte) 遠大於 block size (64 Byte),需 3 個 block 存放 struct ),cache 中存放整個 structure 但大部份的 member 未使用 因此將其他 member 以另一個 structure 表示, `entry` 僅儲存該 struct pointer,必要時再分配記憶體儲存其他資訊 ```clike= typedef struct __PHONE_BOOK_DATA { 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]; } data; typedef struct __PHONE_BOOK_ENTRY { char lastName[MAX_LAST_NAME_SIZE]; struct __PHONE_BOOK_ENTRY *pNext; data *info; } entry; ``` 改進過後的 cache miss rate : 從原本的 85.825 % 降為 ==43.462 %== ``` Performance counter stats for './phonebook_opt' (100 runs): 122,800 cache-misses # 43.462 % of all cache refs ( +- 0.49% ) 282,548 cache-references ( +- 0.60% ) 244,622,819 instructions # 1.94 insn per cycle ( +- 0.02% ) 126,199,311 cycles ( +- 0.50% ) 0.038089531 seconds time elapsed ( +- 0.53% ) ``` 產生的長條圖 ![perfomance comparision: original against optimized(reduze struct size)](https://i.imgur.com/WeZQKQw.png) ### 改良版本(2): 使用 binary search tree 參考 [Sorted Linked List to Balanced BST](http://www.geeksforgeeks.org/sorted-linked-list-to-balanced-bst/) ,其建樹方法就像 tree traversal 中以 LVR 走訪 (反過來說, LVR 的走訪可以得到原本已排序的 linked list ),利用遞迴從原本的 linked list 建立 binary search tree 建立 binary searh tree 需要給 head 與總數為 input,最後回傳 root 節點 ```clike= /* binary search tree implementation */ node *buildBST(entry **head, int n) { if (n <= 0) return NULL; node *left = buildBST(head, n/2); node *root = (node *) malloc(sizeof(node)); root->pbEntry = *head; root->left = left; *head = (*head)->pNext; root->right = buildBST(head, n - n/2 - 1); return root; } ``` 以 binary search tree 搜尋 entry ```clike= entry *findNameByBST(char lastName[], node *root) { if (root == NULL) return NULL; int result; if ( (result = strcasecmp(lastName, root->pbEntry->lastName)) == 0 ) return root->pbEntry; else if (result < 0) return findNameByBST(lastName, root->left); else return findNameByBST(lastName, root->right); return NULL; } ``` 與原始版本再做一次比較: ![perfomance comparision: original against optimized(reduze struct size + BST)](https://i.imgur.com/RYlcx1Y.png) 可以發現 `append()` 時間增加,因為除了建立 linked list,還額外建立了 binary search tree ,增加了一些時間與空間的成本,但比起建立 linked list 的時間來得少,`findName()` 時間極快,時間複雜度從原本的 O(n) 變成 O(lg n) ### 改良版本(3): 使用 hash function hash function 可以讓我們資料分群,找尋資料只要計算完 hash value 從特定的群體開始找尋,節省 linked list 從頭找起的麻煩 不過有好多個 hash functions 啊!這裡嘗試使用 djb2 看看 ```clike= unsigned long djb2_hash(char *str) { unsigned long hash = 5381; int c; while ((c = *str++)) hash = ((hash << 5) + hash) + c; return hash % HASH_TABLE_SIZE; } ``` 為了建立 hash table (內有多個 linked list) 時不要每次都從找尋 linked list 尾端才加入資料,改在linked list 最前面加入,並指定為 head ```clike= entry *insertFront(char lastName[],entry* e) { entry *pHead = (entry *) malloc(sizeof(entry)); strcpy(e->lastName, lastName); pHead->pNext = e; return pHead; } ``` 測試時使用的 hash table 大小為 50021,再與其他方式一起比較 ![performance comparision:original,reduced size,reduced size + BST, and reduced size + hash(djb2)](https://i.imgur.com/Xxh5zC6.png) 結果 djb2 `append()` 的時間又花更久了,因為多計算了 hash value,但是 `findName()` 與 BST 有相近的表現 ### 改變測資 原本都是以 `zyxel` 來測試 `append()` 與 `findName()` 的效能,但是僅僅在接近35萬筆的資料中尋找一筆顯然不符合真實情況,也不太公平 可以用方便快速的工具 `shuf` 來產生隨機的測資,像是 `$ shuf -n 5000 dictionary/words.txt > random.txt` 底下是1000筆人名測試的結果:不過沒有到很準確,因為連同檔案處理的時間也一起算進去了 ![](https://i.imgur.com/uW376VW.png) 本來看不出 BST 與 djb2 的差異,現在可以發現到 `findName()` djb2 與 BST 差了將近 2 倍 ! 可以說 djb2 在 50021 的 table size 平均表現比 BST 好 ### hash 演算法、大小、衝突、時間的分析 (待續) ## 參考資料 [示範用的 phonebook 作業](https://hackmd.io/s/BJRMImUPg#) [2016q3 自己的共筆](https://hackmd.io/s/SJEbssma) [Linux 效能分析工具: Perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) [使用 gnuplot 科學作圖](http://www.phy.fju.edu.tw/~ph116j/gnuplot_tutorial.pdf) [Sorted Linked List to Balanced BST](http://www.geeksforgeeks.org/sorted-linked-list-to-balanced-bst/) [Skip List](https://puremonkey2010.blogspot.tw/2013/04/skip-list.html) [Hash functions](http://www.cse.yorku.ca/~oz/hash.html) [hash function 觀念和實務](https://hackmd.io/s/HJln3jU_e#) [prime number list](http://compoasso.free.fr/primelistweb/page/prime/liste_online_en.php)