# 2017q3 Homework2 (prefix-search) contributed by <`Lihikoka`> [GitHub](https://github.com/Lihikoka/prefix-search) ### Reviewed by `ZixinYang` - 讀入資料時, 把最後一個 character 改成 0, 不只會改掉城市的逗號, 也會把國家的最後一個 character 改掉, 所以應先判斷逗號是否存在。 - 缺少程式執行效率的分析。 - 直接將 freeword 改為 0, 對 CPY 版本來說, malloc 記憶體空間之後就不再釋放記憶體了。不應在這裡限制 CPY 版本的操作。 - [ ] chunking ([Locality](https://www.cs.cornell.edu/courses/cs3110/2010sp/lectures/lec24.html)) # Ternary Search Tree 原理 [Trie](https://zh.wikipedia.org/wiki/Trie) (trie 的發明者 Edward Fredkin 把它讀作 [tri],但是其他作者把它讀作 [traɪ]) 是一種特殊的 tree,可以用在前綴搜尋。[Ternary search tree](https://en.wikipedia.org/wiki/Ternary_search_tree) 是 trie 的一種特例,trie 可以在每個節點有任意個子節點,但是 TST 只允許每個節點有三個子節點,且三個子節點分別為:lo kid、equal kid、hi kid * equal kid 所含的字元為 word 本身的字元 * lo kid 所含的字元為比 equal kid 小 (ascii code) 的字元 * hi kid 所含的字元為比 equal kid 大 (ascii code) 的字元 用 [Wikipedia](https://en.wikipedia.org/wiki/Ternary_search_tree) 的例子來解說 (此 TST 包含的 word 有 as、at、cup、cute、he、i 和 us): c / | \ a u h | | | \ t t e u / / | / | s p e i s ### Insertion 1. 為了 insert `cute`, 先建立根節點 `c` ,然後依序將 `u`、`t`、`e` 插入 equal kid 即完成 2. 為了 insert `at`,先把 `at` 的第一個字元 `a` 和根節點 `c` 作比較,發現不一樣且 `a` 比 `c` 小 ,所以在根節點的 lo kid 插入 `a` ;接著在節點 `a` 的 equal kid 插入 `t` 3. 為了 insert `as` ,先把 `as` 的第一個字元 `a` 和 「 根節點 `c` 、跟節點的 lo kid 」 作比較,發現跟 lo kid 一樣,因此繼續往下找;將 `as` 的第二個字元 `s` 和 「 `a` 節點的 equal kid 」 作比較,發現不一樣,又 `a` 節點的 lo kid 為空,因此將 `s` 插入 `a` 的 lo kid 4. 依此類推... ### Search * 為了搜尋 `cute` ,先將根節點和 `cute` 的第一個字元作比較,發現一樣,因此沿著 equal kid 走,將 `cute` 的第二個字元 `u` 和剛剛的 equal kid 作比較,發現又一樣,重複上述步驟,發現 `cute` 的每個字元都走得完,因此找到了 cute。 ### Auto Complete or Prefix Search * 假設輸入的 prefix 為 "cu" ,根據上面的 TST ,走完節點 `c` 、`u` 後有兩種選擇: `cup` 和 `cute` ,因此可將符合 prefix 的 word 列出來,達成 auto complete ,此技術可以用在搜尋引擎、輸入法補字的功能上。 ## 特性分析 TST v.s. Hashing ### Hashing * Need to examine the entire key ( because that is the way the hash function works ) * Search hits and misses cost the same * The running time and performance relues heavily on the hash function * Does not support as much as operations than TST ( sorting ) ### Ternary Search Tree * Works only for strings * Only examines just enough key characters * Search miss may only involve a few characters * Support more operations ( sorting ) * Faster than hashing ( for missis especially ) and flexible than binary search tree # 開發環境 ``` Linux 4.8.0-53-generic Linux Mint 18.2 Cinnamon 64-bit Model name: Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 8192K ``` # 探索程式 首先執行 test_cpy 程式,依照作業說明輸入: ``` choice: s find words matching prefix (at least 1 char): Tain Tain - searched prefix in 0.000005 sec suggest[0] : Tain, suggest[1] : Tainan, suggest[2] : Taino, suggest[3] : Tainter suggest[4] : Taintrux, ``` 可以發現有幾個單字後面帶有 `,` ,原因是讀檔的時候是讀整個字串 ```clike while ((rtn = fscanf(fp, "%s", word)) != EOF) ``` 這樣會造成之後執行 `f` (find) 指令的時候的麻煩 ``` choice: f find word in tree: Tainan Tainan not found. choice: f find word in tree: Tainan, found Tainan, in 0.000002 sec. ``` 為了實驗的方便性,決定修正這個問題,在 while 迴圈內加入 ```clike size_t len = strlen(word); word[len - 1] = 0; ``` 實驗結果 ``` choice: s find words matching prefix (at least 1 char): Tain Tain - searched prefix in 0.000005 sec suggest[0] : Tain suggest[1] : Tainan suggest[2] : Taino suggest[3] : Tainte suggest[4] : Taintrux ``` >同學你好,這樣好像也會把國家的最後一個字也刪掉,變成國家找不到。 [name=yuan922] ## 此次作業的 TST 結構 [Youtube: Ternary search tree introduction](https://youtu.be/xv4oRyqSKiw) 影片中提到插入一個單字時也會插入一個 value 至最後一個字元,例如 `puts('cat', 728)` 即代表會將 728 插入到 't' 對應到的節點的某個欄位上,在此次作業並不是採用插入 value 的方式,而是採用 「 將整個單字的字串插入至最後一個單字的 `eqkid` 的 `eqkid` ,並且不管其原本是 `tst_node` 的資料型態,檢查的時候直接 cast 成 `char*` 」 ,tree 的結構圖如 github 的 README.md 所示: ``` 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" ``` 第一個被插入的 word 為 「 Shanghai 」 ,可用 gdb 確認上述結構: ``` (gdb) p *(root) $16 = {key = 83 'S', refcnt = 1, lokid = 0x605810, eqkid = 0x605690, hikid = 0x606890} (gdb) p *(root->eqkid) $17 = {key = 104 'h', refcnt = 1, lokid = 0x607280, eqkid = 0x6056c0, hikid = 0x607370} (gdb) p *(root->eqkid->eqkid) $18 = {key = 97 'a', refcnt = 1, lokid = 0x61f3a0, eqkid = 0x6056f0, hikid = 0x60d2b0} (gdb) p *(root->eqkid->eqkid->eqkid) $19 = {key = 110 'n', refcnt = 1, lokid = 0x64a420, eqkid = 0x605720, hikid = 0x639950} (gdb) p *(root->eqkid->eqkid->eqkid->eqkid) $20 = {key = 103 'g', refcnt = 1, lokid = 0x780290, eqkid = 0x605750, hikid = 0x61ce50} (gdb) p *(root->eqkid->eqkid->eqkid->eqkid->eqkid) $21 = {key = 104 'h', refcnt = 1, lokid = 0xdade90, eqkid = 0x605780, hikid = 0x62f5d0} (gdb) p *(root->eqkid->eqkid->eqkid->eqkid->eqkid->eqkid) $22 = {key = 97 'a', refcnt = 1, lokid = 0x0, eqkid = 0x6057b0, hikid = 0x0} (gdb) p *(root->eqkid->eqkid->eqkid->eqkid->eqkid->eqkid->eqkid) $23 = {key = 105 'i', refcnt = 1, lokid = 0x0, eqkid = 0x6057e0, hikid = 0x0} (gdb) p *(root->eqkid->eqkid->eqkid->eqkid->eqkid->eqkid->eqkid->eqkid) $24 = {key = 0 '\000', refcnt = 1, lokid = 0x0, eqkid = 0x7ffff30ce020, hikid = 0x0} (gdb) p (char *) (root->eqkid->eqkid->eqkid->eqkid->eqkid->eqkid->eqkid->eqkid->eqkid) $25 = 0x7ffff30ce020 "Shanghai" ``` :::danger 1. 練習透過 GDB scripts,開發出走訪給定 TST 裡頭的元素的自動化方法 2. 參見 [What are the best ways to automate a GDB debugging session?](https://stackoverflow.com/questions/10748501/what-are-the-best-ways-to-automate-a-gdb-debugging-session) 和 [簡易 GDB Script 教學](https://wwssllabcd.github.io/blog/2013/03/25/how-to-write-gdb-scritp/) :notes: jserv ::: 這麼做的好處在於可以確定搜尋到的是一個完整的 word # 修正程式碼 ## 尋找問題 首先將 `test_ref.c` 中註解要求修改之處之 `CPY` 改成 `REF` ,並觀察輸出, ``` 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: s find words matching prefix (at least 1 char): Tain Tain - searched prefix in 0.000005 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 ``` 可以發現 prefix searching 的過程有問題。 ``` choice: f find word in tree: Tain found Tain in 0.000001 sec. choice: f find word in tree: Tainan found Tainan in 0.000001 sec. ``` 可以發現 find 也有問題, 輸入 `q` 的話則會得到: ``` choice: q *** Error in `./test_ref': free(): invalid pointer: 0x00007ffc3e917500 *** ``` 第一行 ```Error in `./test_ref': free(): invalid pointer: 0x00007ffc3e917500``` 告訴我們進行 `free()` 操作時 pointer 無效,原因是 pointer 指到不可 free 的記憶體位址。 觀察 `test_ref.c` 的 `tst_ins_del` 函式,發現第 273 行 ```clike= 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; } ``` 發現如果是 `cpy` 輸入的是 `1` (`CPY`) 的話,會執行 `strdup` ,如果是 `0` (`REF`) 的話則直接將 `curr->eqkid` 指到 `*s` 指到的 address,但是 `*s` 指到的位址的內容是會變的 (隨著每次讀的 word 而改變) ,因此問題就是 : * 「 tree node 指到的記憶體位址是外部的記憶體位址 (`void *tst_ins_del(tst_node **root, char *const *s, const int del, const int cpy)` 裡的 `*s` ) ,因此每次搜尋的時候會輸出給定的搜尋字串 」 但是並沒有分配記憶體給 `curr->eqkid`,解決辦法就是如同註解所說的在其他處宣告。最簡單的解決方式是在 `test_ref.c` 將 `char *p = word` 改成 `char *p = strdup(word)`。 但是 `strdup` 的 [實作過程](https://opensource.apple.com/source/Libc/Libc-186/string.subproj/strdup.c) (連結為 apple 實作 strdup 的 source code) 中會使用 `malloc` ,因此用多了容易有記憶體破碎的問題,記憶體破碎會導致 cache miss 的機會變大 (spatial locality) ,因此決定引入 memory pool ,一次分配一大塊連續的記憶體。 ## 使用連續記憶體 (memory pool) :::danger 1. 需要描述你的「動機」和「考量點」 2. 過程中又參考什麼文獻呢? (至少列出一篇論文) :notes: jserv ::: > 已補上動機、考量點、參考資料,參考資料為 Stack Overflow。[name=Lihikoka] > ### 動機 & 理由 要宣告連續記憶體有多種方法可以實現,可以用 array of pointer 或 memory pool ,這邊選擇 memory pool ,因為每次要分配的記憶體大小都不一樣 (`word` array 的最大 size 雖然都相同,都是 `WRDMAX` ,但是實際要分配的記憶體大小的只有有效字元的部份,這部份小於或等於 `WRDMAX` ) ,因此在此次作業的 case 中採用 array of pointer 會浪費記憶體空間。 ### Memory pool 程式碼 參考 [Stackoverflow](https://stackoverflow.com/questions/11749386/implement-own-memory-pool) 上別人寫的簡易 memory pool : ```clike= pool *pool_create(size_t size) { pool *p = (pool *) malloc(size + sizeof(pool)); p->next = (char *) &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; } void *pool_alloc(pool *p, size_t size) { if (pool_available(p) < size) return NULL; void *mem = p->next; p->next += size; return mem; } ``` 將 `p = strdup(word)` 改寫成 ```clike p = (char *) pool_alloc(mpool, sizeof word); strcpy(p, word); ``` Command `q` 的地方要改寫成 ```clike pool_destroy(mpool); tst_free(root); ``` 另外,觀察 `tst.c` 內 `tst_ins_del` function 內呼叫 `tst_del_word` 之處: ```clike tst_del_word(root, curr, &stk, 1); ``` 最後一個 parameter 1 代入 argument `freeword`,對應到 `tst_del_word` 的註解: ``` /** if 'freeword = 1' the copy of word allocated and * stored as the node->eqkid is freed, if 'freeword = 0', node->eqkid is * stored elsewhere and not freed */ ``` 若 `freeword = 1` ,會 free 掉記憶體, `freeword = 0` 則不會,在 memory pool 的 case 比較適合此種方法,因為記憶體是配置在 memory pool 內,由於使用的 memory pool API 並沒有包含釋放記憶體 (此處的釋放指的是對 memory pool 而言為 available) ,所以 `freeword` 應該傳入 `0` 。 # 參考資料 * [Youtube: Ternary search tree introduction](https://youtu.be/xv4oRyqSKiw)