2017q3 Homework1 (phonebook)
===
contributed by <`Lihikoka`>
- [x] Memory leak
- [x] Structure size
- [x] Memory pool
- [x] Hash function (BKDR Hash)
- [ ] 不同資料結構
# 開發環境
```
DRAM: 16GB
CPU(s): 8
Core(s) per socket: 4
CPU: Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz
L1d cache: 32K
L1i cache: 32K
L2 cache: 256K
L3 cache: 8192K
```
# 原始程式分析
## Memory Leak
在 `main.c` 最後面釋放記憶體的程式碼,但只釋放了第一、二個 entry 。
``` clike=
if (pHead->pNext) free(pHead->pNext);
free(pHead);
```
改寫成
```clike=
while (pHead) {
e = pHead;
pHead = pHead->pNext;
free(e);
}
free(pHead);
```
可將所有 entry 釋放掉,已解決 memory leak。
## Structure Size
`phonebook_orig.h` 中原始 phone book 的 structure:
``` clike=
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;
```
* 這個 structure 佔用的記憶體大小為 `136 Bytes`,我的系統的 L1 cache 大小為 32 KB ,因此 L1 cache 中能存 `32 * 1024 / 136 = 240.94` 筆 entry。
* 改進方式: 在 `phonebook_orig.c` 的 `findName` 函式中,可以看到搜尋的依據是 `lastName`,其他的項對於搜尋沒有幫助,因此可以考慮使用較小的 structure ,例如把 `lastName` 以外的項利用一個 pointer to structure 指到的 struct 存起來,如此一來 entry 的大小即降為 32 Bytes 。
```clike=
typedef struct __PHONE_BOOK_DETAIL {
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;
} phonebook_detail;
typedef struct __LAST_NAME_ENTRY {
char lastName[MAX_LAST_NAME_SIZE];
phonebook_detail *detail;
struct __LAST_NAME_ENTRY *pNext;
} entry;
```
## 實驗結果
執行 `sudo make cache-test`
未優化:
```
Performance counter stats for './phonebook_orig' (100 runs):
235,5270 cache-misses # 95.170 % of all cache refs ( +- 0.08% )
247,4805 cache-references ( +- 0.10% )
3,2287,3113 instructions # 1.43 insn per cycle ( +- 0.02% )
2,2621,0953 cycles ( +- 0.26% )
0.062875512 seconds time elapsed ( +- 0.97% )
```
縮小 structure size:
```
Performance counter stats for './phonebook_opt' (100 runs):
35,7711 cache-misses # 65.760 % of all cache refs ( +- 0.37% )
54,3961 cache-references ( +- 0.44% )
2,8347,0947 instructions # 1.89 insn per cycle ( +- 0.01% )
1,4989,4403 cycles ( +- 0.19% )
0.044388147 seconds time elapsed ( +- 1.57% )
```
可以看到 cache-misses 減少了 `(95.170 - 65.760) / 95.170 = 30.1%`

