# 2020q3 Homework3 (dict) contributed by < `zzzxxx00019` > > [dict](https://hackmd.io/@sysprog/2020-dict) ## 程式運作解讀: ### test_common.c 程式碼解讀 * 在程式執行初期, REF 被預設為 INS , CPYmask 預設為 -1 , REF 用於決定程式執行何種模式 * 若執行 CPY 模式, REF 會被設為 DEL 、 CPYmask 設為 0 * 透過輸入命令 `./test_common CPY` 進入 CPY 模式,其他字串則進入 REF 模式 ```cpp if (!strcmp(argv[1], "CPY") || (argc > 2 && !strcmp(argv[2], "CPY"))) { CPYmask = 0; REF = DEL; printf("CPY mechanism\n"); } else printf("REF mechanism\n"); ``` * Pool 主要用於 REF 機制,讀檔時將地名永久存入 memory pool ,若 `CPYmask = -1` 條件式成立 * 在 `test_common.c` 中, `poolsize = 2000000 * WRDMAX` ,而 `WRDMAX = 256` * 當使用 REF 機制時,將預先分配 poolsize 個字元空間供讀取檔案與暫存使用 ```cpp if (CPYmask) { /* memory pool */ pool = (char *) malloc(poolsize * sizeof(char)); Top = pool; ``` * 讀入的字串根據 `','` 與 `'\n'` 做為切割依據 * 透過 `tst_ins_del` 將資料插入 Ternary search tree * 而 CPY 與 REF 在此的差異在於 `Top -= offset & ~CPYmask` 出來的結果 * 當 CPY 模式, `CPYmask = 0` , `~CPYmask = 0xff...f` , Top 會被推回原點 * 當 REF 模式, `CPYmask = -1` , `CPYmask = 0x0` , Top 維持在插入字串後的位址 ```cpp char buf[WORDMAX]; 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] == ','); } 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); } ``` * 透過輸入指令 --bench ,可使用 bench_test 這個 function 去對 ternary search tree 進行測試 ```cpp if (argc == 3 && strcmp(argv[1], "--bench") == 0) , int stat = bench_test(root, BENCH_TEST_FILE, LMAX); tst_free(root); free(pool); return stat; ``` * 觀察到 `bench_test` 中,最核心的迴圈部分,其實就是藉由讀取原本的 `cities.txt` ,去搜尋裡面字串的部分,所以理論上每個字串應該都要存在於 `tst` 中,再將每個字串搜尋所需要的時間輸出到 `bench_ref.txt` 中 ```cpp 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++; } ``` ### tst_ins_del 程式碼解讀 ( 某次更新中已移除此 function ) * tst_ins_del 有兩種模式,分別別為 delete 與 insert ,由 del 這個變數決定何種模式,由 cpy 變數決定 insert 的機制為 cpy 還是 ref * 目前已將 `tst_ins_del` 拆為 `tst_ins` 、 `tst_del` ```cpp void *tst_ins_del(tst_node **root, const char *s, const int del, const int cpy) ``` * 透過 `while` 迴圈去搜尋字串 s 是否存在於 tst 中,若為 del 模式, stack 會記錄當前走訪路徑,為節點刪除後做準備 * 若成功搜尋到該字串,可分 del 與 ins 兩種狀況去看會如何處理 : * del 模式會呼叫 `tst_del_word` 並透過 stack 將這個字串刪除 * ins 模式則將 `refcnt + 1` ,並回傳該字串 ```cpp 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 */ } ``` * 如果要插入的字串尚未存在 tst 中,則透過以下迴圈去進行插入的動作,將該字串所缺乏的節點做補齊 * 而 CPY 與 REF 在 insert 的動作上就有所差異: CPY 透過 `strdup` 分配一個儲存空間,而 REF 則在一開始就分配一個巨大的儲存空間提供新資料插入使用 * `char * strdup(const char *s)` 的用途是透過 `malloc` 分配一個與字串 s 相同空間的大小,再將 s 的內容複製到該地址,並將地址回傳 ```cpp 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); } ``` ## 視覺化 ternary search tree + bloom filter 的效能表現並分析 ### 透過 gnuplot ,將 CPY 與 REF 透過 cities.txt 為字典檔所搜尋的時間繪製成圖與分析 在原始程式碼中, bench_test 直接使用 tst_search_prefix 去搜尋字串,沒有預先透過 bloom_test 檢查,為了方便測試,稍微改寫了一下程式,將 --bloom 加入指令: 透過輸入 `./test_common --bloom CPY` ,在執行 `tst_search_prefix` 前會先透過 `bloom filter` 判斷字串是否可能出現在 tst 中 在 test_common.c 中加入此段程式碼: ```cpp else if (argc == 3 && strcmp(argv[1], "--bloom") == 0) { ... int stat = benchbloom_test(root, BENCH_TEST_FILE, LMAX, &bloom); ... return stat; } ``` * 在 `bench.c` 方面,考慮到 `bloom filter` 的特性,並不允許 `fscanf` 這樣的模式輸入尋找字串 ( 會發生空白截斷 ) ,因此參考 `test_common.c` 在字串讀入與處理的部分,對字串做相對的處理,才不會發生 `bloom filter` 找不到符合字串的情況 * 為了實驗的公平性,在 `no bloom filter` 的狀況下,也必須透過 `fgets` 去讀檔,且都不透過 `prefix` 去對字串做切割處理,在一般環境與 bloom filter 的差異下,就在於是否有透過 `bloom_test` 預測字串可能存在於 tst 中 ```cpp while (fgets(buf, WORDMAX, dict)) { for (int i = 0, j = 0; buf[i]; i++) { word[i] = (buf[i + j] == ',' || buf[i + j] == '\n') ? '\0' : buf[i + j]; j += (buf[i + j] == ','); } ... if (bloom_test(*filter, word)) { tst_search_prefix(root, word, sgl, &sidx, max); } ... } ``` ==gunplot 製圖結果( 搜尋字典檔為 "cities.txt" )== ![](https://i.imgur.com/If5K3kK.png) ==CPY mechanism 分析結果 ( No bloom filter )== ``` Performance counter stats for './test_common --bench CPY': 207.75 msec task-clock # 0.999 CPUs utilized 0 context-switches # 0.000 K/sec 0 cpu-migrations # 0.000 K/sec 7349 page-faults # 0.035 M/sec 8,5749,8592 cycles # 4.128 GHz 11,0039,1657 instructions # 1.28 insn per cycle 1,7779,8904 branches # 855.822 M/sec 432,6607 branch-misses # 2.43% of all branches 0.207985136 seconds time elapsed 0.179999000 seconds user 0.027999000 seconds sys ``` ==REF mechanism 分析結果 ( No bloom filter )== ``` Performance counter stats for './test_common --bench REF': 214.12 msec task-clock # 0.999 CPUs utilized 1 context-switches # 0.005 K/sec 0 cpu-migrations # 0.000 K/sec 7169 page-faults # 0.033 M/sec 8,8678,6840 cycles # 4.141 GHz 10,7758,3942 instructions # 1.22 insn per cycle 1,7331,4011 branches # 809.408 M/sec 460,4517 branch-misses # 2.66% of all branches 0.214399980 seconds time elapsed 0.206320000 seconds user 0.008091000 seconds sys ``` ==CPY mechanism 分析結果 ( bloom filter )== ``` Performance counter stats for './test_common --bloom CPY': 217.18 msec task-clock # 0.999 CPUs utilized 2 context-switches # 0.009 K/sec 0 cpu-migrations # 0.000 K/sec 7346 page-faults # 0.034 M/sec 8,9835,1949 cycles # 4.136 GHz 11,4112,9491 instructions # 1.27 insn per cycle 1,8157,1321 branches # 836.023 M/sec 436,5518 branch-misses # 2.40% of all branches 0.217446657 seconds time elapsed 0.201304000 seconds user 0.016104000 seconds sys ``` ==REF mechanism 分析結果 ( bloom filter )== ``` Performance counter stats for './test_common --bloom REF': 224.07 msec task-clock # 0.999 CPUs utilized 1 context-switches # 0.004 K/sec 0 cpu-migrations # 0.000 K/sec 7168 page-faults # 0.032 M/sec 9,2713,6034 cycles # 4.138 GHz 11,1810,8196 instructions # 1.21 insn per cycle 1,7687,3963 branches # 789.361 M/sec 464,1634 branch-misses # 2.62% of all branches 0.224305167 seconds time elapsed 0.220304000 seconds user 0.004005000 seconds sys ``` * 從 `perf` 分析結果來看,在沒有 `bloom filter` 實驗中, `CPY` 與 `REF` 在效率上並沒有很大的落差,此部分還有待深入探討 * 因為搜尋的字典檔是相同於建立 `tst` 的字典檔,因此在實驗過程中,每個字都會 hit ,`bloom filter` 就成為了搜尋過程的一大累贅,因此從上圖可以觀察到,透過 `bloom filter` 所耗費的時間較多 * `bloom filter` 是用來檢查搜尋字串的正確性,避免在龐大的資料庫,搜尋一個確定不存在的資料 ==gunplot 製圖結果( 使用字典檔的字串幾乎不存在於 cities.txt 中 )== ![](https://i.imgur.com/kpLcrLx.png) * 可以發現 `bloom filter` ,在一開始就過濾掉不存在於 `hash table` 裡面的字串,因此不會浪費額外的資源去做 search 的動作,相較於不使用 `bloom filter` 的實驗,必須搜尋到某個階段才能確定字串不存在於 `tst` ,因此需浪費更多的額外資源與時間 :::warning To Do: 思考 CPY 與 REF 在效能上有何落差。 :::