2017q1 Homework1 (phonebook)
===
contributed by <`vtim9907`>
>>固定格式請記得! [name=課程助教][color=red]
### Reviewed by `hugikun999`
* hash table 有許多種,可以比較各種之間的差異性。
* 圖表部分的數據可以調整位置,不然疊再一起不容易觀看
* 考慮所輸入的文字檔是在不事先排序的情況下的效能。
* 為了更貼近現實可以新增刪除 entry 的功能。
* 考慮新增的姓名已經存在電話簿,要如何避免重覆製造 entry。
### Reviewed by `henry0929016816`
* bkdr hash 的乘法 可以使用,被乘數左移 5位 再減 被乘數 的方式,雖然結果同樣是跟原本被乘數乘以 31 的值一樣,但是執行速度有差,由於電腦左一的速度很快。
* 其實網路上都有寫到 hash table size 不要為偶數,然而我有點疑問,2^3^ = 8 , 2^3^-1 = 7 ,照裡來說 n%8 的結果 為 0~7 ,而 n%7 結果為 0~6 ,可以看到 8 比 7 分的還要均勻 ,希望可以用圖表給出 2^n^-1 比 2^n^ 還要好的證明。
# 開發環境
- OS : Ubuntu 16.04.2 LTS
- Kernel Version : 4.8.0-36-generic
- Memory : 32 GB
- CPU : Intel® Core™ i5-6600 Processor
- cache :
- L1i cache : 4*32 KB
- L1d cache : 4*32 KB
- L2 cache : 4*256 KB
- L3 cache : 6144 KB
- L1 cache imformation ( sudo dmidecode -t cache ) :
```
Handle 0x001E, DMI type 7, 19 bytes
Cache Information
Socket Designation: L1 Cache
Configuration: Enabled, Not Socketed, Level 1
Operational Mode: Write Back
Location: Internal
Installed Size: 256 kB
Maximum Size: 256 kB
Supported SRAM Types:
Synchronous
Installed SRAM Type: Synchronous
Speed: Unknown
Error Correction Type: Parity
System Type: Unified
Associativity: 8-way Set-associative
```
- 用簡單 command 查詢系統部份資訊
- 查 ubuntu 版本 ( -a 會列出相關資訊,若只查版本可以用 -r )
`$ lsb_release -a`
- 查 kernel 版本 ( -a 會列出相關資訊,若只查版本可以用 -r )
`$ uname -a`
- 查 cache 資訊
`$ sudo dmidecode -t cache`
- 查 cpu 資訊
`$ lscpu`<br>
比較特別的地方是,用 `$ sudo dmidecode -t cache` 和 `$ lshw` 所查到的 cache 資訊吻合,都顯示我 L1 cache 是 128 KB,但是用 `$ lscpu` 所查到的 cache 資訊卻不相同,顯示我的 L1 cache 只有 32 KB。
所以我去看了我 cpu 的規格 [i5-6600](https://pcpartpicker.com/product/m9Gj4D/intel-cpu-bx80662i56600) ,結果我想是因為四核心的關係,每顆核心的 L1i cache 和 L1d cache 依然都還是 32KB,所以所有指令顯示的都沒錯,只是表達的東西不太一樣。
# 前置作業
孔子說:「 工欲善其事 必先利其器 」,所以開始之前,決定先整理一下作業環境。
- 安裝好用的截圖工具 ------ [Shutter](http://shutter-project.org/)
>其實內建有一個 螢幕截圖 可以用 =w=
>[name=課程助教][color=red]
>>原來如此XD 一開始不知道就直接裝 Shutter 了@@ 不過滿好用的~[name=徐偉庭]
>>~~我也是用shutter,助教很兇0.0~~[name=亮谷]
```
sudo add-apt-repository ppa:shutter/ppa
sudo apt-get update
sudo apt-get install shutter
```
- 修改 vim 的一些基本設定,還有最重要的換顏色!
由於預設的顏色實在有點黯淡,看的不太習慣,決定換點好看的顏色 。
- 可以先到 [這裡](http://www.vim.org/scripts/script_search_results.php?keywords=&script_type=color+scheme&order_by=creation_date&direction=descending&search=search) 找找看有沒有喜歡的 colorscheme
- 把載下來的東西放到 `~/.vim/colors/` 底下( 如果沒有該目錄,就先建出來 )
- 到 `~/.vimrc` 裡設定 `colorscheme <載下來的 color scheme 名稱>`
- 我 color scheme 主題換 `molokai`
![](https://i.imgur.com/aWZer39.png)
# phonebook 優化
## 優化前
執行
```
$ perf stat -r 10 -e cache-misses,cache-references,L1-dcache-load-misses,L1-dcache-store-misses,L1-dcache-prefetch-misses,L1-icache-load-misses ./phonebook_orig
```
得到結果:
```
size of entry : 136 bytes
execution time of append() : 0.035637 sec
execution time of findName() : 0.004737 sec
```
cache-miss 的相關資訊:
```
Performance counter stats for './phonebook_orig' (10 runs):
445,2933 cache-misses # 89.849 % of all cache refs ( +- 0.37% )
495,6032 cache-references ( +- 0.35% )
260,8870 L1-dcache-load-misses ( +- 0.26% )
<not supported> L1-dcache-store-misses
<not supported> L1-dcache-prefetch-misses
14,5130 L1-icache-load-misses ( +- 2.06% )
0.048939923 seconds time elapsed ( +- 1.70% )
```
## Method 1 : 縮小 struct size
參考了教材 [Programming Small](https://hackmd.io/s/HkO2ZpIYe) ,我想先針對改善 cache miss 下手; cache 是很珍貴的資源,所以通常 size 都不大,在這樣的情況下我想先縮小 phonebook 的 struct ,以減少 cache miss 的發生。
>是 cache 喔! =w= [color=red][name=課程助教]
>>阿...打錯一堆@@ 感謝提醒! [name=徐偉庭]
原本的 struct 總共有 136 bytes,因為我的 cache 只有 256 KB, 所以總共可以存約 32 * 1024 / 136 = 240.9 筆資料,在讀取大量資料時,還是很容易發生海量的 cache miss,所以我把 struct 刪減成 32 bytes :
```clike=
typedef struct __PHONE_BOOK_ENTRY {
char lastName[MAX_LAST_NAME_SIZE];
entry_detail *detail;
struct __PHONE_BOOK_ENTRY *pNext;
} entry;
```
因為題目只需要搜尋 lastName ,所以可以把 struct 刪減到只剩 32 bytes,其中當然保留了原來的 lastName 還有 pNext ,除此之外還加了一個 detail 的指標,用來指回原來完整的 struct,不過這裡我並沒有 malloc 空間給它,因為再多做 malloc 勢必會產生更多的 cache miss,但畢竟 dictionary 裡只有 lastName 的資料,那我就省下來,暫且不管其他資料,等有需要用到其他更細部的資料時,再用其他 function 處理。
執行
```
$ perf stat -r 10 -e cache-misses,cache-references,L1-dcache-load-misses,L1-dcache-store-misses,L1-dcache-prefetch-misses,L1-icache-load-misses ./phonebook_opt
```
得到結果
```
size of entry : 32 bytes
execution time of append() : 0.027508 sec
execution time of findName() : 0.002284 sec
```
append 所耗費的時間減少了 0.08 秒左右,想必是因為 size 變小,malloc 消耗的時間因此變短,進而提昇程式的效能。
```
Performance counter stats for './phonebook_opt' (10 runs):
133,2860 cache-misses # 59.392 % of all cache refs ( +- 1.61% )
224,4169 cache-references ( +- 1.03% )
129,2047 L1-dcache-load-misses ( +- 0.27% )
<not supported> L1-dcache-store-misses
<not supported> L1-dcache-prefetch-misses
9,9346 L1-icache-load-misses ( +- 10.19% )
0.037362806 seconds time elapsed ( +- 15.04% )
```
相較於優化前,cache miss 減少了約 70%,而 cache references 減少了約 54.7%,總消耗時間節省了約 23.6%。
![](https://i.imgur.com/XY3zSwI.png)
## Method 2 : Hash table
為了更加優化搜尋的效能,選擇使用 hash table 似乎是個不錯的方法,畢竟搜尋一個 index 比一個一個比對字串還要來得快的多。
而根據 [各种字符串Hash函数比较](https://www.byvoid.com/zhs/blog/string-hash-compare) 內的實驗結果顯示,BKDR hash 相較於其他 hash 方法,在各種情形下似乎比較不容易產生碰撞,所以我選擇使用 BKDR hash 來對字串做 hash 產生建 table 用的索引值。
```clike=
unsigned int BKDR_hash(char *str)
{
unsigned int seed = 31;
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return (hash & 0x7FFFFFFF);
}
```
然而在選擇 table 大小時我遇到了一點困難,我不太確定選擇多大的 table size 比較好,然而我上網看了一些文章,有人說最好挑選測資數量的 1.45 倍,並且是個質數,如此比較不容易發生碰撞;也有一部分的人建議直接用測數字的方式,找出效率最好的數字,因為根據測資的屬性不同,產生的結果就會有出入,似乎沒有多大的 size 最好這回事;不過我可以肯定的是,size 太小則碰撞機率太大,size 太大又有可能會 overflow。
所以我的作法是用 $2^n - 1$,以改變 n 的方式去測,好處是快速不用特別去找質數,也比直接用 $2^n$ 還不容易發生碰撞。
>>請用 LaTeX 表示數學式
>>[name=課程助教][color=red]
n = 11
```
size of entry : 32 bytes
execution time of append() : 0.051272 sec
execution time of findName() : 0.000001 sec
Performance counter stats for './phonebook_hash' (10 runs):
598,2769 cache-misses # 54.948 % of all cache refs ( +- 1.47% )
1088,8151 cache-references ( +- 0.15% )
486,1986 L1-dcache-load-misses ( +- 0.10% )
<not supported> L1-dcache-store-misses
<not supported> L1-dcache-prefetch-misses
14,8238 L1-icache-load-misses ( +- 6.55% )
0.132551090 seconds time elapsed ( +- 3.08% )
```
n = 12
```
size of entry : 32 bytes
execution time of append() : 0.053231 sec
execution time of findName() : 0.000000 sec
Performance counter stats for './phonebook_hash' (10 runs):
572,2466 cache-misses # 53.195 % of all cache refs ( +- 1.19% )
1075,7484 cache-references ( +- 0.29% )
496,5232 L1-dcache-load-misses ( +- 0.07% )
<not supported> L1-dcache-store-misses
<not supported> L1-dcache-prefetch-misses
13,9759 L1-icache-load-misses ( +- 5.82% )
0.134102282 seconds time elapsed
```
n = 13
```
size of entry : 32 bytes
execution time of append() : 0.048652 sec
execution time of findName() : 0.000000 sec
Performance counter stats for './phonebook_hash' (10 runs):
231,0402 cache-misses # 52.510 % of all cache refs ( +- 1.61% )
439,9904 cache-references ( +- 0.77% )
241,0954 L1-dcache-load-misses ( +- 0.19% )
<not supported> L1-dcache-store-misses
<not supported> L1-dcache-prefetch-misses
13,1062 L1-icache-load-misses ( +- 7.89% )
0.084454930 seconds time elapsed ( +- 7.07% )
```
可以看到當 n = 13 ,table size = 2^13 - 1 = 8191,此時的 findName 所消耗的時間已經幾乎等於 0,這的確是我們所預期的,建 hash table 後直接用 index 搜尋的確比每個字串慢慢比對來的快的多。
而 append 所花的時間卻變多了,我想是因為 append 裡做了更多的處理,時間被拉高也是可以預期的。
![](https://i.imgur.com/8EYKo6q.png)
在我準備 free 掉每條 linked list 的時候,意外發現原來先前 main.c 裡並沒有完整的把 linked list 給 free 掉,會造成 memory leak 的情況,所以我以順手改了一下。
然而在我寫 free list 的 function 時,原本想說用 hash table 後,list 的長度應該會短一些,想用 recursive 的方式來釋放全部的 linked list ,沒想到即使 table 的 size 已經大到 16383 時,仍然會造成 segmentation fault 的情況產生...,看來還是應該乖乖用 loop 的方式來釋放 linked list。
## Method 3 : Binary search tree
建一棵 binary search tree,把原來創出來的 linked list 放進創出來的樹裡,理論上來說,因為存資料的結構變成 BST ,所以插入、搜索、刪除的時間複雜度期望為 O(log n),最差的情況為 O(n),所以我預期 findName 的速度應該會比未優化前快不少。
實際做出來就是很普通的 BST,是在原版的 linked list 建出來之後,在把 list 轉成 BST,原本有想過在一邊讀字串時就一邊建樹,但感覺這樣會使運算量大幅增加,所以就還是實做把建好的 list 轉成 tree。
實際運行成果:
```
size of entry : 32 bytes
execution time of append() : 0.038473 sec
execution time of findName() : 0.000000 sec
```
由於仍然保留了縮減過的 struct,所以 struct 的大小只有 32 bytes。
可以看出 findName 的時間一樣縮減到幾乎為 0 秒,BST 果然起了作用!
而 append 比優化前慢了 0.03 秒左右,這是因為我在 append 之後建樹,把建樹的時間也算在 append 裡了,不過只慢了這麼一點還真出乎我意料@@
再來看看 cache 的情況:
```
Performance counter stats for './phonebook_bst' (10 runs):
194,2487 cache-misses # 72.805 % of all cache refs ( +- 0.69% )
266,8058 cache-references ( +- 0.35% )
156,9015 L1-dcache-load-misses ( +- 0.41% )
<not supported> L1-dcache-store-misses
<not supported> L1-dcache-prefetch-misses
11,4174 L1-icache-load-misses ( +- 2.27% )
0.047925877 seconds time elapsed ( +- 5.29% )
```
cache-miss 相較優化前少了快三百萬次,但主要的原因我認為是因為 struct 還是維持 32 bytes 的關係。
各版本圖形化比較:
![](https://i.imgur.com/QRKrH7m.png)
## Method 4 : 改變 BST node 結構
由於老師提到 BST 可以再做優化,所以我又再次思考我原來的 BST 有哪裡可以優化,我想為了符合 cache line 的特性,決定把 node 的結構更該成 64 bytes 的大小,這樣一個 node 應該就剛好就在同一條 cache line 上 @@"
原來 node 的結構就只包含兩個分別指到左、右子樹的指標,和一個指到一個 phonebook entry 的指標:
```clike=
typedef struct __BST_NODE {
struct __BST_NODE *left;
struct __BST_NODE *right;
entry *pEntry;
} bst;
```
更改後的結構與原來的大致雷同,不同的地方是一個節點就包含了 6 個指到 phonebook entry 的指標,有點類似一個比較大的 node 的感覺:
```clike=
typedef struct __BST_NODE {
struct __BST_NODE *left;
struct __BST_NODE *right;
entry *pEntry[6];
} bst;
```
雖然不太確定結果會如何,但是我預期在 append 的時候會稍微快一些,因為拿一個 node 就可以塞 6 個值,而且也不會增加 cache-miss,所以應該會比原來的 append 的 cache-miss 更少、速度更快; 而 findName 的部份因為一個點要比對六次,所以時間應該是原來 BST 的六倍,但時間複雜度同樣都還是 O(log n)。
經過實測:
![](https://i.imgur.com/UwH8Ymj.png)
改版的 BST append 的時間果然變少,比原來的 BST 快 0.006 秒,效能提昇約 17% ; 而 findName 的時間雖然比較慢,但在搜尋同一筆資料的情況下,時間依然接近 0 秒,畢竟時間複雜度相同,應該不會慢太多。
再來看一下 cache-miss 有沒有減少:
原版
```
Performance counter stats for './phonebook_bst' (100 runs):
187,3471 cache-misses # 71.267 % of all cache refs ( +- 0.14% )
262,8803 cache-references ( +- 0.12% )
3,2452,4839 instructions # 2.18 insn per cycle ( +- 0.01% )
1,4914,7703 cycles ( +- 0.06% )
0.041766708 seconds time elapsed ( +- 0.37% )
```
新版
```
Performance counter stats for './phonebook_bst2' (100 runs):
169,4826 cache-misses # 69.828 % of all cache refs ( +- 0.31% )
242,7143 cache-references ( +- 0.12% )
2,5219,4566 instructions # 1.99 insn per cycle ( +- 0.02% )
1,2681,9661 cycles ( +- 0.13% )
0.036295141 seconds time elapsed ( +- 0.38% )
```
雖然覺得這樣修改後仍然還有很大的優化空間,不過幸好這次的想法至少有優化到@@"
## 問題討論
- 本例選用的 dataset (也就是輸入電話簿的姓名) 是否存在問題?
1. 有代表性嗎?跟真實英文姓氏差異大嗎?
- 因為本例選用的 dataset 裡有大多數不具意義的英文字母組合,若要談與真實英文姓名的差異,那想必差異慎大;但如果是 dataset 裡代表的是別名或化名之類的資料,那也許就還滿符合真實情況。
- 討論到 dataset 的代表性,我認為除了資料與真實英文姓氏有差異外,還有一個重點是資料的重複性,以真實情況來看,資料如果夠多,肯定會有一狗票的重複姓氏出現,但是在本例裡的資料似乎沒有重疊的資料,這樣跟真實情況就差異慎大;總體來說,不太具代表性。
2. 資料難道「數大就是美」嗎?
- 我不這麼認為,就如前一題探討到的代表性,如果所有的資料都不具代表性,那麼再多也不「美」;讓我想到資料探勘,就算餵進去大量的資料,但如果餵的資料質量不好,那麼不僅效率不好,產出的結果也沒有什麼意義,精通資料視覺化以及統計學專家 Kaiser Fung 在 [Good models + Bad data = Bad analysis](http://junkcharts.typepad.com/numbersruleyourworld/2017/01/good-models-bad-data-bad-analysis.html) 一文裡也提到,質量不好的資料就算經過很好的模型分析,最後的分析結果的精確度和參考度都是很差的。
3. 如果我們要建立符合真實電話簿情境的輸入,該怎麼作?
- 其實網路上有很多姓名產生器,但是可能沒辦法快速的產生大把資料,所以比較快的方法大概是去一些列出大量名字的網站,比如說 [List of people by name](https://simple.wikiquote.org/wiki/List_of_people_by_name) ,把裡面的人名爬下來,其中裡面的姓氏也有重複,也更符合真實情況。
# Reference
- [Programming Small](https://hackmd.io/s/HkO2ZpIYe)
- [吳勃興學長的筆記](https://hackmd.io/s/Bkx3cYX6)
- [wikipedia 雜湊表](https://zh.wikipedia.org/wiki/%E5%93%88%E5%B8%8C%E8%A1%A8)
- [各种字符串Hash函数比较](https://www.byvoid.com/zhs/blog/string-hash-compare)
- [Hash Table:Intro(簡介)](http://alrightchiu.github.io/SecondRound/hash-tableintrojian-jie.html)
- [Hash Table:Chaining](http://alrightchiu.github.io/SecondRound/hash-tablechaining.html)
- [二元搜尋樹](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%85%83%E6%90%9C%E5%B0%8B%E6%A8%B9#.E6.8F.92.E5.85.A5)