# 2017q3 Homework2 (prefix-search) contributed by<`kevin550029`> ## Ternary Search Tree 基本介紹 :::danger 1. 用「自己理解的認知」,改寫以下描述 2. 技術報告的訓練,就是強化思考和資料彙整的能力,日後才可有基礎做研究 :notes: jserv ::: * 不同於 trie 的結構, 每個 node 只有三個 pointer * TST 的 1 個 node 中有包含 5 個 component, 如圖所示 ![](http://www.geeksforgeeks.org/wp-content/uploads/Ternary-Search-Tree.png) 1. data field: 儲存 node 的內容 1. EndOfString field: 用來指出該 node 是否為一個字串的最後一個元字元 1. Left Pointer: 指向值小於自己的 child 1. Equal Pointer: 指向值等於自己的 child 1. Right pointer: 指向值大於自己的 child ## 測試環境 ``` Linux version 4.4.0-96-generic Ubuntu 16.04.3 LTS L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 6144K CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 4 On-line CPU(s) list: 0-3 Thread(s) per core: 1 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 94 Model name: Intel(R) Core(TM) i5-6400 CPU @ 2.70GHz ``` ## 測試 prefix-search 程式碼 ``` 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 ``` * Insertion a 可以將字元加入樹中 ``` choice: a enter word to add: test_city_kevin test_city_kevin - inserted in 0.000008 sec. (259113 words in tree) ``` * Find f 可判斷某個字元是否在樹中 ``` choice: f find word in tree: test_city_kevin found test_city_kevin in 0.000006 sec. find word in tree: xxxxx_ooooo xxxxx_ooooo not found. ``` * Search prefix s 將含有指定開頭字串的字元找出來 ``` choice: s find words matching prefix (at least 1 char): Taiw Taiw - searched prefix in 0.000009 sec suggest[0] : Taiwala, suggest[1] : Taiwan ``` 這邊觀察到有些城市名中會帶有逗號 舉例來說,若直接輸入Taiwala, 而不是 `Taiwala,` 會回傳 Taiwala not found * delete 刪除功能要在 refcnt 為零的時候才能成功刪除 ``` choice: d enter word to del: Taiwan deleting Taiwan Taiwan (refcnt: 18) not removed. delete failed. ``` ## 修改 test_ref.c 開始針對 test_ref.c 中 `FIXME` 的部分作修改 ```clike= /* FIXME: insert reference to each string */ res = tst_ins_del(&root, &p, INS, CPY); ``` 將原本使用的 CPY 改為 REF, 並使用 s 搜尋 prefix: `Amer`, 結果如下 ``` choice: s find words matching prefix (at least 1 char): Amer Amer - searched prefix in 0.000018 sec suggest[0] : Amer suggest[1] : Amer suggest[2] : Amer suggest[3] : Amer suggest[4] : Amer ``` 合理結果應該是要列出以 `Amer` 為 prefix 的所有字元 但這邊都輸出成 Amer, 推測原因可能是所有 node 都存取到同一個記憶體位置 使用 q 時, 也會發生錯誤 ``` choice: q *** Error in `./test_ref': free(): invalid pointer: 0x00007ffebbfe5dd0 *** ``` * 觀察 tst_ins_del() ```clike= /* 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); ``` 發現使用 cpy 時,會執先行 strdup() strdup()會先用maolloc()配置和 s 字符串相同的空間大小 然後將參數 s 字符串的内容複製到該地址,然后把該地址回傳 而使用 ref 的話, 則直接將 curr->eqkid 指到 s 指到的 address 但是 s 指到的位址內容應該要隨著每次讀的 word 而改變 * 修改方法 在 test_ref.c 分配位置給 word 時, 使用 strdup() ```clike= char *p = strdup(word); /* 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; } ``` * 重新執行 同樣使用 s 搜尋 prefix: `Amer`, 發現已可正常執行 ``` choice: s find words matching prefix (at least 1 char): Amer Amer - searched prefix in 0.000022 sec suggest[0] : Amerang, suggest[1] : Amerdingen, suggest[2] : America, suggest[3] : American suggest[4] : Americana, suggest[5] : Americus, suggest[6] : Amerongen, suggest[7] : Amersfoort, suggest[8] : Amersham suggest[9] : Amersham, suggest[10] : Amery, ``` * 修改後 q 一樣可正常執行, 因為 strdup() 用 maolloc() 來配置記憶體, 可以使用 free() 來釋放 ## 效能評估&比較 參考 [st9007a](https://hackmd.io/s/SkeKQEq3Z) & [tina0405](https://hackmd.io/JwDhDMAYHYGMBYC0sDMliPgNgIa0QEbgBMKiWkWF4KWArBQIxA==#分析資料) 同學的共筆設計測試方法 * 設計bench_test Function ```clike= char word[SAMPLES][WRDMAX]; //儲存要拿來做搜尋的 prefix char prefix[3] = ""; char *sgl[LMAX] = {NULL}; int sidx=0, count=0; //從 cities.txt 中取出一些城市名稱, 拿來做搜尋的 prefix //SAMPLES 可以指定要拿出多少筆, 這邊設成 30000 FILE *prefixF = fopen(IN_FILE,"r"); for (int i=0; i < SAMPLES; i++) { fscanf(prefixF, "%s", word[i]); } fclose(prefixF); double t1, t2; FILE *fp = fopen(output_file,"w"); //輸出時間結果用的檔案 for (int i=0; i < SAMPLES; i++) { //取出在word中, 每個城市的前三個字元, 當作準備要搜尋的 prefix if (strlen(word[i]) < 3) continue; strncpy(prefix, word[i], 3); // 執行 prefix search 並計算時間 t1 = tvgetf(); tst_search_prefix(root, prefix, sgl, &sidx, LMAX); t2 = tvgetf(); fprintf(fp, "%d %.6f sec\n", count, t2 - t1); count++; //紀錄第幾筆 search } fclose(fp); ``` * 利用gnuplot繪圖 關於gnuplot語法, 參考以下幾個網站 [GT Chen's Blog: Gnuplot](http://gtchen.pixnet.net/blog/post/5873441-gnuplot-%28part-i%29), [Gnuplot 簡單數據繪圖](https://ithelp.ithome.com.tw/articles/10158860) 用 3000 個長度為 3 的 prefix 來做搜尋得到的結果 ![](https://i.imgur.com/dKxeSWl.png) 縮小圖的範圍,在輸出一次 ![](https://i.imgur.com/jl20wgD.png) 光看上圖很難看出明顯的分別 所以再去計算個別的平均值和標準差 可以發現在平均搜尋時間上, CPY 花的時間較少 ``` CPY Version Mean: 0.000067 Standard Deviation:0.000076 ``` ``` REF version Mean: 0.000072 Standard Deviation:0.000083 ``` 兩個方法的cache-misses的情況相差不多 ``` CPY Version Performance counter stats for 'sudo chrt -f 99 taskset -c 0 ./test_cpy --bench' (100 runs): 2080 cache-misses # 33.556 % of all cache refs ( +- 1.44% ) 6197 cache-references ( +- 0.78% ) 3,5200 instructions # 0.42 insns per cycle ( +- 0.48% ) 8,4727 cycles ( +- 1.30% ) 0.852326170 seconds time elapsed ( +- 0.93% ) ``` ``` REF Version Performance counter stats for 'sudo chrt -f 99 taskset -c 0 ./test_ref --bench' (100 runs): 2082 cache-misses # 33.628 % of all cache refs ( +- 1.36% ) 6192 cache-references ( +- 0.86% ) 3,5289 instructions # 0.42 insns per cycle ( +- 0.48% ) 8,3771 cycles ( +- 0.96% ) 0.863542987 seconds time elapsed ( +- 0.28% ) ``` * 引入 Memory pool 方法 ## 作業要求 > 「作業要求」放在超連結就好,不需要先貼出來,這樣是拿不到 P 幣的。 > Show me the code! > [name=jserv"][color=red] > got it > 先貼在這邊方便自己查看, 完成後會移除 > [name="kevin550029"] * 在 GitHub 上 fork [prefix-search](https://github.com/sysprog21/prefix-search),並修改 `Makefile` 以允許 `$ make bench` 時,可對 ternary search tree 的實作做出效能評比,涵蓋建立 tst 和 prefix search 的時間,應比照 [clz](https://hackmd.io/s/Hyl9-PrjW) 透過統計模型,取出 95% 信賴區間 * 測試程式碼應該接受 `--bench` 的執行參數,得以在沒有使用者輸入的前題下,對 tst 程式碼進行效能分析,應涵蓋 `cpy` 和 `ref` 兩種途徑 (詳情參閱 `tst.h` 的程式碼註解) * 解釋 tst 的優點和引入 tst 到電話簿或戶政管理系統的適用性 * 修改 [test_ref.c](https://github.com/sysprog21/prefix-search/blob/master/test_ref.c),參照裡頭標註 `TODO` 的部分,替換成真正有效的 API 和對應的程式碼,這裡要求你學習分析現有的程式碼並得以擴充 * 分析 `test_cpy` 和 `test_ref` 兩種方式 (copy/reference) 的效能,並且針對現代處理器架構,提出效能改善機制 * 指出目前實作中,針對各國城市的 prefix search 有無缺陷呢?比方說無法區分城市和國家,並提出具體程式碼的修正 * 引入 [phonebook](https://hackmd.io/s/HJJUmdXsZ) 的基礎工作,透過 [ternary search tree](https://en.wikipedia.org/wiki/Ternary_search_tree) 讓電話簿程式得以更符合人性,當使用者新增資料時,可以在居住地透過 prefix search 找出最接近的城市並避免輸入錯誤 * 詳細觀察 `tst.h` 和 `tst.c` 為何要 forward declaration 呢?分離介面和實作有何好處呢?這樣的思維又該如何運用於 [phonebook](https://hackmd.io/s/HJJUmdXsZ) 呢?提出想法並且實作 * 研究程式碼的 `tst_traverse_fn` 函式,並思考如何運用這個實作,注意到 [callback function](https://en.wikipedia.org/wiki/Callback_(computer_programming)) 的程式開發技巧/模式 * 將你的觀察、上述要求的解說、應用場合探討,以及各式效能改善過程,善用 gnuplot 製圖,紀錄於「[作業區](https://hackmd.io/s/HkVvxOD2-)」 ## 參考資料 [Ternary Search Tree](http://www.geeksforgeeks.org/ternary-search-tree/) [Ternary Search Tree (Deletion)](http://www.geeksforgeeks.org/ternary-search-tree-deletion/)