# 2017q1 Homework1 (phonebook) contributed by < `claaaaassic` > ### Reviewed by `chenweiii` - 建議可以嘗試引入 memory pool,append() 的時間會更加漂亮 - 建議可以把 hash table 的 entry struct 湊成近 cache block 的大小,在我的實驗裡 cache misses 下降了約 7% - 我認爲透過字母統計去比較兩份姓名分佈是否相似,是有疑慮的,更何況不同的 hash function 對各個字母的加權值又有所不同。或許透過同一 hash function,然後去觀察兩份姓名的 hash value 分佈,是一個可以進行的方向。 ## 準備 GNU/Linux 開發工具 * 安裝 ubuntu 16.04 LTS ``` $ lscpu 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 型號: 42 Model name: Intel(R) Core(TM) i5-2410M CPU @ 2.30GHz 製程: 7 CPU MHz: 809.295 CPU max MHz: 2900.0000 CPU min MHz: 800.0000 BogoMIPS: 4589.19 虛擬: VT-x L1d 快取: 32K L1i 快取: 32K L2 快取: 256K L3 快取: 3072K NUMA node0 CPU(s): 0-3 ``` * 安裝開發工具 ``` $ sudo apt-get update $ sudo apt-get install build-essential $ sudo apt-get install linux-tools-common linux-tools-generic $ sudo apt-get install cppcheck astyle colordiff gnuplot ``` ## 學習使用 Git 與 GitHub * linux 產生 ssh key 貼到 github 個人設定裡面 * 更改 git config --global 的 user.name user.email * clone 這次作業 ```$ git clone https://github.com/claaaaassic/phonebook.git``` ### **我沒有認真讀文章...** 於是 : [實在是太經典了](https://www.facebook.com/groups/system.software2017/permalink/1322345671164132/) > 當然你可能只是想說「我沒有這麼崇高的目標,我來大學只是為了混文憑」,既然如此,拜託不要讀成大,重考去台大吧,後者的文憑才稍為有點強度 在我 push 兩個中文的 commit 後就被抓出來鞭了,趕緊把中文改成國際化語言**英文**,因為我的兩個 commit 就算是同一個實驗功能,用這個機會把他們合併起來,加入英文 commit ```shell $ git log $ git rebase HEAD~2 最新的 commit 前方改成 suqash 他會把前方有 s 的 commit 與他前一個紀錄進行合併 因為我是要兩個合併成一個,所以只需要修改一個就好 儲存後會出現 vim 編輯視窗,在這裡就可以把合併完的英文 commit 輸入進去囉 結束後要強制把遠端的紀錄改成新的 $ git push --force origin master ``` 看完老師推薦閱讀的文章 [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/),下面說明一下我閱讀完推薦文章後改的 commit message ![](https://i.imgur.com/A4BhH7n.png) ### How to Write a Git Commit Message 首先是推薦文章內的重點:好的 commit message 的七個規則 The seven rules of a great Git commit message 1. Separate subject from body with a blank line 標題與內文以一個空白行分隔 > 這樣可以在使用 $ git log 閱讀時叫容易分辨出哪行是標題 2. Limit the subject line to 50 characters 標題字數最多 50 > 因為標題字數超過 50 在 github 上會被隱藏到 ... 裡面 > 需要額外點開才能完整知道這個 commit 在做什麼 > 超過 50 字可能就不是一個精簡的標題了 3. Capitalize the subject line 標題首字大寫 4. Do not end the subject line with a period 標題結尾不要句點 5. Use the imperative mood in the subject line 標題用命令口吻 6. Wrap the body at 72 characters 內文一行字數不超過 72 > 這樣在 $ git log 裡面看會比較清楚,排版比較好看 7. Use the body to explain what and why vs. how 內文說明是什麼、為什麼而不是如何做 > 因為看 commit 的人是想知道你為什麼要做這件事,而不是要知道你如何做這件事的 一開始很難想要怎麼寫別人才能一目了然,應該需要練習才能寫的更好吧 ! 想到更好的就可以改,等到別人發現你可笑的 commit 時就已經來不及了QQ ## 學習效能分析工具 perf * 安裝 perf ```$ sudo apt-get install linux-tools-common``` ```$ perf list``` ```$ perf top``` 這裡出現我的權限預設值是 3 還蠻奇怪的,==還沒查到為什麼==,正常應該要是 1 最後我還是用這個指令改成正常的預設值 1 ```$ sudo sh -c 'echo 1 >/proc/sys/kernel/perf_event_paranoid'``` ![](https://i.imgur.com/1o5pSvA.png) 接著繼續下去,使用 perf 測試 perf_top_example.c ![](https://i.imgur.com/VEqGcNO.png) ## 研究軟體最佳化 ### Makefile 這裡不太懂 ```$ make```為什麼可以打開 Makefile 依照裡面的規則編譯檔案,找了一下 [Makefile 概念與基礎I](https://dywang.csie.cyut.edu.tw/dywang/linuxProgram/node49.html) * 當執行 make 時,make 會在當時的目錄下搜尋文字檔 makefile ( or Makefile ),其記錄了原始碼如何編譯的詳細資訊。 * make 使用 makefile 的好處: * 簡化編譯時所需要下達的指令 * 若在編譯完成之後,修改了某個原始碼檔案,則 make 僅會針對被修改了的檔案進行編譯,其他的 object file 不會被更動 * 最後可以依照相依性來更新( update )執行檔。 > 以下測試都有先清除既有 cache ```$ echo 1 | sudo tee /proc/sys/vm/drop_caches``` ### 原始程式 ``` $ ./phonebook_orig size of entry : 136 bytes execution time of append() : 0.057314 sec execution time of findName() : 0.005804 sec $ perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./phonebook_orig Performance counter stats for './phonebook_orig' (100 runs): 2,018,903 cache-misses # 91.012 % of all cache refs ( +- 0.44% ) 2,218,282 cache-references ( +- 0.46% ) 262,352,481 instructions # 1.18 insn per cycle ( +- 0.02% ) 221,787,124 cycles ( +- 1.24% ) ``` size of entry : 136 bytes cache-misses : 91.012 % 參考 [cache 原理和實驗](https://hackmd.io/s/S14L26-_l#) - cache 總大小是 32 KB - 一個 cache 的 block 為 64 B - 一個 entry 的大小為 (136 B) + memory control block(8 B) = 144 B 一個 entry 需要用到的 block 數 144 B / 64 B = 2.25 所以需要用到 3 個 block 如果縮小 entry 的大小應該可以增加 cache 存放 entry 的數量,減少 cache miss 的數量 ### 優化版本 1 : 改寫 struct __PHONE_BOOK_ENTRY 的成員,搬動到新的結構中 因為都只用到搜尋 last_Name 的功能,所以就不先分配 detail 的記憶體,以後需要再加上去 ```shell 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; typedef struct __PHONE_BOOK_ENTRY { char lastName[MAX_LAST_NAME_SIZE]; detail *detail; struct __PHONE_BOOK_ENTRY *pNext; } entry; ``` size of entry 從 **136 bytes** 降到 **32 bytes** cache-misses 可以從 **91%** 降到 **63%** ``` $ ./phonebook_opt size of entry : 32 bytes execution time of append() : 0.046207 sec execution time of findName() : 0.003105 sec $ perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./phonebook_orig Performance counter stats for './phonebook_opt' (100 runs): 342,113 cache-misses # 62.647 % of all cache refs ( +- 0.29% ) 546,093 cache-references ( +- 2.10% ) 244,702,580 instructions # 1.11 insn per cycle ( +- 0.02% ) 221,244,338 cycles ( +- 0.89% ) ``` cache miss 減少,在時間上也看得出有減少 ![](https://i.imgur.com/01TEBxV.png) ### 優化版本 2 : 使用 hash function 來加速查詢 使用 hash 加速搜尋時間,雖然 append()的時間會變長,但是可以大幅降低 findName() 的時間 這邊參考 [各种字符串Hash函数比较](https://www.byvoid.com/zhs/blog/string-hash-compare) 使用 BKDR hash function - table size 參考 [Programming Small](https://hackmd.io/s/S1rbwmZ6#0) 設為 42737 - seed 設為 131 cache miss 從 **63%** 降到 **55%** findName() 的時間也降到 **0.000000 sec** ```shell size of entry : 32 bytes execution time of append() : 0.057896 sec execution time of findName() : 0.000000 sec Performance counter stats for './phonebook_hash' (100 runs): 355,920 cache-misses # 54.538 % of all cache refs ( +- 0.36% ) 652,609 cache-references ( +- 0.45% ) 236,541,025 instructions # 1.46 insn per cycle ( +- 0.02% ) 161,652,254 cycles ( +- 0.34% ) ``` ![](https://i.imgur.com/5Hrb7cx.png) 圖表可以看出改用 hash function 後大部分時間都花在 bkdrHash 上面,append 不到他的一半 ```shell Samples: 89 of event 'cycles', Event count (approx.): 561473 Overhead Shared Object Symbol 28.90% phonebook_hash [.] bkdrHash 13.00% libc-2.23.so [.] _IO_fgets 11.25% phonebook_hash [.] append 10.75% phonebook_hash [.] main 7.50% libc-2.23.so [.] _int_malloc 5.64% libc-2.23.so [.] __memcpy_sse2 4.12% libc-2.23.so [.] __strcpy_sse2_unaligned 3.17% [kernel] [k] release_pages 3.15% libc-2.23.so [.] _IO_getline_info 2.83% libc-2.23.so [.] memchr 2.15% [kernel] [k] unmap_page_range 1.11% phonebook_hash [.] strcpy@plt 1.07% [kernel] [k] __mod_zone_page_state 1.07% [kernel] [k] __fsnotify_parent 1.07% libc-2.23.so [.] malloc 1.06% [kernel] [k] try_charge 1.04% [kernel] [k] _raw_spin_lock 1.03% [kernel] [k] clear_page 0.08% [kernel] [k] flush_smp_call_function_queue 0.00% [kernel] [k] native_write_msr ``` ### table size 與效能的關係 接下來想來研究一下 table size 到底該設為多大的質數才對呢 選了幾個萬級的質數來測試一下,結果在 50000 左右的質數效能最好,append() 時間最短以及 cache-misses 比例最小 第一次測試 ![](https://i.imgur.com/LgtSCIi.png) 第二次測試 ![](https://i.imgur.com/HmT7zzn.png) #### 看來應該是介於 40000 到 80000 之間的質數比較適合 而兩次的測試 append 時間跟 table size 沒有一定的關係 ![第一次](https://i.imgur.com/9Na8uPA.png) ![第二次](https://i.imgur.com/qiueZCw.png) ### 指定問題 * 有代表性嗎?跟真實英文姓氏差異大嗎? * 不具代表性,參考[維基百科:英語姓氏](https://zh.wikipedia.org/wiki/%E8%8B%B1%E8%AF%AD%E5%A7%93%E6%B0%8F)真實英文姓名是有固定來源而非隨機輸入組合而成,在比較早期還不是大部分人不識字時,登錄姓氏是以發音取相似可行的姓氏,而非使用隨機的英文排列 * 分析一下我們所使用的 words.txt 他所有字母出現個數下去做統計圖,可以看得出他不是亂數使用字母組合而成的 ![](https://i.imgur.com/huMTgsZ.png) * 接下來使用 dictionary/all-names.txt 來觀察個數的統計圖,可以看出 words.txt 這份資料使用的字母數量其實跟真實姓名分佈相近的,在整體比例上來說,差異不大,但是裡面單獨每一個的姓名與真實姓名卻沒有完全一樣(有待做出分析圖) ![](https://i.imgur.com/ghMQ2Ro.png) * 資料難道「數大就是美」嗎? * 首先資料要先符合實際情況,根據存在比例不同的每個姓氏數量不同,姓氏數量也要齊全,在符合這些情況下,大量的資料可以消除較少使用或已無人使用的姓氏,符合真實情況, * In the United States, 1,712 surnames cover 50% of the population,參考[維基](https://en.wikipedia.org/wiki/Surname) ![](https://i.imgur.com/M8EemSS.jpg) * 如果我們要建立符合真實電話簿情境的輸入,該怎麼作? * 在有一個有代表性的資料下,使用隨機抽樣的方法 ### 疑問 最後想要弄清楚一件事,[cache 原理和實驗](https://hackmd.io/s/S14L26-_l#)上面計算 cache 的方式是錯的 * 條件: * cache 總大小是 32 KB * 一個 cache 的 block 為 64 B * 一個 entry 的大小為 (136 B) + memory control block(8 B) = 144 B * 8-way set associative * 計算: * **有多少 block?** (我考慮 main.c findName() & append 這 2 個 function 因為串練結跟搜尋存取花最多時間) 350000 筆資料 * 每筆 144 B = 5040000 B 5040000 B / 64 B * 2 次 function = 1575000 block 讀一個 block 是一次 ref * **cache 有幾個 set?** cache 32 KB/block 64Byte = 512 個block 512 / 8-way set = 64 個 * **一個 cache line 可以存多少筆 entry?** 144 B / 64 B = 2.25 所以是 3 個 block 已知是 8-way set 所以可以存 2 個 * **cache miss** 1575000 (總 block)/ 3 (一個 entry 需要 3 block) = 525000 組 entry 525000 / 64 (set 數) = 8203.125 (填到相同 set 的個數) 由上知一個 cache line 最多可以放 2 組 entry,所以有兩次機會 8203.125 / 2 = 4101.5625 (可能被對到次數) 4101.5625 * 64 (set 數)* 3 (entry 佔的 block 數) = 797500 (cache hit 次數) 1574000 - 787500 = 786500 (cache miss) 786500 / 1574000 = 50% (miss rate) 怪怪的,不知道怎麼正確的算出來 ### code review 後的修改 在 review 完 [bananahuang](https://hackmd.io/GwTgJgxsBMIMwFoBGAOOEEBY5IKYIEM4BWJBOOEFAM2pCUxFwAYg#)的共筆之後,看到自己還未 free 整個 linked-list,所以修改一下程式。 * 首先參考 bananahuang 使用 valgrind 檢查記憶體的使用情況 `$ valgrind --leak-check=full ./phonebook_opt` ``` ==5403== HEAP SUMMARY: ==5403== in use at exit: 11,196,768 bytes in 349,899 blocks ==5403== total heap usage: 349,906 allocs, 7 frees, 11,207,152 bytes allocated ``` 這邊可以看出我總共配置 349906 次記憶體以及釋放 7 次 接著在 main.c 更改程式,從原本只清除兩個節點,改成清除整個 list ```shell while(pHead->pNext) { entry *next = pHead->pNext; pHead->pNext = next->pNext; free(next); } free(pHead); ``` 成功釋放所有用完的記憶體 ``` ==5915== HEAP SUMMARY: ==5915== in use at exit: 0 bytes in 0 blocks ==5915== total heap usage: 349,906 allocs, 349,906 frees, 11,207,152 bytes allocated ==5915== ==5915== All heap blocks were freed -- no leaks are possible ``` 在 hash 的部份另外加了一個函式釋放記憶體,最後也成功釋放所有記憶體。 ``` void freeHashTable(hashTable *ht) { for(int i=0; i<HASHTABLE_SIZE; i++) { entry *e = ht->list[i]; if(!e) { free(e); continue; } while (e->pNext) { entry *next = e->pNext; e->pNext = next->pNext; free(next); } free(e); } free(ht->list); free(ht); } ``` 這邊我卡一小段時間,不知道為什麼沒有加判斷這個節點是不是 null 的時候會只有 14,362 frees,但最後加上去還是成功釋放完畢 ``` if(!e) { free(e); continue; } ``` ## 參考文件 [perf 原理和實務](https://hackmd.io/s/B11109rdg#) [dada 的共筆](https://embedded2016.hackpad.com/ep/pad/static/YkqjhwgnQcA) [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/) [Makefile 概念與基礎I](https://dywang.csie.cyut.edu.tw/dywang/linuxProgram/node49.html) [cache 原理和實驗](https://hackmd.io/s/S14L26-_l#) [Programming Small](https://hackmd.io/s/S1rbwmZ6#0) [各种字符串Hash函数比较](https://www.byvoid.com/zhs/blog/string-hash-compare) [Hsiang 的共筆](https://hackmd.io/s/HylfV2FFe#) 他提到可以使用 perf 分析 cache-misses 發生的地方有機會可以使用看看 ```shell ⚡ sudo perf record -e cache-misses ./phonebook_orig ⚡ sudo perf report ```