`append` 的時間少了 19.2% , `findName` 的時間少了 59.5% 。
# Memory Pool
Memory pool 是一種記憶體管理的方式,每一次呼叫 malloc 系統就會分配一塊記憶體給 process ,但是 malloc 、 free 太多次的話很容易產生零碎的記憶體片段,導致下一次如果要 malloc 一塊較大的記憶體時可能會失敗,而使用 memory pool 的話則是一次先 malloc 一大塊記憶體,然後再自行管理。
參考 [Stackoverflow](https://stackoverflow.com/questions/11749386/implement-own-memory-pool) 上別人寫的簡易版 memory pool 並將其特化成適合此次作業的版本
```clike=
typedef struct __POOL {
entry *next;
entry *end;
} pool;
entry *append_with_mempool(char lastName[], entry *e, pool *p)
{
e->pNext = pool_alloc(p, sizeof(entry));
if (NULL == e->pNext) return NULL; // memory pool is full
e = e->pNext;
strcpy(e->lastName, lastName);
e->pNext = NULL;
return e;
}
pool *pool_create(size_t size)
{
pool *p = (pool*) malloc(size + sizeof(pool));
p->next = (entry*) &p[1];
p->end = p->next + size;
return p;
}
void pool_destroy(pool *p)
{
free(p);
}
size_t pool_available(pool *p)
{
return p->end - p->next;
}
entry *pool_alloc(pool *p, size_t size)
{
if (pool_available(p) < sizeof(entry)) return NULL;
entry *mem = (entry*) p->next;
p->next += size;
return mem;
}
```
其中 `pool_alloc` 函式的 argument `size` 是需要謹慎選擇的,如果所需要分配的記憶體超過了一開始分配給 memory pool 的記憶體,則會發生 segmentation fault (core dump) ,處理的方式比較麻煩,在本次實作中並沒有考慮處理機制,因此 `size` 的值必須夠大。
本次實作中為了動態配置 memory pool 或 hash tabel (有關 hash 的部份下一部份會介紹) 的大小有用一個 `count_file_line` 函式來計算 file 的行數,程式碼如下:
```clike=
static unsigned int count_file_line(FILE* fp)
{
unsigned int num_line = 0;
char line[MAX_LAST_NAME_SIZE];
while (fgets(line, sizeof(line), fp)) num_line++;
rewind(fp);
return num_line;
}
```
並將 `size` 設為 `行數 + redundant` ,其中 `redundant` 設為 `500`
但是改成使用 memory pool 之後卻在 `append` 10940 個 entry 之後發生 segmentation fault,將 `pool->end` 和 每一次的 `e` 作比較發現 `e` 沒有超過 `pool->end` ,所以推斷不是分配給 memory pool 的記憶體大小不夠大,詳細原因待查明。
## 2017/10/15 新增
與 `ChuiYiTang` 同學討論過後,發現 memory pool API 的 pool_alloc 有問題,問題出在 ```p->next += size;``` ,因為 p->next 的型態為 `entry*`,`entry` 的大小為 `32 bytes` ,因此對 `entry*` 作加法時是以 `sizeof(entry)` 為單位,也就是說我以為加上了 `size` ,其實是加上了 `size * sizeof(entry)` 因此 `pool->next` 增加的比預期的還快 32 (`sizeof(entry)`) 倍,所以很快就超過分配的 memory pool 記憶體範圍,因此發生 segmentation fault (實在是不夠懂 C 語言啊!) ,實驗過程如下:
```
(gdb) p pool_alloc(pPool, sizeof(entry))
$1 = (void *) 0x7fffe2440020
(gdb)
$2 = (void *) 0x7fffe2440420
```
可以看到 `pool->next` 一次增加了 $(400)_{16}=4\times256=1024$ Bytes,預期只有 32 Bytes,修正程式碼如下:
```clike=
entry *pool_alloc(pool *p, size_t size)
{
if (pool_available(p) < size) return NULL;
void *mem = p->next;
p->next = (void *) p->next + size;
return mem;
}
```
把第 5 行改成如上所示,把 `p->next` 強制轉型成 `void*` ,因為 `sizeof(void)` 為 1 Byte ,這樣就能讓行為跟預期的一樣了:
```
(gdb) p pool_alloc(pPool, sizeof(entry))
$1 = (void *) 0x7fffe2440020
(gdb)
$2 = (void *) 0x7fffe2440040
```
一次加了 32 Bytes ,符合預期。
```
perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./phonebook_mempool
Performance counter stats for './phonebook_mempool' (100 runs):
27,7214 cache-misses # 69.563 % of all cache refs ( +- 0.19% )
39,8508 cache-references ( +- 0.82% )
2,5219,5433 instructions # 1.59 insn per cycle ( +- 0.02% )
1,5846,5443 cycles ( +- 0.25% )
0.043636574 seconds time elapsed ( +- 0.88% )
```

可以發現使用 memory pool 一次分配大塊的連續記憶體的話,`append` 的時間可以降低 40 % 的時間,原因是因為多次使用 `malloc` 的話容易造成記憶體破碎,spatial locality 不好,進而導致 cache miss 增加,但是用 perf 分析出來的結果卻與推論不符合,原因待查證。
# Hash
## Hash 的原理
假設我們要做搜尋,我們可以維護一個 hash table ,裡面紀錄著 `key` 與 `value` 的對應關係,`key` 是資料本身,`value` 則是將所有可以搜尋的資料餵給一個叫作 hash function 的函式的輸出 (hash value)。
由以上我們可以得出搜尋的過程為:
1. 將待查資料放進 hash function
2. 將 hash function 的輸出當 index 來找出 hash table 的值,若找的到即成功,找不到則失敗。
`hash_value = hash(data), hash_table[hash_value] 存在 or 不存在`
理想上用 hash 可以將搜尋的時間複雜度降為 O(1)。
但是也有可能兩個輸入值得到同一個 hash value ,這時就會產生 collision (碰撞) ,解決 collision 問題的方法有兩種:開放定址法 (open addressing)、鍵結法 (chaining)。
### 開放定址法 (open addressing)
線性探測 (linear probing) 、二次探測 (quadratic probing)
在 append hash table 時,一個資料經過 hash function 運算得到已經出現出現過的 key 值時,我們則將這個 `key` 作「調整」,而調整的方式根據 probing 的方法會有不同的結果:
* Linear probing:
遇到碰撞時就把 $key+1$ ,若還是遇到碰撞就再 $+1$ ,依此類推。
* Quadratic Probing:
遇到碰撞就把 $key+1^2$ ,若再遇到則改成 $+2^2$ ,遇到第 $i$ 次碰撞則 $+i^2$。
使用 linear probing 會使得有連續一坨填滿的 bucket ,這樣會使得搜尋需要花很多的時間來跨越這些填滿的部份,會造成效能上的損失。
### 鍵結法 (chaining)
若遇到碰撞就在該 bucket 後加上一個 linked list ,若 hash value 相同則都去那個 linked list 查找 / append。
### 實驗 & 結果
此次作業採用的 hash function 是 BKDR function ,處理碰撞的方法是 open addressing 的 quadratic probing , hash table 的 loading factor 是 0.75
```clike=
unsigned int BKDRhash(char *str)
{
unsigned int seed = 131;
unsigned int hash = 0;
while (*str)
hash = hash * seed + (*str++);
return (hash & 0x7FFFFFFF);
}
entry *findName_hash(char lastName[], entry *ht[], int ht_size)
{
unsigned int hash_value = BKDRhash(lastName) % ht_size;
if (NULL == ht[hash_value]) return NULL;
for (int i = 0; i < ht_size; i++)
if (!strcmp(lastName, ht[(hash_value + i * i) % ht_size]->lastName))
return ht[(hash_value + i * i) % ht_size];
return NULL;
}
void append_hash(char lastName[], entry *ht[], int ht_size)
{
unsigned int hash_value = BKDRhash(lastName) % ht_size;
for (int i = 0; i < ht_size; i++) {
if (NULL == ht[(hash_value + i * i) % ht_size]) {
ht[(hash_value + i * i) % ht_size] = (entry*) malloc(sizeof(entry));
strcpy(ht[(hash_value + i * i) % ht_size]->lastName, lastName);
break;
}
}
}
```
跑 `perf` 看結果:
```
$perf stat --repeat 100 -e cache-misses,cache-references,instructions,cycles ./phonebook_hash
Performance counter stats for './phonebook_hash' (100 runs):
55,8906 cache-misses # 45.902 % of all cache refs ( +- 0.50% )
121,7606 cache-references ( +- 0.23% )
3,2939,1985 instructions # 1.27 insn per cycle ( +- 0.03% )
2,5951,7427 cycles ( +- 0.40% )
0.073122489 seconds time elapsed ( +- 0.99% )
```
cache-misses 降到 45.902%
HackMD 有時候怪怪的,明明就貼圖了可是卻顯示不出來

`append` 的時間增加了快兩倍,但是 `findName` 的時間如預期的非常低。