# 2018q3 Homework3 (dict) ###### tags: `GOGOGOGOGOGOG` # 作業目標 * 在 GitHub 上 fork [dict](https://github.com/sysprog21/dict),研讀並修改 `Makefile` 以允許 `$ make plot` 時,視覺化 ternary search tree + bloom filter 的效能表現。注意,需要設計實驗,允許不同的字串輸入並探討 TST 回應的統計分佈 * 要考慮到 prefix search 的特性還有詞彙的涵蓋率 * 限用 gnuplot 來製圖 * 參閱 [Hash Table Benchmarks](http://incise.org/hash-table-benchmarks.html) * 以 perf 分析 TST 程式碼的執行時期行為並且充分解讀 * 考慮到 CPY 和 REF 這兩種實作機制,思考效能落差的緣故,並且提出後續效能改善機制 * 充分量化並以現代處理器架構的行為解讀 * 注意:這兩者都該加入 Bloom filter,而且分析具體效益 (原有程式存在若干缺陷,也該指出並修正) ## 事前準備 - 首先先將perf進行安裝,輸入`sudo apt-get install linux-tools-common `系統便會開始自行安裝,但要注意的點是安裝的檔案版本不同的問題,老師所給的指令`$ sudo apt-get install linux-tools-3.16.0-50-generic linux-cloud-tools-3.16.0-50-generic `其版本不一定適用於每個人,需要根據提示字元進行安裝。 - 由於這次需要自行設定makefile所以需要開啟權限,透過指令` sudo sh -c " echo -1 >/proc/sys/kernel/perf_event_paranoid" ` 來開啟權限。 - 可以參考這篇 https://hackmd.io/s/Skwp-alOg# 非常詳盡的解說了gnuplot的使用,但這次作業scripts已經都寫好了,需要更改的部份不多。 - 關於如何編譯Makefile: https://hackmd.io/c/rJKbX1pFZ/https%3A%2F%2Fhackmd.io%2Fs%2FSySTMXPv # 了解Trie和Ternary search tree (TST) ## Trie Trie ,又叫做前綴樹或是字典樹,是一個動態配置或關聯性配置的資料結構,用來保存關聯數組,常用在 string。 而當運用在 string 時, Trie 中每個節點由一個字元組成。 * Trie樹的基本性質可以歸納為: * 根節點不包含字元,除了根節點外的每個節點只包含一個字元。 * 從根節點到某一個節點,路徑上經過的字符連接起來,為該節點對應的字符串。 * 每個節點的所有子節點都有相同的前綴字串。 * 不是所有的節點都有對應的值,只有葉子節點和部分內部節點所對應的 key 才有相關的值。 * Trie 的應用: 主要在 bioinformatics , information retrieval ## Ternary Search Tree (TST) 介紹 Trie 樹的結構,它的實現簡單但空間效率低。如果要支持26個英文字母,每個節點就要保存26個指針,假如還要加入標點符號、區分大小寫,內存用量就會急劇上升,以至於不可行。 因為 trie 的節點數組中保存的空指針,佔用了太多內存,因此考慮改用其他數據結構去代替,像是用 hash map。但是管理成千上萬個 hash map 不是什麼好主意,而且它還會使數據的相對順序信息丟失,所以使用 Ternary Tree 替代 trie。 * Binary Search Tree 查詢與建表時間是 O(log2n),但是在最糟的情況下時間複雜度可能轉為 O(n),除了要儲存的字串外不須另外分配空間。 * Trie 的時間複雜度是 O(n),能夠實現前者難以做到的 prefix-search,缺點是會有大量的空指針表,造成空間開銷過大。 * Ternary Search Tree,是一種 trie(亦稱 prefix-tree) 的結構,並且它結合字典樹的時間效率和 BST 的空間效率優點。 * 每個節點 (Node) 最多有三個子分支,以及一個 Key * Key 用來記錄 Keys 中的其中一個字元 (包括 EndOfString)。 * low :用來指向比當前的 Node 的 Key 小 的 Node。 * equal :用來指向與當前的 Node 的 Key 一樣大 的 Node。 * high :用來指向比當前的 Node 的 Key 大 的 Node。 * Ternary Search Tree 的應用: * spell check: Ternary Search Tree 可以當作字典來存儲所有的單詞。一旦在編輯器中輸入單詞,可以在Ternary Search Tree中並行搜索單詞以檢查正確的拼寫。 * Auto-completion: 使用搜索引擎進行搜索時,它會提供自動完成(Auto-complete)功能,讓用戶更加容易查找到相關的信息;假如:我們在Google中輸入ternar,它會提示與ternar的相關搜索信息。 ![](https://images0.cnblogs.com/blog/183411/201212/30214221-c8318b20976d4b4da3f68bad9d1ffaf1.png) Google根據我們的輸入ternar,提示了ternary,ternary search tree等等搜索信息,自動完成(Auto-complete)功能的實現的核心思想三叉搜索樹。 對於Web應用程序來說,自動完成(Auto-complete)的繁重處理工作絕大部分要交給服務器去完成。很多時候,自動完成(Auto-complete)的備選項數目巨大,不適宜一下子全都下載到客戶端。相反,三叉樹搜索是保存在服務器上的,客戶端把用戶已經輸入的單詞前綴送到服務器上作查詢,然後服務器根據三叉搜索樹算法獲取相應數據列表,最後把候選的數據列表返回給客戶端。 ![](https://images0.cnblogs.com/blog/183411/201212/30214224-1fdcd259d4fa4b398d2a5a1fa50edb30.png) reference: [Ternary Search Tree 的應用] (http://www.cnblogs.com/rush/archive/2012/12/30/2839996.html) ## TST的實作程式碼: 首先一樣的fork本次的作業,並且進行`git clone https://github.com/GOGOGOGOGOGOG/dict.git `但要注意的點是本次作業除了要`git init ` 以外,需要先安裝pref等等套件才可以執行。 - 了解Makefile: ``` TESTS = test_cpy test_ref TEST_DATA = s Calva CFLAGS = -O0 -Wall -Werror -g ... test: $(TESTS) echo 3 | sudo tee /proc/sys/vm/drop_caches; perf stat --repeat 100 \ -e cache-misses,cache-references,instructions,cycles \ ./test_cpy --bench $(TEST_DATA) perf stat --repeat 100 \ -e cache-misses,cache-references,instructions,cycles \ ./test_ref --bench $(TEST_DATA) bench: $(TESTS) @for test in $(TESTS); do\ ./$$test --bench $(TEST_DATA); \ done output.txt: test calculate ./calculate plot: output.txt bench_cpy.txt bench_ref.txt gnuplot scripts/runtime.gp eog runtime.png gnuplot scripts/runtime3.gp eog runtime3.png gnuplot scripts/runtimept.gp eog runtime2.png calculate: calculate.c $(CC) $(CFLAGS_common) $^ -o $@ clean: $(RM) $(TESTS) $(OBJS) $(RM) $(deps) rm -f bench_cpy.txt bench_ref.txt ref.txt cpy.txt output.txt caculate -include $(deps) ``` 前半部份為: * `TESTS ` 是進行test_cpy和test_ref的程式碼,也就是進行make後的所產生的執行碼。 * `TEST_DATA `測試的字串,原本為 Tai 但未來滿足作業需求進行實驗,我改成了Calva,下面會解釋實驗流程。 * `CFLAGS` 代表 `GCC` 編譯時用的參數。 * 後半部份的`TEST `輸入 `make test` 會使用 `pref` 去測試 `test_cpy` 和 `test_ref` 總共會測試100次。 * `make bench` 則會對兩個執行檔 `test_cpy` 和 `test_ref` 進行檢測。 * `make clean `會對執行檔和txt檔進行清除。 ### make plot的執行與建構: - 在著手進行`make plot `之前我們必須先整理清楚各個GNU的.gp檔所需要的內容是什麼,例如:runtime.gp 是需要`output.txt` 而 runtime3.gp 則是需要`test_cpy.txt `等等..... ( 關於如何生成`bench_cpy.txt ` `bench_ref.txt ` `ref.txt ` `cpy.txt ` `output.txt `我會在後面的bug標題進行說明。) ### `plot` ```clike= plot: output.txt bench_cpy.txt bench_ref.txt gnuplot scripts/runtime.gp eog runtime.png gnuplot scripts/runtime3.gp eog runtime3.png gnuplot scripts/runtimept.gp eog runtime2.png ``` 在第一行中代表的是輸入plot指令後需要的檔案,分別為`bench_cpy.txt ` `bench_ref.txt `和 `output.txt `而指令 `gnuplot scripts/runtime.gp ` 則是執行gnuplot該gp檔的程序,`eog runtime.png `則是顯示圖形。執行畫面如下: 首先鍵入指令`make plot `後,輸入完密碼開始進行檢測並紀錄執行時間。 ![](https://i.imgur.com/BnAgHqG.png) 執行完成後會產生三張圖形:分別是cpy和ref的執行時間比;cpy的搜尋效率;ref和cpy的搜尋效率對照,如圖: ![](https://i.imgur.com/bQoaQel.png) ![](https://i.imgur.com/mE3jP8O.png) ![](https://i.imgur.com/JgE2TuQ.png) ## 字串實驗的實現與紀錄 本次作業的要求: 視覺化 ternary search tree + bloom filter 的效能表現。注意,需要設計實驗,允許不同的字串輸入並探討 TST 回應的統計分佈。於是我做了以下實驗,其得到的結果是:ref的執行效率低於cpy並且會隨著輸入字串的增加越來越明顯,如同我上述所提到的我將測試的字串改為`Calva `但其實我一開始是從字串`C `開始進行實驗然後再逐一增加字串數量,例如增加為`Ca ` 等等...測試的結果如下 ![](https://i.imgur.com/luSjw3J.png) 增加為Ca ![](https://i.imgur.com/QOXG2QD.png) 增加為Cal ![](https://i.imgur.com/1Blmft4.png) 增加為Calv ![](https://i.imgur.com/QrooeFH.png) 增加為Calva ![](https://i.imgur.com/BoVNMfx.png) 可以發現雙方的執行時間從0.38秒的差距拉開到0.47秒,可知ref和cpy的效率的確是差了一些。 :::warning 探討造成此現象可能的原因? ::: :::info 10/15 將runtime.gp作修改: ```go= reset set ylabel 'time(sec)' set style fill solid set key center top set title 'perfomance comparison' set term png enhanced font 'Verdana,10' set output 'runtime.png' plot [:][y=0:5.000]'output.txt' using 2:xtic(1) with histogram title 'cpy', \ '' using 3:xtic(1) with histogram title 'ref' , \ '' using ($0-0.500):(0.110):2 with labels title ' ' textcolor lt 1, \ '' using ($0-0.500):(0.320):3 with labels title ' ' textcolor lt 2, ``` ::: ## Bug的問題與解決: 首先比較容易發現的bug在test_cpy.c和test_ref.c之間的差異,執行完程式碼後會發現無法生成ref的文字檔,其原因是在於test_ref.c中程式碼少了輸出txt檔的程式碼:如下: ```clike= if (argc == 2 && strcmp(argv[1], "--bench1") == 0) { int stat = bench_test(root, BENCH_TEST_FILE, LMAX); tst_free_all(root); return stat; } FILE *output; output = fopen("ref.txt", "a"); if (output != NULL) { fprintf(output, "%.6f\n", t2 - t1); fclose(output); } else printf("open file error\n"); ``` 將此程式碼輸入後即可產生`bench_ref.txt ` 除此之外也要記得執行calculate.c檔來產生`output.txt ` 不過最讓我好奇的還是: ## 尋找 search "A" 發生 Segfault 的原因 如圖:![](https://i.imgur.com/JUEMviw.png) 會發現在輸入A後會出現segfault的狀況,後來發現其問題是出在 tst.c檔中: ```C=317 void tst_suggest(const tst_node *p, const char c, const size_t nchr, char **a, int *n, const int max) { if (!p || *n == max) return; tst_suggest(p->lokid, c, nchr, a, n, max); if (p->key) tst_suggest(p->eqkid, c, nchr, a, n, max); else if (*(((char *) p->eqkid) + nchr - 1) == c) a[(*n)++] = (char *) p->eqkid; tst_suggest(p->hikid, c, nchr, a, n, max); } ``` 參考了:(https://hackmd.io/nLSb34t0SE2V-56Gxi7ErA?both# )之後才發現原來是因為,*n不斷的滿足條件進入遞迴(return)當進行到330行時,程式早已經不符合原來的設定了,*n的持續增加會造成存取到不正確的記憶體。需要將程式修改成: ```clike= else if (*(((char *) p->eqkid) + nchr - 1) == c && *n != max) ``` 即可修復這個bug。 :::info ### 10/16 新增關於輸入功能cpy和ref之間的效率比較 - 修改runtimebox裡的gp內容,如下: ```clike reset set xlabel 'test' set ylabel 'time(microsec)' set title 'perfomance comparison(add)' set term png enhanced font 'Verdana,10' set output 'addruntime.png' set format x "%10.0f" set xtic 10 set xtics rotate by 45 right plot [:300][:]'cpy.txt' title 'cpy_ add',\ 'ref.txt' title 'ref_ add' ``` - 圖形顯示 ![](https://i.imgur.com/R38CFq7.png) 可以看到cpy和ref在輸入功能中仍舊是cpy的速度較快,其中原因我想也來自於cpy和ref存取的方式不同,前者是用TST不過後者是用bloom filter,但關於本次作業要求要兩個都加入bloom filter還是不懂。 10/17更新: ref還使用了bloom filter去搜尋字串所以可能是因為這樣,在時間上才會高於cpy。 ::: ## 關於coredumped的問題 :::warning ~~時間不夠待補上~~ 對ref進行perf分析時,會導致coredumped,如圖: ![](https://i.imgur.com/S7opUJe.png) ::: :question: 為什麼執行perf會導致test_ref coredumped呢?是不是內部還有bug?待用gdb去做檢測。 # Linux perf效能分析 ![](https://i.imgur.com/EB9qttp.png) ![](https://i.imgur.com/fYWIJOw.png) :::info 10/19更新: 在test_cpy新增bloom_filter進行測試: ```clike= #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include "bloom.h" #include "bench.c" #include "tst.h" /** constants insert, delete, max word(s) & stack nodes */ enum { INS, DEL, WRDMAX = 256, STKMAX = 512, LMAX = 1024 }; #define REF INS #define CPY DEL long poolsize = 2000000 * WRDMAX; #define BENCH_TEST_FILE "bench_cpy.txt" #define TableSize 5000000 /* simple trim '\n' from end of buffer filled by fgets */ static void rmcrlf(char *s) { size_t len = strlen(s); if (len && s[len - 1] == '\n') s[--len] = 0; } #define IN_FILE "cities.txt" int main(int argc, char **argv) { char word[WRDMAX] = ""; char *sgl[LMAX] = {NULL}; tst_node *root = NULL, *res = NULL; int rtn = 0, idx = 0, sidx = 0; FILE *fp = fopen(IN_FILE, "r"); double t1, t2; if (!fp) { /* prompt, open, validate file for reading */ fprintf(stderr, "error: file open failed '%s'.\n", argv[1]); return 1; } t1 = tvgetf(); bloom_t bloom = bloom_create(TableSize); /* memory pool */ char *pool = (char *) malloc(poolsize * sizeof(char)); char *Top = pool; while ((rtn = fscanf(fp, "%s", Top)) != EOF) { char *p = Top; /* insert reference to each string */ if (!tst_ins_del(&root, &p, INS, REF)) { /* fail to insert */ fprintf(stderr, "error: memory exhausted, tst_insert.\n"); fclose(fp); return 1; } else { /* update bloom filter */ bloom_add(bloom, Top); } idx++; Top += (strlen(Top) + 1); // break; } t2 = tvgetf(); fclose(fp); printf("ternary_tree, loaded %d words in %.6f sec\n", idx, t2 - t1); ``` 後來經過討論後,發現原來 test_ref是代表tst加上bloom_filter,而test_cpy則是只有tst而已,而在之前的圖形中,之所以ref需要的時間會比cpy還的長的原因是因為bloom_filter需要建立還有更新的時間,再兩者都加入bloom_filter後所產生的搜尋圖形和新增圖形之比較如下: ![](https://i.imgur.com/xXapp4j.png) 由上圖可知,兩者所需的時間是差不多的。 ![](https://i.imgur.com/iPY662q.png) ::: :::info 提問: ![](https://i.imgur.com/iPY662q.png) 上面的圖形是關於新增功能的比較,但不太清楚過程中的波動是因為什麼原因造成的? :::