2016q3 Homework1 / phonebook
===
###### tags: `jserv`
Date: 2016/09/30
contribute by <`hankgo`>
+ [github](https://github.com/hankgo/phonebook)
+ youtube
---
# 電腦環境
+ OS: Lubuntu 16.04 (upgrade from 15.10)
因為舊的版本無法用內建的圖形介面升級,說缺少元件,所以使用終端機輸入指令升級。[參考網址](http://askubuntu.com/questions/765648/upgrading-from-15-10-to-16-04-apt-not-installed)
```=
sudo apt-get update
sudo apt-get upgrade
sudo apt-get -f install # (not "install -f"!)
sudo apt-get -y install apt
sudo do-release-upgrade
```
+ CPU: [Intel i3-2350m](http://ark.intel.com/zh-tw/products/53438/Intel-Core-i3-2350M-Processor-3M-Cache-2_30-GHz) ( 2 core / 4 thread )
+ 使用指令 `$ lscpu`
+ L1 data cache = 32K
+ L1 instruction cache = 32K
+ L2 cache = 256K
+ L3 cache = 3072K
+ RAM: 8GB
# github 指令
+ [參考資料](http://blog.gogojimmy.net/2012/01/17/how-to-use-git-1-git-basic/)
+ `$ git clone xxxx.git` fork 之後下載檔案
+ `$ git status` 看狀態
+ `$ git add [filename]` 將檔案加入追蹤清單
+ `$ git add .` 將全部檔案加入追蹤清單
+ `$ git add -i` 互動模式,用挑選的加入清單
+ `$ git reset [filename]` 特定檔案從 stage 移開 (移開追蹤)
+ `$ git reset .` 檔案從 stage 移開
+ `$ git commit` commit,事後會問你要加入什麼訊息
+ `$ git commit -m "message"` 一併將 message commit
+ `$ git commit -v` 顯示檔案變動增減記錄
+ 如何修改/取消上一次的 commit
+ `$ git commit --amend` 修改上一次的 commit 訊息。
+ `$ git commit --amend [filename1] [filename2]...` 將檔案1、檔案2加入上一次的 commit。
+ `$ git reset HEAD^ --soft` 取消剛剛的 commit,但保留修改過的檔案。
+ `$ git reset HEAD^ --hard` 取消剛剛的 commit,回到再上一次 commit的 乾淨狀態。
+ `git log` 查看過去 commit 的紀錄
+ `git log -stat` 看到更詳盡的訊息
+ `git log -p` 看到檔案更詳細的變更內容
+ 基本流程:修改檔案 => 加入 stage (git add) => 提交( git commit )=> 繼續修改其他檔案 => 上傳到 github (git push)
# 改善方式
## reduce structure size
+ phonebook_orig.h
```=
/* original version */
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;
entry *findName(char lastName[], entry *pHead);
entry *append(char lastName[], entry *e);
#endif
```
+ phonebook_opt.h
```=
typedef struct __DETAIL_ENTRY {
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];
} detailEntry;
typedef struct __PHONE_BOOK_ENTRY {
char lastName[MAX_LAST_NAME_SIZE];
detailEntry *detail;
struct __PHONE_BOOK_ENTRY *pNext;
} entry;
```
因為目前的搜尋只會使用到 lastName 而已,所以先將其他的欄位用別的 detailEntry structure 包起來,原始的 __PHONE_BOOK_ENTRY 只剩下 lastName、pNext、還有 detailEntry 這樣,名字搜尋到之後,真的有需要再去 detailEntry 去撈資料。如此一來,原始的 structure 的大小就會縮減,cache可以塞的 structure 的數量會增加。
這個嘗試之中,尚未去更動 phonebook_orig.c 的內容, phonebook_opt.c 的內容目前是跟 phonebook_orig.c 的內容是相同的,沒有做太大更動。
### OPTION Control
改好了之後,跑了數次發現怎麼圖表都是一樣的。發現我的 Program 沒有把 opt.txt 印出來,再看看哪邊有跟 opt.txt 有關係,發現以下 code,兩個 orig 跟 opt 版本都是共用一個 main,是利用 OPT 的 define 狀況來控制適用哪一個 output file,未來要實作 hash optimized 的版本,也可能需要適當的修改這段 code 來共用 main.c。所以這個 #if define(OPT) 是關鍵
```=
#if defined(OPT)
output = fopen("opt.txt", "a");
#else
output = fopen("orig.txt", "a");
#endif
```
一開始在 main.c 找了好久找不到,經過很長一段時間發現 phonebook_opt.h 有下列這段 code,只是一開始 clone 的時候是被註解的。解除註解就順利產生 opt.txt 也順利做出第一個改善效能的成果。
```
#define OPT 1
```
未來要再增加使用 hash function 的效果,則需要新增新的 OPT 定義和輸出,故修改如下 (MAKEFILE 等也需要一併修改):
參考資料:
+ [#ifndef, #define, #endif的用法(整理)](http://huenlil.pixnet.net/blog/post/24339151)
+ [ruby0109 github](https://github.com/ruby0109/phonebook/blob/master/main.c)
## Hash Function
### 前置修改
我將第二種優化的方式寫在新的檔案 phonebook_hash,然後共用同一個 main.c。根據觀察 makefile 等檔案的結果,其他的檔案需要做對應的調整
+ Makefile:要增加 CFLAGS_hash、增加 phonebook_hash 的 compile (EXEC、phoebook_hash 的動作)、增加 phonebook_hash 的 perf test、make clean 要記得清 phonebook_hash 所產生的 hash.txt
+ main.c:利用 define OPT 來增加 output.txt 檔名 switch 的支援 (opt_hash.txt)
```
#if defined(OPT)
output = fopen("opt.txt", "a");
#elif defined(HASH)
output = fopen("hash.txt", "a");
#else
output = fopen("orig.txt", "a");
#endif
```
+ calculate.c:增加 phonebook_hash 的時間計算
+ scripts/runtime.gp:增加屬於 hash 方式的圖表
### hash function 簡介
hash function 常用於密碼學。常見的函數有一對一、多對一,一對一的函數可以求出其函數的反函數 (可以根據輸出結果反推輸入值)。而多對一的函數則沒辦法有反函數。密碼學有時候會利用 hash function 來製作真實密碼的部分特徵。hash function 其實就是對原始輸入字串做一連串的特殊函數運算,輸出一個固定長度的結果,也就是原始輸入字串的特徵。hash function 個人覺得重要的特性如下:
+ 不容易從輸出的資訊反推回原始資訊
+ 相同輸出資訊,但由兩組以上相同資訊的字串輸入到 hash function 的機率很低,若有則為 hash collision,以下為範例:
```
hash function h(x) = x ^ 2
h(5) = 25; h(-5) = 25,這時候就有 hash collision
```
所以根據上述特性,hash function 函數的設計上,要盡量不容易被反推,且要注意避免 collisiion。
根據前人和同學的心得,我也先嘗試了 djb2,是個專門處理文字字串的 hash function,不管字串長度為何,都可以得到一個 unsigned int 型態的 hash value,djb2 的原型如下:
```=
unsigned long hash(unsigned char *str)
{
unsigned long hash = 5381;
int c;
while (c = *str++)
hash = ((hash << 5) + hash) + c; /* hash * 33 + c */
return hash;
}
```
根據我們所需,修改成以下版本,並導入 hashTable 取餘數的想法 :
```=
unsigned int djb2Hash(char lastName[])
{
unsigned long hash = 5381;
int c;
while ((c = *lastName++)) {
hash = (( hash << 5 ) + hash ) + c; // hash*33+c
}
return (unsigned int) hash % HASH_TABLE_SIZE;
}
```
### 實做想法
我們會設法建立一個 hash table,先對這些 hash value 進行分組。目前作業提供的 words.txt 資料約有 35萬筆,最理想的狀況是,我們可以平均地分成若干組,每一組都有共同的 index,當我們要查詢某個名字時也先利用共同的 hash function 求出 index,在到某一組去搜尋,就可以免去從頭到尾慢慢搜尋的窘狀。那要分成幾組呢?根據以前有學到 k-means clustering,若在一個理想狀況下,最適合的組數就是直接對資料筆數開根號。目前我們的資料有 35萬筆,所以開根號後為 591.x,我就先取個 591 組來試試看。
目前使用的 djb2 的 output 為 unsigned int,我們利用 hash value 取餘數的方式來分組,創立了一個 hashTable 的 array,而 index 為 hash value 除以 HASH_TABLE_SIZE 的餘數。該 array 為其中一組符合某 index 的 entry。根據 entry 的特性,可以利用 linklist 把整串符合某 index 的 entry 通通串起來。感覺大概如下圖:

那怎麼樣建立這個 hashTable 呢?將資料帶入 hashTable 是由 append 方法處理的。先利用 hash function 求出 hash value,並取餘數求出 hashIndex。而放入資料的方式,類似 stack 的方式,放在 hashTable 裡面的 entry,是最後一筆放進去的資料,如果又有心的資料要進來,則需要進行 push。概念如附圖。
```=
entry *append(char lastName[], entry *e)
{
// save the new entry first
entry *newEntry = (entry *) malloc(sizeof(entry));
strcpy(newEntry->lastName, lastName);
// calculate the hash value
unsigned int hashIndex = djb2Hash(lastName);
// check whether the hashTable is empty, create or lineup
if(hashTable[hashIndex] == NULL) {
hashTable[hashIndex] = newEntry;
hashTable[hashIndex]->pNext = NULL;
} else {
newEntry->pNext = hashTable[hashIndex];
hashTable[hashIndex] = newEntry;
}
return newEntry;
}
```

接著就是進行 findName(),找尋名字的動作。基本上就是根據 hashIndex 先到某組資料的頭,開始依序的去找尋組內的名字比對。後續流程跟原始的 findName() 相似
```=
entry *findName(char lastName[], entry *pHead)
{
// choose which hashtable to find
unsigned int hashTarget = djb2Hash(lastName);
entry *probe = NULL;
if ( hashTable[hashTarget] == NULL ) {
return NULL;
} else {
probe = hashTable[hashTarget];
}
while ( probe != NULL ) {
if (strcasecmp(lastName, probe->lastName) == 0)
return probe;
probe = probe->pNext;
}
return NULL;
while (pHead != NULL) {
if (strcasecmp(lastName, pHead->lastName) == 0)
return pHead;
pHead = pHead->pNext;
}
return NULL;
}
```
### Reference
+ [函數的概念](https://market.cloud.edu.tw/content/junior/math/ch_yl/content/math4_1.html)
+ [密碼系統介紹](http://120.105.184.250/cswang/thit/Security/slides/密碼系統.ppt)
+ [djb2](http://www.cse.yorku.ca/~oz/hash.html)
---
# Result
## Original
+ size of entry : 136 bytes
+ average execution time of append() : 0.070686 sec
+ average execution time of findName() : 0.006691 sec
```
perf stat --repeat 100 \
-e cache-misses,cache-references,instructions,cycles \
./phonebook_orig
```
```
Performance counter stats for './phonebook_orig' (100 runs):
2,019,235 cache-misses # 91.440 % of all cache refs ( +- 0.19% )
2,208,271 cache-references ( +- 0.15% )
261,401,917 instructions # 1.37 insns per cycle ( +- 0.04% )
191,014,931 cycles ( +- 0.28% )
0.099477194 seconds time elapsed
```
## Reduce Structure Size
+ size of entry : 32 bytes
+ average execution time of append() : 0.054300 sec
+ average execution time of findName() : 0.003689 sec
```
perf stat --repeat 100 \
-e cache-misses,cache-references,instructions,cycles \
./phonebook_opt
```
```
Performance counter stats for './phonebook_opt' (100 runs):
345,704 cache-misses # 61.357 % of all cache refs ( +- 0.20% )
563,430 cache-references ( +- 0.26% )
244,249,489 instructions # 1.71 insns per cycle ( +- 0.03% )
142,634,916 cycles ( +- 0.15% )
0.080214917 seconds time elapsed ( +- 2.47% )
```
## Hash Function
+ size of entry : 32 bytes
+ average execution time of append() : 0.083085 sec
+ average execution time of findName() : 0.000000 sec
```
perf stat --repeat 100 \
-e cache-misses,cache-references,instructions,cycles \
./phonebook_hash
```
```
Performance counter stats for './phonebook_hash' (100 runs):
295,119 cache-misses # 73.568 % of all cache refs ( +- 0.07% )
401,152 cache-references ( +- 0.38% )
244,445,915 instructions # 1.63 insns per cycle ( +- 0.04% )
149,928,975 cycles ( +- 0.51% )
0.085473493 seconds time elapsed ( +- 2.30% )
```
## PLOT

以結果論,雖然 findName() 的速度上,幾乎是不用花到任何時間。但在一開始建立 hashTable 時需要耗費較多的時間成本。故總耗費時間成本為 append() 時間 + N * findName() 時間,N 為搜尋名字的次數,N 越大才能彰顯出 hash function 方式的威力。
---
## different dataset (老師課堂解說 待改善)
+ 目前的 dataset 是依照字母排序的,不符合現在真實情況。
+ 未來要新增名字刪減功能,若有用到 hash function,可能會產生碰撞 (hash collision)。
---
# Reference
+ [舊版筆記](https://embedded2016.hackpad.com/HW-01-QX2MTGoK0j7)