# 2020q3 Homework3 (dict) contributed by < ``yceugene`` > ###### tags: `linux2020` `sysprog2020` ## :memo: 目錄 [TOC] ### Outline 1. Trie & Ternary tree 2. ternary search tree + bloom filter 的效能表現並分析 3. CPY & REF 機制差異 4. Bloom filter 空間浪費改善 5. 引入 test-common.c 統合 test_cpy.c 和 test_ref.c 6. 紀錄多餘逗號修正 7. 安裝 perf 8. Perf 分析 CPY & REF 機制效能 9. perf record 找 cache-misses 熱點 10. memory leaks 的偵測及修正 ### Environment ``` $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 6 On-line CPU(s) list: 0-5 Thread(s) per core: 1 Core(s) per socket: 6 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 158 Model name: Intel(R) Core(TM) i5-9500 CPU @ 3.00GHz Stepping: 10 CPU MHz: 800.036 CPU max MHz: 4400.0000 CPU min MHz: 800.0000 BogoMIPS: 6000.00 Virtualization: VT-x L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 9216K NUMA node0 CPU(s): 0-5 ``` ### Trie Trie 又叫做 ``digital tree`` 或 ``prefix-tree``(或稱字典樹),是一種 ``search tree``,通常用來存放字串。其特性是適合用來做 prefix search 但礙於各節點存有大量空指標,容易占用記憶體空間。 ### Ternary search tree(TST) TST 是一種 trie 的資料結構,其優點是結合了 trie 的時間效率和 BST 的空間效率。TST 的節點會包含一個 key(用來記錄 Keys 中的其中一個字元),以及至多三個 child ,大於、等於、小於。 在 ``dict`` 中 TST 結構定義如下: ``` c typedef struct tst_node { char key; /* char key for node (null for node with string) */ unsigned refcnt; /* refcnt tracks occurrence of word (for delete) */ struct tst_node *lokid; /* ternary low child pointer */ struct tst_node *eqkid; /* ternary equal child pointer */ struct tst_node *hikid; /* ternary high child pointer */ } tst_node; ``` 特性: * 因為各個節點至多只會有 3 個 child,因此使用起來更節省記憶體。 ### 初步執行 dict 專案 * 取得 dict 程式碼,編譯並測試 ``` $ make $ ./test_common CPY ``` * 執行結果 ``` CPY mechanism ternary_tree, loaded 206849 words in 0.295581 sec 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: f find word in tree: Taiwan Bloomfilter found Taiwan in 0.000002 sec. Probability of false positives:0.006306 ---------- Tree found Taiwan in 0.000006 sec. choice: s find words matching prefix (at least 1 char): Tain Tain - searched prefix in 0.000009 sec suggest[0] : Tain suggest[1] : Tainan suggest[2] : Taino suggest[3] : Tainter Lake suggest[4] : Taintrux ``` ### copy 機制 (CPY) & reference 機制 (REF) * 比較 copy 與 reference 機制的效率 * make bench ``` $ make bench CC test_common.o CC tst.o CC bloom.o LD test_common COPY mechanism test_common => choice: find words matching prefix (at least 1 char): Tai - searched prefix in 0.000305 sec REFERENCE mechanism test_common => choice: find words matching prefix (at least 1 char): Tai - searched prefix in 0.000452 sec (base) ``` * make bench TEST_DATA="s T" ``` $ make bench TEST_DATA="s T" COPY mechanism test_common => choice: find words matching prefix (at least 1 char): T - searched prefix in 0.000330 sec REFERENCE mechanism test_common => choice: find words matching prefix (at least 1 char): T - searched prefix in 0.000370 sec ``` ## 程式碼理解 ### main() 先針對 CPY 與 REF 兩種機制做不同的記憶體配置,主要差別是 Top 指標指向的空間不同。 ``` c if (!strcmp(argv[1], "CPY") || (argc > 2 && !strcmp(argv[2], "CPY"))) { CPYmask = 0; REF = DEL; printf("CPY mechanism\n"); } else printf("REF mechanism\n"); ``` CPY 下的配置: Top 指向一塊 char array ```c enum{ WRDMAX = 256}; char word[WRDMAX] char *Top = word; char *pool = NULL; ``` REF 下的配置: Top 指向一塊 heap 中的空間(大小為 2000000 * WRDMAX) ```c pool = malloc(poolsize); Top = pool; ``` 對 ``cities.txt`` 每個 row 做 parsing,再將字串 assign 給 pool: ```c= 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] == ','); } ``` 對 bloom filter 做新增: ```c= while (*Top) { if (!tst_ins(&root, Top, 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; } ``` CPY 與 REF 兩種機制,在做 TST Insert 時會有不同,CPY 機制是在插入新字串時,malloc 一塊記憶體存放新字串;REF 機制由於在讀取檔案時,就已將全部字串放進 heap 中了,因次在樹中插入字串時,只需要存放對應的 heap address就可以了。 ```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; } } ``` 在 CPY 機制下,每處理完一列資料時,需要把 Top 還原回 word 的起始位址;REF 下則不用: ``Top -= offset & ~CPYmask`` ## bloom filter Bloom filter 是利用 hash function,在不用走訪全部元素的前提,預測特定字串是否存於資料結構中。但只能做預測,有一定的錯誤率。 建立 bloom filter: ```c bloom_t bloom = bloom_create(TableSize); ``` ### Bloom filter 主體宣告: ```c typedef unsigned int (*hash_function)(const void *data); typedef struct bloom_filter *bloom_t; ``` 用一個 linked list 存放用到的所有 hash function ``` c struct bloom_hash { hash_function func; struct bloom_hash *next; }; struct bloom_filter { struct bloom_hash *func; void *bits; // 即 table size_t size; // 為 table 大小 }; ``` ### tst_ins(&root, Top, REF) ## ternary search tree + bloom filter 的效能表現 ## 以 perf 分析 TST 程式碼的執行時期行為 ## 考慮到 CPY 和 REF 這兩種實作機制