# 2020q3 Homework3 (dict)
contributed by < `Sisker1111` >
###### tags: `進階電腦系統理論與實作`
[TOC]
## 作業說明及程式碼
> [I04: dict](https://hackmd.io/@sysprog/2020-dict)
> [GitHub](https://github.com/Sisker1111/dict)
## key word
ternary search tree、Bloom filter、bit-wise operation、benchmark、GNU make and Makefile、Valgrind、perf.
# 程式原理
## CPY 與 REF 機制
原始程式中,會根據輸入參數分成 CPY 和 REF 兩種模式,並分別作對應的處理。
```cpp=
if (!strcmp(argv[1], "CPY") || (argc > 2 && !strcmp(argv[2], "CPY"))) {
CPYmask = 0;
REF = DEL;
printf("CPY mechanism\n");
} else
printf("REF mechanism\n");
```
在 REF 模式下,`CPYmask` = -1,因此改配置給一個更大的空間給`Top`。
```cpp
long poolsize = 2000000 * WRDMAX;
enum { INS, DEL, WRDMAX = 256, STKMAX = 512, LMAX = 1024 };
....
if (CPYmask) {
/* memory pool */
pool = (char *) malloc(poolsize * sizeof(char));
Top = pool;
}
```
* 接著會開始讀取`"cities.txt"`這個檔案,根據`,`及`\n`兩個字元去做切割
```cpp=
while (fgets(buf, WORDMAX, fp)) {
int offset = 0;
for (int i = 0, j = 0; buf[i + offset]; i++) {
Top[i] =
(buf[i + j] == ',' || buf[i + j] == '\n') ? '\0' : buf[i + j];
j += (buf[i + j] == ',');
}
while (*Top) {
if (!tst_ins(&root, Top, INS, REF)) {
.
.
}
}
```
* 再來, 呼叫 tst_ins 來插入 tst 的節點,如果是在 CPY 模式下,會透過 strdup 去呼叫 malloc 配置一塊等同於 Input 字串`s`的空間,並複製其內容到`eqdata`,如果是在 REF 模式下,因為已事先為 Top 配置了一個很大的記憶體空間來擺放所有的待搜尋字串,因此只要 refererence 過去即可
```cpp=
if (cpy) { /* allocate storage for 's' */
const char *eqdata = strdup(s);
if (!eqdata)
return NULL;
curr->eqkid = (tst_node *) eqdata;
return (void *) eqdata;
} else { /* save pointer to 's' (allocated elsewhere) */
curr->eqkid = (tst_node *) s;
return (void *) s;
}
```
`Top -= offset & ~CPYmask;`這個機制會判斷是否讓 Top 回到起始位置,在 CPY 模式 Top 會回到 word 的起始位置,REF 模式則保持不變.
最後,我認為 CPY 及 REF 兩種機制應該是在效能與空間成本的一個tradeoff, CPY 機制下雖然要多花 branch 去存取字串,但每次只需配置一定大小的空間, REF 雖然只要 refererence 過去就可以存取該字串,但須先配置一個很大的空間.
## tst_search
計算目前搜尋到的 `prefix = *s` 以及被搜尋的 `node key = cur` 兩個字元的差值並存入`diff`,用來判斷兩個字元是否相等.
* 當`diff == 0` 判斷 `*s`是否是空字元,是的話則 search 完成,否則繼續比對下一個字元,往`eqkid`移動.
* 當`diff < 0` 表示`*s` 比`cur->key`小,往`lokid`移動.
* 當`diff > 0` 表示`*s` 比`cur->key`大,往`hikid`移動.
```cpp=
while (curr) { /* loop over each char in 's' */
int diff = *s - curr->key; /* calculate the difference */
if (diff == 0) { /* handle the equal case */
if (*s == 0) /* if *s = curr->key = nul-char, 's' found */
return (void *) curr->eqkid; /* return pointer to 's' */
s++;
curr = curr->eqkid;
} else if (diff < 0) /* handle the less than case */
curr = curr->lokid;
else
curr = curr->hikid; /* handle the greater than case */
}
```
## tst_ins
原始程式 tst_ins() 中,第一個 while 的目的是要判斷要 insert 的字串是否已經存在 Ternary search tree中:
```cpp=
//tst.c
const char *p = s;
tst_node *curr, **pcurr;
if (!root || !s)
return NULL; /* validate parameters */
if (strlen(s) + 1 > STKMAX / 2) /* limit length to 1/2 STKMAX */
return NULL; /* 128 char word length is plenty */
pcurr = root;
while ((curr = *pcurr)) {
if (*p == 0 && curr->key == 0) {
curr->refcnt++;
return (void *) curr->eqkid;
}
pcurr = next_node(pcurr, &p);
}
```
* `while ((curr = *pcurr))`用來判斷 tst 走到的地方是否還有節點.
* `next_node()`負責把 Insert 字串`*s`和 tst 中的節點比大小,操作和 search 時相同.
* `if (*p == 0 && curr->key == 0)`成立代表`*s`和 tst 都同時走到底並且確立`*s`已經存在 tst 中,並紀錄重複插入的次數(`curr->refcnt++;`),最後直接返還一個 NULL ,在底下這個 test_common 程式中可以看到,`if(res)`成立時才會計算花費的時間.
```cpp=
//test_common.c
if (bloom_test(bloom, Top)) /* if detected by filter, skip */
res = NULL;
else { /* update via tree traversal and bloom filter */
bloom_add(bloom, Top);
res = tst_ins(&root, Top, REF);
}
t2 = tvgetf();
if (res) {
idx++;
Top += (strlen(Top) + 1) & CPYmask;
printf(" %s - inserted in %.10f sec. (%d words in tree)\n",
(char *) res, t2 - t1, idx);
}
```
最後,我們要加剩餘的字元都加進 Ternary search tree 中,一直到`*p`結束為止.
```cpp=
//test_common.c
for (;;) {
if (!(*pcurr = calloc(1, sizeof **pcurr))) {
fprintf(stderr, "error: tst_insert(), memory exhausted.\n");
return NULL;
}
curr = *pcurr;
curr->key = *p;
curr->refcnt = 1;
curr->lokid = curr->hikid = curr->eqkid = NULL;
if (*p++ == 0) {
if (cpy) { /* allocate storage for 's' */
. . .
} else {
. . .
}
}
pcurr = &(curr->eqkid);
}
```
## tst_del_word
透過 `tst_del_word` 將 TST 上的字串進行刪除。
```cpp
tst_node *victim = node;
tst_node *parent = tst_stack_pop(stk);
if (!victim->refcnt) {
if (!victim->key && freeword)
free(victim->eqkid);
while (!parent->lokid && !parent->hikid && !victim->lokid &&
!victim->hikid) {
...
}
```
* `victim` 會從等待被刪除的字串本身開始。
* 如果該 `refcnt` > 0,表示尚有 reference 存在,則不需刪除 。
* `if (!victim->key && freeword) free(victim->eqkid)`,判斷是否為 CPY 模式,若是,則首先釋放字串本身。
* while 迴圈中,如果待移除的 `victim` 和其 `parent` 沒有其他 child,則可以直接釋放。
```cpp
if (victim->lokid && victim->hikid) { /* victim has both lokid/hikid */
if (!victim->lokid->hikid) { /* check for hikid in lo tree */
...
} else if (!victim->hikid->lokid) { /* check for lokid in hi tree */
...
} else /* can't rotate, return, leaving victim->eqkid NULL */
return NULL;
} else if (victim->lokid) { /* only lokid, replace victim with lokid */
...
} else if (victim->hikid) { /* only hikid, replace victim with hikid */
...
} else { /* victim - no children, but parent has other children */
...
```
* 在該移除的候選節點 `victim` 的同時,如果`victim`具有 low child 和 high child,則需要做相對應的處理,這部分的程式碼相對龐大,詳情請見[原始程式碼](https://github.com/Sisker1111/dict):
## 改善 Bloom filter 所用的資源
在 bloom filter 中,我們只需要 1 個 bit 就能表達我們需要的狀態,但 C 語言裡並沒有定義 1 bit 的 Boolean Type,儘管我們只需要 1 bit 但系統最低仍會分配 1 byte 的空間,因此我們會浪費 7/8 的 table 空間,改進方式如下:
```cpp=
bloom_t bloom_create(size_t size)
{
bloom_t res = calloc(1, sizeof(struct bloom_filter));
res->size = size;
//res->bits = malloc(size + 7);
res->bits = calloc((size + 7) >> 3, 1);
bloom_add_hash(res, djb2);
bloom_add_hash(res, jenkins);
return res;
}
```
這樣做的目的是,使得每個 byte 的每個 bit 都是 Bloom filter 的一個 element (意即 bit 和 bit 間是獨立看待的),因此,在搜尋時我們會需要先找到該 hash value 位於哪個 "byte",然後再找出位於哪個 "bit",以下程式在做這件事情:
```cpp=
bool bloom_test(bloom_t filter, const void *item)
{
struct bloom_hash *h = filter->func;
uint8_t *bits = filter->bits;
while (h) {
unsigned int hash = h->func(item);
hash %= filter->size;
if (!(bits[hash >> 3] & (0x40 >> (hash & 7)))) {
return false;
}
h = h->next;
}
return true;
}
```
# Ternary search tree + bloom filter 的效能表現