# 2018q1 Homework2 (prefix-search)
contributed by <`t5i0m7`>
## 開發環境
Architecture: x86_64
CPU 作業模式: 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
每核心執行緒數: 2
每通訊端核心數: 2
Socket(s): 1
NUMA 節點: 1
供應商識別號: AuthenticAMD
CPU 家族: 21
型號: 16
Model name: AMD A10-5800K APU with Radeon(tm) HD Graphics
製程: 1
CPU MHz: 1400.000
CPU max MHz: 3800.0000
CPU min MHz: 1400.0000
BogoMIPS: 7599.92
虛擬: AMD-V
L1d 快取: 16K
L1i 快取: 64K
L2 快取: 2048K
NUMA node0 CPU(s): 0-3
```
## Ternary search tree
這次的重點放在ternary search tree這個資料結構該怎麼做insert、delete、search等行為,並透過了解其演算法修改prefix-search裡的code,並比較裡面code兩種不同資料儲存分配方法的效能。
* Ternary search tree 視覺化
透過共筆上提供的線上[視覺化工具](https://www.cs.usfca.edu/~galles/visualization/TST.html)和網路上的一些資料像是[維基百科](https://en.wikipedia.org/wiki/Ternary_search_tree)可以了解到這個資料結構是怎麼做insert和search的動作。
```
o
|-c
|-0
------------+------------
|lokid |eqkid |hikid
o o o
|-a
|-0
----+----
| | | note: any of the lokid or hikid nodes
o can also have pointers to nodes
|-t for words that "cat" or "ca" is
|-0 a partial prefix to.
----+----
| | |
o
|-0
|-1 <== the refcnt is only relevant to the final node
----+----
| | |
NULL o NULL
"cat"
```
上述圖是由prefix-search裡的readme提供,當要搜尋一串字串時,字母由第一個字進入root node並與node裡的key進行比較,相等、大於、小於會去進入對應的node,噹當有相等的情形發生,將會比對字串下一個字,直到結尾字元。如果直到結尾字元都有直表示此字串在這個ternary search tree裡是存在的,並在eqkid的指標填入字串位置。
* TST node 結構
以這次prefix search tree一個Ternary search tree node裡的element包含四個東西,key、refcnt、hikid、lokid、edkid。
:::info
key -> 每個node裡都會存一個character當作key來搜尋
refcnt -> 當final word時就會為1
lokid -> 比目前node key level還低的key的node
hikid -> 比目前node key level還高的key的node
eqkid -> 當符合Key下一個接的node
:::
:::warning
在insert的function裡這段code不太符合insert的方法,在readme有提到refcnt為1的情況是此node的key為結尾字元
```clike
curr = *pcurr;
curr->key = *p;
curr->refcnt = 1;
curr->lokid = curr->hikid = curr->eqkid = NULL;
```
由上code可看出在每次新增節點時都會令refcnt為1,不過基本上不影響code的運行,因為在判斷字串結尾時已經不是用refcnt來進行判斷,從search function可以知道判斷結尾已改成判斷自尾相減為0且字尾字元為結尾字元
:::
## test_ref裡的BUG
首先要更改存在test_ref裡Fix me的地方,看到tst\_ins\_del(&root, &p, INS, CPY);可以想到跟傳進去的參數CPY有關,畢竟這個程式的命名就跟REF有關於是我直接改成tst\_ins\_del(&root, &p, INS, CPY)
```
choice: s
find words matching prefix (at least 1 char): Tain
Tain - searched prefix in 0.000011 sec
suggest[0] : Tain
suggest[1] : Tain
suggest[2] : Tain
suggest[3] : Tain
suggest[4] : Tain
suggest[5] : Tain
suggest[6] : Tain
suggest[7] : Tain
suggest[8] : Tain
suggest[9] : Tain
suggest[10] : Tain
suggest[11] : Tain
```
很悲劇的,程式能用但結果跟test_cpy差異很大,於是再細看tst\_ins\_del這條function裡寫了啥,有可能是REF這個參數的變化改變了哪些條件,於是找到下方的code,這是tst\_ins\_del裡的一段
```clike
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;
}
}
```
這段code指的是當字串要插入最後一個結尾字元發生的事,可以看出用CPY可以決定eqkid接的完整字串複製方式。當使用CPY參數時,是直接allocate一段空間來塞入字串,而使用REF參數值是將讀到的字串首位reference到eqkid,但這個讀進來的字串fscanf存入的變數位置的是一樣的,導致每個有結尾字串node的eqkid的reference都相同,這也影響到prefix-search的function。
* 解決方法
在test_ref讀入程式資料時使用static array來儲存資料,reference到的資料都有一塊空間能使用,為了方便之後比較我使用,我將字串word改為二維矩陣char word\[CITY_NUM\]\[WRDMAX\],其中最大字串大小為128、城市資料大小為30000筆,這樣就能解局Fix Me區域內的Bug,實際運作為下
```
choice: s
find words matching prefix (at least 1 char): Ame
Ame - searched prefix in 0.000015 sec
suggest[0] : Ameca,
suggest[1] : Americana,
suggest[2] : Amersfoort,
suggest[3] : Ames,
```
當用先宣告一個二為矩陣用來去存之後會存入的字串資料,要delete資料時矩陣裡會發生fragmentation,後來想想這相當於是寫一個Memory pool,不過這種我也是之後才知道這叫Memory pool,用這種寫法要再寫處理fragmentation的function,而這部份我打算使用一條linked list來儲存被刪掉資料的位置,並在改寫insert function,當這條linked list不為empty則優先選linked list底下的位置填入。
## 資料讀入格式問題
* 用於test_ref的prefix search function
```
choice: s
find words matching prefix (at least 1 char): Ame
Ame - searched prefix in 0.000015 sec
suggest[0] : Ameca,
suggest[1] : Americana,
suggest[2] : Amersfoort,
suggest[3] : Ames,
```
整個test_ref裡寫的功能都不能用,裡面的資料都是亂的甚麼都找不到
:::info
這跟資料讀入有關,忘了後面還有未處理的標點符號問題,所以我再重新看raw data,發現有幾個如果用原本的程式會發生的問題
* 城市與國家沒分清楚
* 空格影響到城市
:::
各國的城市有分層級,而會有從國家->州區/省區->城市,所以我這裡測試一慮使用最小分層的城市,也就是第一個逗號以前的字串為城市的資料。
* 修改code讓讀入的城市不因空格與國家字串干擾
```clike
for ( ; (rtn = fgets(word[idx], WRDMAX , fp) != NULL ); idx++) {
rmcrlf(word[idx]);
rmcomma(word[idx]);
char *p = word[idx];
/* FIXME: insert reference to each string */
if (!tst_ins_del(&root, &p, INS, REF)) {
fprintf(stderr, "error: memory exhausted, tst_insert.\n");
fclose(fp);
return 1;
}
// city num too big checj
if(idx > 30000)
break;
}
```
* 更改後結果
```
choice: s
find words matching prefix (at least 1 char): Ame
Ame - searched prefix in 0.000042 sec
suggest[0] : Ameca
suggest[1] : Amecameca de Juárez
suggest[2] : Amelia
suggest[3] : American Canyon
suggest[4] : American Fork
suggest[5] : Americana
suggest[6] : Americus
suggest[7] : Amersfoort
suggest[8] : Amersham
suggest[9] : Amersham on the Hill
suggest[10] : Ames
suggest[11] : Amesbury
suggest[12] : Amet
suggest[13] : Amethi
```
從結果可以看出prefix search的結果更加正確,尾端部會有逗號,也不會因為空格跑出一些奇怪的結果或是該有的城市跑不出來。
## Ternary search tree eqkid覆蓋問題
```c
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;
}
}
```
:::warning
上述這段code取至 tst_ins_def 函式裡,依照上面寫的,在找到字串有結尾字元之後,在其node的eqkid接上原始字串,但假如ABC與AB這兩個字串來講AB不會洗掉要接到C的eqkid嗎?
如下圖表示,ABC接下的eqkid會被之後AB的eqkid所覆蓋
:::
<img width="200" height="200" align="left" src="https://i.imgur.com/cdrokFj.png" >
<img width="180" height="180" align="right" src="https://i.imgur.com/LA5t5pl.png" >
:::info
在之後我了解到結尾字元也要算在Ternary search tree裡面,當時只想到字串比較沒想到還缺比較結尾字元,而真實情況應該如下圖(O代表結尾字元)
:::
<img width="180" height="180" align="right" src="https://i.imgur.com/P5pS2Pz.png" >
<img width="180" height="180" align="right" src="https://i.imgur.com/NSNPRB8.png" >
## REF CPY效能比較
我的測試方式是重複做一百次測試,並將每次create tst的時間跟search tst的時間都記錄並分別計入在個別的檔案,而底下兩個是perf統計的狀況。
* test_cpy repeat 100 times
```
146,358 cache-misses # 9.906 % of all cache refs ( +- 0.67% ) (75.83%)
1,477,486 cache-references ( +- 0.55% ) (53.62%)
99,146,525 instructions # 0.82 insn per cycle ( +- 0.19% ) (76.90%)
120,918,081 cycles ( +- 0.48% ) (75.05%)
0.035312134 seconds time elapsed ( +- 1.01% )
```
* test_ref repeat 100 times
```
229,856 cache-misses # 12.994 % of all cache refs ( +- 0.67% ) (72.74%)
1,768,879 cache-references ( +- 0.54% ) (55.81%)
96,545,321 instructions # 0.70 insn per cycle ( +- 0.33% ) (79.31%)
138,133,805 cycles ( +- 0.23% ) (74.77%)
0.040373533 seconds time elapsed ( +- 1.10% )
```