---
tags: Linux2020
---
# 2020q3 Homework3 (dict)
contributed by < `YLowy` >
## 修改 dict 中錯誤
[bloom.c 的錯誤更正](https://hackmd.io/@YLowy/HkyD5pqBv#%E5%B0%8D%E6%96%BC-bloomc-%E9%80%B2%E8%A1%8C%E9%8C%AF%E8%AA%A4%E6%9B%B4%E6%AD%A3)
> [sysprog21/dict](https://github.com/sysprog21/dict) 已引入修正,請 git merge 或 rebase
> :notes: jserv
## Code Overview
開始前先解讀 dict 程式碼
### test_common.c
- [ ] main()
```c=
int main(int argc, char **argv)
{
...
int CPYmask = -1;
...
if (!strcmp(argv[1], "CPY") || (argc > 2 && !strcmp(argv[2], "CPY"))) {
CPYmask = 0;
REF = DEL;
printf("CPY mechanism\n");
} else
printf("REF mechanism\n");
...
```
:::warning
:warning: 上述程式碼寫得很差,你大可直接改寫。可參考 [x-compressor](https://github.com/jserv/x-compressor/blob/master/x.c#L59-L71),檢查 `argv[0]` 的名稱,後者就是執行檔的名稱,因此你可以建立 `test-common` 執行檔後,透過 [soft link](https://www.cyberciti.biz/faq/creating-soft-link-or-symbolic-link/) 來建立形如 `test-cpy` 和 `test-ref` 的檔案,再由 C 程式判斷其作用。對照看 [x-compressor/Makefile](https://github.com/jserv/x-compressor/blob/master/Makefile#L17-L19)
:notes: jserv
:::
CPYmask 預設為 0xFFFFFFFF
若是對此程式傳入參數 " CPY " 會將 CPYmask 值帶入成 0x00000000
並在之後讀檔案 cities.txt 之後, t1 紀錄當下時間
```c=61
t1 = tvgetf();
```
首先建立 bloom filter table
```c=63
bloom_t bloom = bloom_create(TableSize);
```
如果以 REF 機制會建立一個 memory pool , 並且讓 Top 指向該 pool 首位
```c=67
if (CPYmask) {
/* memory pool */
pool = (char *) malloc(poolsize * sizeof(char));
Top = pool;
}
```
再來做基本的資料處理
```c=74
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] == ',');
}
...
```
要解讀這裡首先要先觀察 cities.txt 內容
```
Shanghai, China
Buenos Aires, Argentina
Mumbai, India
Mexico City, Distrito Federal, Mexico
Karachi, Pakistan
İstanbul, Turkey
Delhi, India
Manila, Philippines
Moscow, Russia
...
```
:::danger
注意:不是「每一行」格式都相同,作業說明已提及例外狀況。
:notes: jserv
:::
~~我們可以發現每一行中的格式為 "城市","空格""國家"~~
...
ASCII " "(空格) -> 0x20
ASCII "\0"(NUL) -> 0x00
...
而這裡想要進行的處理為將該行讀取到的資料 ( buf ) 的逗號、逗號後的空格、跳行取代掉
舉例而言,
`Shanghai, China` 會轉變成 `Shanghai\0China\0`
`Mexico City, Distrito Federal, Mexico` 會轉變成 `Mexico City\0Distrito Federal\0Mexico\0`
所以讀取一行內的資料會穿插 "\0" 變成多組字串形成字串組
所以會用下面的 `while (*Top) {...` 將此字串組餵進 tst 以及 bloom filter 中
並讓 Top 指向轉換後資料的首位
```c=81
while (*Top) {
if (!tst_ins_del(&root, Top, INS, REF)) { /* fail to insert */
fprintf(stderr, "error: memory exhausted, tst_insert.\n");
fclose(fp);
return 1;
}
bloom_add(bloom, Top);
idx++;
int len = strlen(Top);
offset += len + 1;
Top += len + 1;
}
Top -= offset & ~CPYmask;
memset(Top, '\0', WORDMAX);
}
```
取得資料後,會將該資料餵進 tst_ins_del(...) 以及 bloom_add(...)
idx 為總加入單字數
offset 指派為 該資料長度 + 1 以計算位移
Top 為 pointer type ,用意為讓 Top 能夠指向 " 下一筆欲加入處理過後資料的位置 "
這邊還要解釋 CPY REF 兩種機制在此處不同的處理 :
1. REF 機制中 CPYmask 為 0xFFFFFFFF
在`Top -= offset & ~CPYmask;` 中
Top = Top - offset & 0x00000000
Top 並不會作修正
先前提到 REF 架構時候會建立一個 memory pool 並讓將位址指派給 Top
Top 會成為 memory pool 中紀錄下一筆資料的起始位址
2. CPY 機制中 CPYmask 為 0x00000000
在`Top -= offset & ~CPYmask;` 中
Top = Top - offset & 0xFFFFFFFF
Top 會成為原先的指向 word 的位址
```c=36
char word[WRDMAX] = "";
```
之後兩者都在該 Top 指標指向空間往後 WORDMAX 大小做資料清空,全部帶入"\0", 並重複動作讓所有 cities.txt 的資料寫入 tst 以及 bloom filter
全部做完之後會寫入到第二個時間 t2
```c=96
t2 = tvgetf();
```
t2 - t1
此處可以知道選用方法花費多少時間建立 ternary_tree
在 main 函式中的 tst_ins_del(&root, Top, INS, REF) 也必須了解
在一開始的 Enumeration 中宣告
```c=15
enum { INS, DEL, WRDMAX = 256, STKMAX = 512, LMAX = 1024 };
```
其中 INS , DEL 用來帶進 tst_ins_del 的參數
後面的 REF 在 main程式碼中用來表示為 CPY 或者 REF 機制
```c=49
REF = DEL;
```
我個人不喜歡這樣的的表達,應該要有更容易解讀方式實作
### tst.c
- [ ] tst_ins_del
```c=277
void *tst_ins_del(tst_node **root, const char *s, const int del, const int cpy)
{
int diff;
const char *p = s;
tst_stack stk = {.data = {NULL}, .idx = 0};
tst_node *curr, **pcurr;
```
diff 用來表示 加入字串的當時ASCII - 字元當前 node 的ASCII數值 (似乎可以省略這個變數?)
創建一個 stack stk (為何指標陣列大小要給 2 倍的WORDMAX?)
```c=225
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 */
```
檢查輸入是否合理
```c=230
pcurr = root; /* start at root */
while ((curr = *pcurr)) { /* iterate to insertion node */
diff = *p - curr->key; /* get ASCII diff for >, <, = */
if (diff == 0) { /* if char equal to node->key */
if (*p++ == 0) { /* check if word is duplicate */
if (del) { /* delete instead of insert */
(curr->refcnt)--; /* decrement reference count */
/* chk refcnt, del 's', return NULL on successful del */
return tst_del_word(root, curr, &stk, cpy);
} else
curr->refcnt++; /* increment refcnt if word exists */
return (void *) curr->eqkid; /* pointer to word / NULL on del */
}
pcurr = &(curr->eqkid); /* get next eqkid pointer address */
} else if (diff < 0) { /* if char less than node->key */
pcurr = &(curr->lokid); /* get next lokid pointer address */
} else { /* if char greater than node->key */
pcurr = &(curr->hikid); /* get next hikid pointer address */
}
if (del)
tst_stack_push(&stk, curr); /* push node on stack for del */
}
```
考慮到目前使用到 INSERT , 先預想 del = 0 可以把上述程式碼簡化成容易理解版本:
```c=230
pcurr = root;
while ((curr = *pcurr)) {
diff = *p - curr->key;
if (diff == 0) {
if (*p++ == 0) {
curr->refcnt++;
return (void *) curr->eqkid;
}
pcurr = &(curr->eqkid);
} else if (diff < 0) {
pcurr = &(curr->lokid);
} else {
pcurr = &(curr->hikid);
}
}
```
很容易看得出curr 會指向當前 tst 中正要開始做當下字串 INSERT 的 node
```c=257
for (;;) {
/* allocate memory for node, and fill. use calloc (or include
* string.h and initialize w/memset) to avoid valgrind warning
* "Conditional jump or move depends on uninitialised value(s)"
*/
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 (!*root) /* handle assignment to root if no root */
*root = *pcurr;
/* Place nodes until end of the string, at end of stign allocate
* space for data, copy data as final eqkid, and return.
*/
if (*p++ == 0) {
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;
}
}
pcurr = &(curr->eqkid);
}
```
上面會對於接下來要加入的字元建立並且新增該 tst_node 空間,並且在讀取到最後一個字元時 ( 也就是該字元的下一個字元為 '\0' ) 時檢查是 CPY 或者 REF 機制
如果是 CPY 機制以 strdup 複製一個等同 s 指向的字串,並指派給 eqkid
如果是 REF 機制則直接把該字串的 pointer (也就是 s) 指派給 eqkid
為什麼要那麼做?原因 :
#### [分飾多角的 eqkid](https://hackmd.io/@sysprog/2020-dict#%E5%88%86%E9%A3%BE%E5%A4%9A%E8%A7%92%E7%9A%84-eqkid)
Linux man page 對於 [strdup](https://man7.org/linux/man-pages/man3/strdup.3p.html) 的描述:
> The strdup() function returns a pointer to a new string which is a duplicate of the string s. Memory for the new string is obtained with malloc(3), and can be freed with free(3).
查 C11 規格書其實沒有講到 strdup() ,後來發現這其實並非規範在 ISO C standard 中,仰賴實作,以 malloc() 在 heap 中建立副本。
當在 Insert 新的單字的時候,會一路 malloc 建立一條 tst_node list 並在最後的 eqkid 指向一個 malloc 的字串。
在 Insert 像是 "Pneumonoultramicroscopicsilicovolcanoconiosis" 這種很難出現分支的單字,基本上在 Heap 中的 tst_node 以及最後的 eqkid 指向的空間會建立在一起。
- [ ] tst_del_word
這邊程式碼非常長,而且 branch 行為非常多,如果不從脈絡以及大綱開始看會很難看懂
這段程式碼的核心行為是對於每一種 del 前提條件下進行調整
主要行為分為兩種 :
1. 刪掉該點之後如何接上不該被刪掉的節點
2. 接上節點時候要考量的 Rotate 行為
以下拆成 5 個最大部分進行,當該 "tst_node" 成為欲刪除目標時候考量行為
```cpp
static void *tst_del_word(tst_node **root,
tst_node *node,
tst_stack *stk,
const int freeword)
{
tst_node *victim = node; /* begin deletion w/victim */
tst_node *parent = tst_stack_pop(stk); /* parent to victim */
if (!victim->refcnt) { /* if last occurrence */
if (!victim->key && freeword) /* check key is nul */
free(victim->eqkid); /* free string (data) */
/* remove unique suffix chain - parent & victim nodes
* have no children. simple remove until the first parent
* found with children.
*/
while (!parent->lokid && !parent->hikid && !victim->lokid &&
!victim->hikid) {
// A Part
}
/* check if victim is prefix for others (victim has lo/hi node).
* if both lo & hi children, check if lokid->hikid present, if not,
* move hikid to lokid->hikid, replace node with lokid and free node.
* if lokid->hikid present, check hikid->lokid. If not present, then
* move lokid to hikid->lokid, replace node with hikid free node.
*/
if (victim->lokid && victim->hikid) { /* victim has both lokid/hikid */
// B Part
} else if (victim->lokid) { /* only lokid, replace victim with lokid */
// C Part
} else if (victim->hikid) { /* only hikid, replace victim with hikid */
//D Part
} else { /* victim - no children, but parent has other children */
//E Part
}
} else /* node->refcnt non-zero */
printf(" %s (refcnt: %u) not removed.\n", (char *) node->eqkid,
node->refcnt);
return victim; /* return NULL on successful free, *node otherwise */
}
```
#### A Part
最值觀的刪除條件,無後顧之憂所以我全都要刪
![](https://i.imgur.com/huz7hrO.gif)
#### B Part
有了左右兩個子節點時,就要連同考慮該節點刪除後該如何接,rotate 方式其實與資料結構中的紅黑數相似,不過這邊要考慮三個節點,所以程式碼看起來比較多
![](https://i.imgur.com/65hcHjJ.gif)
#### C Part
直接將父節點的 eqkid 指向自己的子節點即可
![](https://i.imgur.com/x9yW1mp.gif)
#### D Part
同 C Part ,不過改成hikid
![](https://i.imgur.com/8jZUaYm.gif)
#### E Part
自身無子節點,可以父節點有其他的子節點,勢必要讓其中一個取代自己成為父節點指向的 eqkid ,這邊也要考慮 rotate 問題所以程式碼看起來很大
![](https://i.imgur.com/6RegFOF.gif)
### bloom.c
觀察 bloom filter 所進行之行為
在 加入單字前會先建立 bloom filter table
資料結構如下:
test_common.c
`bloom_t bloom = bloom_create(TableSize);`
```graphviz
digraph A {
rankdir=L;
a [label="bloom" shape = circle]
b [shape=record fontsize=24 label="{<fun>func|<bits>bits|<sizes>size = 5000K}"]
c [shape=record fontsize=24 label="{bitTable}"]
d [shape=record fontsize=24 label="{function pointer = \"djb2\"|<next>next}"]
e [shape=record fontsize=24 label="{function pointer = \"jenkins\"|<next>next}"]
null [label="Null" shape = circle]
a->b
b:fun->d
b:bits->c
d:next->e
e:next->null
}
```
再來會對該單字進行 Hash 提並且記錄在 bitmap 中
`bloom_add(bloom, Top);`
其對應方式如下
```c=83
void bloom_add(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;
bits[hash >> 3] |= 0x40 >> (hash & 7);
h = h->next;
}
}
```
由於用 bitmap 紀錄,先找到此對應的 byte 位置
bits[hash >> 3] :
```graphviz
digraph A{
rankdir=LR;
b [shape=record fontsize=24 label="{ 0| 0| 0| 0| 0| 0| 0| 0 }"]
}
```
hash>>3
hash & 7 => hash & 00000111
hash 值會剩下後 3 bit ,也就是該 byte 之 offset (000 ~ 111) 也就是 0 到 7
再來對該bytes 進行 " or " 操作紀錄 Hash map
|= 0x40 >> (hash & 7) ... 對該 bit 進行 0 -> 1 操作
```graphviz
digraph A{
rankdir=LR;
b [shape=record fontsize=24 label="{ 0|1| 0| 0| 0| 0| 0| 0 }"]
}
```
為何是 0x40 ?? 應該是 0x80 (0b10000000) 才對
### 對於 bloom.c 進行錯誤更正
早在一開始測試自己的輸入時就發現這個問題,有些原本記錄在 tst 中的資料並沒有能被找到, bloom filter 只會有 "顯示有但事實上沒有" 的錯誤情況,如果 bloom filter 沒能找到之後也不會再 tst 中找尋
```c=168
if (bloom_test(bloom, word)) {
t2 = tvgetf();
printf(" Bloomfilter found %s in %.6f sec.\n", word, t2 - t1);
printf(
" Probability of false positives:%lf\n",
pow(1 - exp(-(double) HashNumber /
(double) ((double) TableSize / (double) idx)),
HashNumber));
t1 = tvgetf();
res = tst_search(root, word);
t2 = tvgetf();
if (res)
printf(" ----------\n Tree found %s in %.6f sec.\n",
(char *) res, t2 - t1);
else
printf(" ----------\n %s not found by tree.\n", word);
} else
printf(" %s not found by bloom filter.\n", word);
```
在inputdata.txt:
```
ap, apple, applepen, applepencil,Penum
```
make 完之後會發現他載入5個單字
選用 serach ap
結果會找到 4 個單字
```
choice: s
find words matching prefix (at least 1 char): ap
ap - searched prefix in 0.000001 sec
suggest[0] : ap
suggest[1] : apple
suggest[2] : applepen
suggest[3] : applepencil
```
find ap :+1:
```
choice: f
find word in tree: ap
Bloomfilter found ap in 0.000001 sec.
Probability of false positives:0.000000
----------
Tree found ap in 0.000000 sec.
```
find apple :-1:
```
Commands:
a add word to the tree
f find word in tree
s search words matching prefix
d delete word from the tree
q quit, freeing all data
choice: f
find word in tree: apple
apple not found by bloom filter.
```
全部單字中只有 apple 並未能找到
我懷疑是 bloom filter 這裡寫錯了
對 bloom_add, bloom_test 進行修改 0x40 ->0x80
```c=83
void bloom_add(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;
bits[hash >> 3] |= 0x80 >> (hash & 7);
h = h->next;
}
}
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] & (0x80 >> (hash & 7)))) {
return false;
}
h = h->next;
}
return true;
}
```
remake 之後以相同的 inputdata 再試一次
```
Commands:
a add word to the tree
f find word in tree
s search words matching prefix
d delete word from the tree
q quit, freeing all data
choice: f
find word in tree: apple
Bloomfilter found apple in 0.000002 sec.
Probability of false positives:0.000000
----------
Tree found apple in 0.000001 sec.
```
成功找到問題點,果然是這邊的問題
### GDB 實驗
新增 testinput.txt :
```
ap, apple
apple, Pneum
```
修改 test_common.c :
```c=32
#define IN_FILE "testinput.txt"
```
```
$ make clean
$ make
```
```
$ gdb -q ./test_common
```
```
Reading symbols from ./test_common...
(gdb) b main
Breakpoint 1 at 0x185f: file test_common.c, line 35.
(gdb) r CPY
Starting program: /home/ylowy/linux2020/dict/test_common CPY
Breakpoint 1, main (argc=0, argv=0x0) at test_common.c:35
35 {
(gdb) until 97
CPY mechanism
main (argc=2, argv=0x7fffffffe518) at test_common.c:97
97 fclose(fp);
(gdb) print t1
$1 = 1601403009.8308451
(gdb) print t2
$2 = 1601403009.8309977
```
執行到 tst 建立完成, t1 , t2 時間
```
(gdb) ptype tst_node
type = struct tst_node {
char key;
unsigned int refcnt;
struct tst_node *lokid;
struct tst_node *eqkid;
struct tst_node *hikid;
}
(gdb) print sizeof(tst_node)
$39 = 32
```
>先前提到的 alignment 問題
sizeof(char) = 1
sizeof(unsigned int) = 4
sizeof(tst_node *) = 8
```
(gdb) p *root
$3 = {key = 97 'a', refcnt = 1, lokid = 0x55555555ca90,
eqkid = 0x55555555c930, hikid = 0x0}
(gdb) p root
$4 = (tst_node *) 0x55555555c900
```
```
(gdb) print *(char*)(root->eqkid->eqkid->eqkid)
$11 = 97 'a'
(gdb) print *(char*)(root->eqkid->eqkid->eqkid+1)
$12 = 112 'p'
(gdb) print *(char*)(root->eqkid->eqkid->eqkid+2)
$13 = 0 '\000'
```
可以清楚了解第一個字串 " ap " 建立過程,以及在 " key = '\0' " node 建立的 eqkid指向字串空間 " 'a' , 'p' , '\0' "
```
(gdb) print*root
$7 = {key = 97 'a', refcnt = 1, lokid = 0x55555555ca90, eqkid = 0x55555555c930, hikid = 0x0}
(gdb) print root
$8 = (tst_node *) 0x55555555c900
(gdb) print root->eqkid
$11 = (struct tst_node *) 0x55555555c930
(gdb) print *(root->eqkid)
$12 = {key = 112 'p', refcnt = 1, lokid = 0x0, eqkid = 0x55555555c960, hikid = 0x0}
(gdb) print root->eqkid->eqkid
$13 = (struct tst_node *) 0x55555555c960
(gdb) print *(root->eqkid->eqkid)
$14 = {key = 0 '\000', refcnt = 1, lokid = 0x0, eqkid = 0x55555555c990, hikid = 0x55555555c9b0}
```
在 55 c9 00 中建立 root (32 bytes tst_node)
第一個 97 代表 'a' (char) -> 3 個 byte 是沒用的
第二個 1 代表 'refcnt' (unsigned int)
第三四個 1431685776 21845 ( 10進位的 lokid address ) -> 0x55555555ca90
第五六個 1431685424 21845 ( 10進位的 eqkid address ) -> 0x55555555c930
第七八個 0 0 ( 10進位的 hikid address ) -> 0x000000000000
`0x55555555c920: 0 0 49 0`
:::info
~~這行不確定存放甚麼 不過也~~說明就算是接著 malloc() 也不會把記憶體位置連續配置在一起
答案: 當在使用 malloc 要求 N bytes 空間的時候,會在 Heap 中配置 N+4 的空間,並把前4個 byte 寫入該配置空間大小,回傳第 5 個位址。
這也是為何 free() 不需要空間大小,只需要位移 -4 取得 malloc 時期配置空間。
:::
root -> eqkid 指向的紀體空間 ( ASCII('p') = 112 )
`0x55555555c930: 112 1 0 0`
依序搜索下去
```
0x55555555c960: 0 1 0 0
0x55555555c970: 1431685520 21845 1431685552 21845
```
1431685520 21845 ( 0x55555555c990 ) -> 該 tst_node 的 eqkid 由於是尾端('\0') 該eqkid 會指向字串
```
0x55555555c900: 97 1 1431685776 21845
0x55555555c910: 1431685424 21845 0 0
0x55555555c920: 0 0 49 0
0x55555555c930: 112 1 0 0
0x55555555c940: 1431685472 21845 0 0
0x55555555c950: 0 0 49 0
0x55555555c960: 0 1 0 0
0x55555555c970: 1431685520 21845 1431685552 21845
0x55555555c980: 0 0 33 0
0x55555555c990: 28769 0 0 0
0x55555555c9a0: 0 0 49 0
0x55555555c9b0: 112 1 0 0
0x55555555c9c0: 1431685600 21845 0 0
0x55555555c9d0: 0 0 49 0
0x55555555c9e0: 108 1 0 0
0x55555555c9f0: 1431685648 21845 0 0
0x55555555ca00: 0 0 49 0
0x55555555ca10: 101 1 0 0
```
對於該 0x55555555c990 找尋
```
(gdb) print *(char*)(0x55555555c990)
$15 = 97 'a'
(gdb) print (char*)(0x55555555c990)
$16 = 0x55555555c990 "ap"
```
可以發現跟我們實作的內容相同
textinput.txt 在 Heap 空間中可以確定是字串是建構在該 list 下一個
```
(gdb) print (char*)(root->lokid->eqkid->eqkid->eqkid->eqkid->eqkid->eqkid)
$25 = 0x55555555cbb0 "Pneum"
```
再來試試看 REF 機制
`(gdb) r REF`
版面緣故直接找到尾端 node ,而 REF 架構中 eqkid 會指向遙遠的 0x7fffd9390010
```
(gdb) p *(root->eqkid->eqkid)
$6 = {key = 0 '\000', refcnt = 1, lokid = 0x0, eqkid = 0x7fffd9390010, hikid = 0x55555555c990}
```
該 process 的 Heap 到 Stack 的距離
以上是使用 GDB 觀察 test_common CPY 以及 REF 機制的 Insert Data.txt 行為的分析,剛好回顧一下 GDB 操作
## 在 GitHub 上 fork dict,視覺化 ternary search tree + bloom filter 的效能表現並分析。注意,需要設計實驗,允許不同的字串輸入並探討 TST 回應的統計分佈
```
./test_common --bench CPY
```
在測試程式中有這樣提到 bench_test()
test_common.c
```c=103
if (argc == 3 && strcmp(argv[1], "--bench") == 0) {
int stat = bench_test(root, BENCH_TEST_FILE, LMAX);
tst_free(root);
free(pool);
return stat;
}
```
呼叫到的程式碼主要如下,從 cities 中的每個單字取前 3 個字元做搜尋,並且判斷其時間分布
bench.c
```c=47
while (fscanf(dict, "%s", word) != EOF) {
if (strlen(word) < sizeof(prefix) - 1)
continue;
strncpy(prefix, word, sizeof(prefix) - 1);
t1 = tvgetf();
tst_search_prefix(root, prefix, sgl, &sidx, max);
t2 = tvgetf();
fprintf(fp, "%d %f msec\n", idx, (t2 - t1) * 1000);
idx++;
}
```
這邊可以定義要每個單字的前幾個字元做比對
```c=9
#define PREFIX_LEN 3
```
### 實驗一 根據原先程式 bench.c 中測量 tst_prefix_search 程式
以前 3 個字元做 suggest 測量時間
照題目所敘述存在此段差異原因為 cache miss
bench cpy
![](https://i.imgur.com/kKTtXL9.png)
bench ref
![](https://i.imgur.com/wXARN9x.png)
觀察輸出圖發現,兩機制在搜尋上所花費時間其實差不了多少,但是 CPY 機制可以讓花費最大時間壓在 0.7 左右,並且相對穩定。
再來思考這樣的實驗的合理性,實驗一在於給定 "一定可以找到" `tst_search_prefix(root, prefix, sgl, &sidx, max);`的前提上做實驗
### 實驗二 為了量測 bloom filter 的效能表現的 tst_search 量測
CPY :
![](https://i.imgur.com/Gf5vfsa.png)
REF :
![](https://i.imgur.com/upYZvV4.png)
想法為量測單次執行時間,但是這並不是很好的量測實驗,一來要考量到 tst_search 的 input ,二來是時間過於短,應該要不影響輸入資料差距情況下測量多次測量以觀察時間。
### 實驗三
實驗資料其實想挺久的,後來我的作法為直接引入新的 cities2.txt ,並把裡面一個字元換成另一個字元。
實驗中進行 100 次測試,每次測試會把比對資料 (cities.txt, cities2.txt, cities3.txt...) 內的單詞一一進行比對並且統計總時間。
這樣比起隨機輸入測試好處是不會存在 "sdsddede " , "ewrwed" 這種毫無意義的測試,並起透過計算知道輸入的資料錯誤率,達到相對的驗證。
第一個測試我讓 cities.txt 裡面的 `'C'` 更換為 `'B'`,像是單辭 China 會被替換為 Bhina 。
而以下都是以 CPY 機制進行的實驗。
```
ternary_tree, loaded 206849 words in 0.157690 sec
C word count : 17126
```
error rate : 8.279469564 %
citiesChange.py
```python=
import os
def alter(file,old_str,new_str):
with open(file, "r") as f1,open("cities2.txt", "w+") as f2:
for line in f1:
if old_str in line:
line = line.replace(old_str, new_str)
f2.write(line)
alter("cities.txt", "C", "B")
```
先從原本資料 cities.txt 進行比對,由於每個單辭都確定是可以找到的,我們可以預期再增加 bloom filter 之後時間花費會提升,畢竟增加了 hash 計算以及一些分支行為。
而以下也是為我們預期結果,再確定都可以找到情況下,幒測量時間上升了 15ms 左右。
錯誤率 0% (0/206849)
![](https://i.imgur.com/QvYRSH1.png)
![](https://i.imgur.com/UMpUIve.png)
再來替換`'a'` 該單字再總資料中出現相當頻繁,以提升比對資料錯誤率進行實驗。
我們可以觀察到花費時間還是上升。但是總體時間上升情形不像上個實驗那麼嚴重
錯誤率 8.27% (17126/206849)
![](https://i.imgur.com/vM4022B.png)
![](https://i.imgur.com/thDUE2S.png)
在原先 cities.txt 中,再次替換字元 `'e'` 使錯誤率上升並做觀察。
在有 bloom filter 情形所花費時間相對穩定,卻還是沒有使用 bloom filter 花費時間較低。
錯誤率 74.06% (153206/206849)
![](https://i.imgur.com/8sfxOKq.png)
![](https://i.imgur.com/9DHdvX1.png)
最後替換 `'a'`,`'e'`,`'i'`,`'o'`,讓錯誤率上升至非常高以做觀察,未使用 bloom filter 效率還是比較高,這似乎與我們預先猜想情逛不符合。
錯誤率 98.90% (204590/206849)
![](https://i.imgur.com/Mdk78vx.png)
![](https://i.imgur.com/ozcPNeC.png)
:::success
實驗結果與預期不同,我認為應該是在 tst 中結構不夠大,使得 "實際找尋的成本" 小於 "利用 filter " 判斷的成本,若是能增加此 tst 的內容,應該是可以得到預期結果。
:::
## 以 perf 分析 TST 程式碼的執行時期行為並且充分解讀
### Perf 實驗
`make test`
```
Performance counter stats for './test_common --bench CPY s Tai' (100 runs):
510,9294 cache-misses # 52.157 % of all cache refs ( +- 0.50% )
979,6034 cache-references ( +- 0.12% )
5,8790,6063 instructions # 1.16 insn per cycle ( +- 0.02% )
5,0593,6526 cycles ( +- 0.18% )
0.189083 +- 0.000529 seconds time elapsed ( +- 0.28% )
```
```
Performance counter stats for './test_common --bench REF s Tai' (100 runs):
499,0523 cache-misses # 51.495 % of all cache refs ( +- 0.60% )
969,1313 cache-references ( +- 0.14% )
5,5442,8501 instructions # 1.11 insn per cycle ( +- 0.00% )
4,9995,3329 cycles ( +- 0.22% )
0.186238 +- 0.000442 seconds time elapsed ( +- 0.24% )
```
## 研讀 Xor Filters: Faster and Smaller Than Bloom and Cuckoo Filters 論文並參閱對應程式碼實作 xor_singleheader,引入上述 dict 程式碼中,需要比較原有的 bloom filter 和 xor filter 在錯誤率及記憶體開銷
### 研讀 [Xor Filters](https://arxiv.org/abs/1912.08258)
Bloom filter 為 dict 所使用實作
Cuckoo Filter 可以比 Bloom filter 使用更少的空間以及更快的速度
而本篇的 xor filter 可以有更少的空間需求又更快的速度
U : 一個大箱子放置許多 String
S : 從 U 箱子中取出一些 String
x : S 裡面的一個 element
fingerprint(x) 得到一個 k-bits 值 想像成
```c=
char *x ={'a','p','p','l','e'};
uintk_t myStringfgpt = fingerprint(x);
```
array B : k-bits 值得陣列
大小為 c 個,c 比 |S| 大些(題目設定)
```c=
#define c ⌊1.23 · |S|⌋ + 32
uintk_t B[c];
```
想像B陣列平均成 3 等分
3 個 hash function h0, h1, h2 分別會 hash 到分別三塊上
Mapping
| 陣列 | finger print |
| ---- | ------------ |
| 0 | 00010011... |
| 1 | 00010110... |
| 2 | 01000011... |
| 3 | 10010011... |
| 4 | 01010011... |
| 5 | 01110011... |
| 6 | 00010011... |
| 7 | 00000111... |
| 8 | 00011011... |
只要在 U 這個大箱子裡的 element y, h0(y) 範圍只會在0-2,h1(y) 範圍只會在3-5,h2(y) 範圍只會在6-9
![](https://i.imgur.com/SFgHilV.png)
![](https://i.imgur.com/8rWMqX9.png)
![](https://i.imgur.com/bjICGtA.png)
這裡要注意 H array 和 Q queue
H 可以存放多筆 key 的 array (這裡實作應該是 assign pointer to queue 方式)
S 裡面的元素都帶入之後
Q 先放置第一輪 "單獨 key" 的位置,並且 push 至 Q 中
Q 完成之後再將其中元素依依 pop 出來,再將三個 key 從 H 移除,如果移除後 H[i] 這個位置又只有一個 key 就 push 進 Q 裡,重覆到 H 中沒有任何元素
我的輸出會是一個 Stack σ
![](https://i.imgur.com/OGmJB5C.png)
其實跟著這 4 個演算法走就可以知道在幹嘛了
Algo 4 :
B[i] ← fingerprint(x) ~~xor~~ = B[h0(x)] xor B[h1(x)] xor B[h2(x)]
稍微更正下
XOR FILTERS
B[h0(x)] xor B[h1(x)] xor B[h2(x)] = fingerprint(x)
這裡使用 XOR 操作跟 [HW2q6](https://hackmd.io/@YLowy/BJMA34fHP#%E6%B8%AC%E9%A9%97-6) 有一點點小小相似,都使用到相同的 XOR 特性
### 參閱對應程式碼實作 [xor_singleheader](https://github.com/FastFilter/xor_singleheader),引入上述 dict 程式碼
## 如何縮減頻繁 malloc 帶來的效能衝擊呢?提示: memory pool
## 在現有的開放原始碼專案中,找出使用 bloom filter 的專案,並解析其導入的考量