# 2018q3 Homework3 (dict) contributed by < [`siahuat0727`](https://github.com/siahuat0727) > [作業說明 E02: dict](https://hackmd.io/s/B1Bq5Jat7#%E4%BD%9C%E6%A5%AD%E8%A6%81%E6%B1%82) ## 開發環境 ``` $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 158 Model name: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz Stepping: 9 CPU MHz: 826.723 CPU max MHz: 3800.0000 CPU min MHz: 800.0000 BogoMIPS: 5616.00 Virtualization: VT-x L1d cache: 32K L1i cache: 32K L2 cache: 256K L3 cache: 6144K NUMA node0 CPU(s): 0-7 ``` ## Ternary Search Tree 實作細節 ### 分飾多角的 eqkid 資料結構 ```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; ``` 閱讀 `tst_ins_del` 時遇到很多困難,比如: ```C void *tst_ins_del(tst_node **root, const char *s, const int del, const int cpy) { ... while ((curr = *pcurr)) { /* iterate to insertion node */ ... return (void *) curr->eqkid; /* pointer to word */ ... pcurr = &(curr->eqkid); /* get next eqkid pointer address */ ... } for (;;) { ... curr->eqkid = (tst_node *) s; return (void *) s; ... } } ``` 乍看之下完全一頭霧水,怎麼有時候將 `eqkid` 傳出去當 word 起始位置,有時候又用它來取得下一個 node,有時候將傳進來的 `const char *s` 轉型成 `tst_node *` ~~WTF?!~~ 指派給它?(程式碼跟原始版本有些差異,[詳見這裡](https://hackmd.io/ClIwCm59Ttya3_FPwez2IQ?view#tst_ins_del-%E5%A5%87%E6%80%AA%E7%9A%84%E5%8F%83%E6%95%B8)) 如果沒辦法直接讀懂,其實從 `tst_suggest` 可以看出一些端倪: ```C void tst_suggest( ... ) { ... 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; ... } ``` 若 `key` 非 0(`key` 爲 0 的 node 的存在是爲了表示 parent node 的結尾),則將 `eqkid` 當 node 指標進行 recursive call,否則當作字串使用。 這裡需要記錄字串是因爲走訪到 node 之後要得知該 node 代表的字串是有點麻煩的事情(若有記錄 parent,可以往上透過一些規則找回),但這裡很巧妙的利用了 `eqkid` 當作 `char *` 指向該 node 表示的字串。 ### CPY 機制與 REF 機制的差別 而作業中所謂的 copy 與 reference 機制,前者是傳入 `tst_ins_del` 的字串地址所表示的值**隨時會被覆寫**,因此 `eqkid` 需要 `malloc` 新空間將其 **copy** 存起來,而後者是傳進來的字串地址所表示的值**一直屬於該字串**,以此可將其當作 **reference** 直接存起來,不用 `malloc` 新空間。 ### 與 [Ternary Search Tree 視覺化](https://www.cs.usfca.edu/~galles/visualization/TST.html) 的差異 承接上面,`eqkid` 分飾多角,沒有需要重複使用的時候嗎? |Insert XY|Insert XYZ|Insert XY and XYZ |--|--|--| | ![](https://i.imgur.com/Sfb5JEz.png) | ![](https://i.imgur.com/CgpvAKU.png) | ![](https://i.imgur.com/QbPSMk8.png) | 如上面第三種情況,node Z 的 `eqkid` 該指向 ``"XY"`` 表示該字串結束,還是指向下一個 `key` 爲 0 的 node 表示 `"XYZ"` 的結束呢? 再仔細觀察及思考會發現 node Z 的 key 爲 `'z'`,已無法表示字串 `"XY"` 的結束,`tst_node` 中也沒有類似綠色的屬性,所以 tst.c 的實作實際上是這樣的: |Insert XY and XYZ in order|Insert XYZ and XY in order| |--|--| |![](https://i.imgur.com/KU6JPaA.png)|![](https://i.imgur.com/vTpNRJd.png)| **注意:上圖(兩張都)請將 N 當作小綠空圓(表示字串 `"XY"` 的結束)並無視它下面的小綠空圓,而左圖 `>N` 爲 `>0`** > 有空再 p 圖 QQ,進度趕不上了 可見用來表示結束的 node 永遠不會有 ternary equal child, 因此 `eqkid` 就不會分身乏術啦。 ## 前期修改 ### test_CPY.c 補上 bloom filter 參考 test_ref.c,統一[加入 bloom filter](https://github.com/siahuat0727/dict/commit/33b57bcb6973b1e66cc6e306f383258f99dfd65d),才能比較兩種實作機制的效能差異。 ### test_ref.c 補上 bench_test 參考 test_cpy.c,將缺失的[程式碼補齊](https://github.com/siahuat0727/dict/commit/a9f2e89935538b00c3777246bfa5c371fb0e92bb)。 ### bench.c 中 bug 修復 程式將 `cities.txt` 中每個名稱(包括城市與國家名)在已建立好的 ternary tree 中進行搜索,照理來說每次搜索都要成功(建 tree 和 search 用的是相同資料),但若對程式碼進行以下修改: bench.c 的[這裡](https://github.com/siahuat0727/dict/blob/a9f2e89935538b00c3777246bfa5c371fb0e92bb/bench.c#L51) ```C if (tst_search_prefix(root, prefix, sgl, &sidx, max) != NULL) puts("Found"); else puts("Not found"); ``` 執行 `make && ./test_ref --bench` 會發現 Output: ``` ... Not found Not found ... ``` 原因是 `char prefix[3] = "" ` 的大小只有 `3`,`strncpy` 時沒預留 `\0` 的位置: ```C int bench_test(const tst_node *root, char *out_file, const int max) { char prefix[3] = ""; ... while (fscanf(dict, "%s", word) != EOF) { ... strncpy(prefix, word, 3); ... tst_search_prefix(root, prefix, sgl, &sidx, max) ... } ... ``` 而 `tst_search_prefix` 中 `s` 的使用上是以找到 `'\0'` 作爲結束: ```C void *tst_search_prefix(const tst_node *root, const char *s, char **a, int *n, const int max) { ... const char *start = s; ... /* get length of s */ for (; *start; start++) ; /* wait */ const size_t nchr = start - s; ... } ``` 因此 `nchr` 絕大多數情況下不是預想中的 3,而是較大的值,導致尋找非預期的 prefix,搜索失敗。 [修改方式](https://github.com/siahuat0727/dict/compare/a9f2e89...6833c18#diff-a9937b3765788bcf4c80db22f17c4032):將 `prefix` 大小改爲 4 ,`strncpy` 限制 `n` 爲 `sizeof(prefix)-1` 可確保最後的 `'\0'` 不會被覆蓋。 ### 修改 bench.c 及繪圖程式 時間單位 錯誤 ```C int bench_test(const tst_node *root, char *out_file, const int max) { ... fprintf(fp, "%d %f sec\n", idx, (t2 - t1) * 1000000); // should be microseconds ... } ``` `t2 - t1` 的單位爲**秒**,乘上 `1e6` 後單位應爲**微秒** [統一單位爲 msec 並新增 `Makefile` 畫圖功能](https://github.com/siahuat0727/dict/commit/e7306791992a6082d0b4e8c9b9a5cd656741626f)後,執行 `make clean && make search-all && make plot` 並等待數十秒…… ``` search-all: $(TESTS) @for test in $(TESTS); do\ ./$$test --bench ; \ done ``` ![](https://i.imgur.com/xA7keNA.png) 這裡是給定每個已存在的字串 prefix 進行搜索,測試找到所有符合 prefix 的字串所需的時間,因此與建樹過程中 `malloc` 所花的時間等等**沒有關係**。 很明顯的,以 **REF 機制**建立的 tree 在進行**搜索**時速度是比較**慢**的。 ## Perf 分析 爲什麼會出現上面 CPY 機制明顯優於 REF 機制的結果呢? 使用 perf 對上述實驗進行分析: ``` perf-search: $(TESTS) @for test in $(TESTS); do\ echo 3 | sudo tee /proc/sys/vm/drop_caches; \ perf stat \ -e cache-misses,cache-references,instructions,cycles \ ./$$test --bench; \ done ``` 執行 `make perf-search` ``` Performance counter stats for './test_cpy --bench': 1,681,368,653 cache-misses # 58.179 % of all cache refs 2,890,009,712 cache-references 49,637,166,894 instructions # 0.90 insn per cycle 54,932,228,832 cycles 15.492978290 seconds time elapsed ``` ``` Performance counter stats for './test_ref --bench': 1,967,405,626 cache-misses # 62.223 % of all cache refs 3,161,862,019 cache-references 49,705,117,369 instructions # 0.71 insn per cycle 70,053,549,114 cycles 19.957402867 seconds time elapsed ``` 明明**搜索過程一模一樣**,但兩者的 cache-misses 次數卻相差甚遠。 仔細思考兩者的差異,其實**只有** ternimal node 的 `eqkid` 所指向的**字串地址來源不同**,CPY 機制是單獨 `malloc` 的小空間,REF 機制是 `malloc` 大空間的一小部分。所以過程中唯一稱得上不同的,大概只有最後對字串地址進行 dereference ,到對應的地址讀出字串的地方。 於是嘗試刪掉最後的 dereference 相關的部分(原本用來檢查是否找到符合的 prefix,刪掉會導致結果不正確,但這裡的重點是尋找 REF 相對耗時的地方),發現這樣一來兩者的分析資料就差不多了!(不一定哪種機制比較快) ```C 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) <= delete this only a[(*n)++] = (char *) p->eqkid; tst_suggest(p->hikid, c, nchr, a, n, max); } ``` ``` Performance counter stats for './test_cpy --bench': 1,390,728,865 cache-misses # 52.515 % of all cache refs 2,648,254,194 cache-references 45,808,349,880 instructions # 0.97 insn per cycle 47,022,265,491 cycles 12.974203801 seconds time elapsed ``` ``` Performance counter stats for './test_ref --bench': 1,340,926,158 cache-misses # 50.404 % of all cache refs 2,660,373,578 cache-references 45,869,337,319 instructions # 0.98 insn per cycle 46,894,627,000 cycles 12.866257905 seconds time elapsed ``` ![](https://i.imgur.com/9DbfzA3.png) 也就是說,造成 REF 相對慢的原因,與 `*(((char *) p->eqkid) + nchr - 1)` 背後發生的事情相關! > 這世界怎麼越來越奇怪…… 待研究 進一步用 perf 分析每個 intruction 發生 cache miss 的次數: `$ perf record -e cache-misses ./test_ref --bench && perf report` ``` 98.89% test_ref test_ref [.] tst_suggest 0.22% test_ref test_ref [.] tst_search_prefix 0.08% test_ref test_ref [.] tst_ins_del ``` 由於測試的是 prefix-search,時間都花在 `tst_suggest` 上不意外,先附上 [`tst_suggest`]() 前半段中每一行程式碼對應的組語指令 ``` tst_suggest(): const tst_node *p, // -0x8 相對於 %rbp 的地址 const char c, // -0xc const size_t nchr, // -0x18 char **a, // -0x20 int *n, // -0x28 const int max) // -0x10 { push %rbp mov %rsp,%rbp sub $0x30,%rsp mov %rdi,-0x8(%rbp) mov %esi,%eax mov %rdx,-0x18(%rbp) mov %rcx,-0x20(%rbp) mov %r8,-0x28(%rbp) mov %r9d,-0x10(%rbp) mov %al,-0xc(%rbp) if (!p || *n == max) cmpq $0x0,-0x8(%rbp) ↓ je 10e mov -0x28(%rbp),%rax mov (%rax),%eax cmp %eax,-0x10(%rbp) ↓ je 10e return; tst_suggest(p->lokid, c, nchr, a, n, max); movsbl -0xc(%rbp),%esi mov -0x8(%rbp),%rax mov 0x8(%rax),%rax mov -0x10(%rbp),%r8d mov -0x28(%rbp),%rdi mov -0x20(%rbp),%rcx mov -0x18(%rbp),%rdx mov %r8d,%r9d mov %rdi,%r8 mov %rax,%rdi → callq tst_suggest if (p->key) mov -0x8(%rbp),%rax movzbl (%rax),%eax test %al,%al ↓ je 9c tst_suggest(p->eqkid, c, nchr, a, n, max); movsbl -0xc(%rbp),%esi mov -0x8(%rbp),%rax mov 0x10(%rax),%rax mov -0x10(%rbp),%r8d mov -0x28(%rbp),%rdi mov -0x20(%rbp),%rcx mov -0x18(%rbp),%rdx mov %r8d,%r9d mov %rdi,%r8 mov %rax,%rdi → callq tst_suggest ↓ jmp e2 else if (*(((char *) p->eqkid) + nchr - 1) == c) 9c: mov -0x8(%rbp),%rax mov 0x10(%rax),%rax mov -0x18(%rbp),%rdx sub $0x1,%rdx add %rdx,%rax movzbl (%rax),%eax cmp %al,-0xc(%rbp) ↓ jne e2 a[(*n)++] = (char *) p->eqkid; mov -0x28(%rbp),%rax ... 10e: nop } ``` 看懂我們需要的指令不難,只需了解 `mov` 時 `0x8(%rax)` 會對 `%rax + 0x8` 這地址做 dereference,而 `(%rax)` 可理解爲 `0x0(%rax)` 的縮寫。 另外還需知道 [x86 General registers ](http://www.eecg.toronto.edu/~amza/www.mindsec.com/files/x86regs.html)。 ``` 32 bits : EAX EBX ECX EDX 16 bits : AX BX CX DX 8 bits : AH AL BH BL CH CL DH DL ``` 進一步比較兩種機制的差異,左邊的數值是 cache miss 發生在該指令的 period。 `test_ref`: 兩處熱點 ![](https://i.imgur.com/h18IW1X.png) ![](https://i.imgur.com/im84PJi.png) `test_cpy`: 一處熱點 ![](https://i.imgur.com/2VRQofI.png) ![](https://i.imgur.com/BOUJuwD.png) 一開始很困惑到底爲什麼第一處是發生在 `mov -0x10(%rbp),%r8d`(把 `max` 的值給 `%r8d` register), 原來 [perf 在回報時會有偏差](https://stackoverflow.com/questions/43794510/linux-perf-reporting-cache-misses-for-unexpected-instruction),這裡看起來實際發生 cache miss 是在上一個 instruction, `mov 0x8(%rax),%rax`, 即對 p 做 dereference(這裡是 function 裡的第一次,之後這 function 再對 `p` 做 dereference 時就很大機率 cache hit 了) `p->lokid` =>`(*p).lokid`, 其實以 compiler 的角度(也是實際過程)應該是 `*(tst_node*)((char*)p + offsetof(tst_node, lokid))`, 這裡的 `0x8` 即是所謂的 `offsetof(tst_node, lokid)`(在編譯時期可以算出來,所以指令中才是赤裸裸的 `0x8`),`rax` 在這裡是 `p`。 第二處便是之前提到的字串 dereference `*(((char *) p->eqkid) + nchr - 1)`(這裡一樣有回報偏差,實際上也是上一條 instruction `movzbl (%rax),%eax`),原本以爲 REF 機制在這裡發生 cache miss 的機率只是比 CPY 機制稍高,沒想到竟然高了**十幾倍**!應該說 CPY 機制在這裡完全沒有特別容易 cache miss 的傾向。 於是決定把地址印出來看看: ```C void tst_suggest (...) { ... 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; printf("%p\n", (((char *) p->eqkid) + nchr - 1)); } ... } ``` `./test_cpy --bench`: ``` 0x556d88e35022 0x556d88d31862 0x556d88cedf92 0x556d8870d522 ... ``` `./test_ref --bench`: ``` 0x7f76374d48d7 0x7f763735fcf8 0x7f76374b9321 0x7f763743ac33 ... ``` `malloc` 小空間的 `0x556d88e35022` 等等大概是 heap 空間,那麼 `0x7f76374d48d7` 呢? google 搜了好久後來還是在 man pages 中找到: `$ man malloc` >**NOTES** Normally, malloc() allocates memory from the heap, and adjusts the size of the heap as required, using sbrk(2). When allocating blocks of memory larger than MMAP_THRESHOLD bytes, the glibc malloc() implementation allocates the memory as a private anonymous mapping using mmap(2). MMAP_THRESHOLD is 128 kB by default, but is adjustable using mallopt(3). Prior to Linux 4.7 allocations performed using mmap(2) were unaffected by the RLIMIT_DATA resource limit; since Linux 4.7, this limit is also enforced for allocations performed using mmap(2). 原來在 `malloc` 大於 `MMAP_THRESHOLD` bytes 的空間時,會呼叫 `mmap`,而 `mmap` 會(在 heap 和 stack 之間)創建一個 `vm_area_struct`,將檔案映射到虛擬記憶體空間。詳見[认真分析mmap:是什么 为什么 怎么用](https://www.cnblogs.com/huxiao-tee/p/4660352.html)。 ![](https://i.imgur.com/SxKZPKk.png) [圖片來源](https://jin-yang.github.io/post/kernel-memory-mmap-introduce.html) Standard segment layout in a Linux process. (32-bit) ![](https://i.imgur.com/syPRnCp.png) [圖片來源](https://manybutfinite.com/post/anatomy-of-a-program-in-memory/) 但這樣還是無法解釋實驗結果的差異,再對程式進行以下觀察: ```C #include <stdint.h> void tst_suggest (...) { ... 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; printf("%ld\n", (intptr_t)(void*)(((char *) p->eqkid) + nchr - 1) - (intptr_t)(void*)&p->lokid); } ... } ``` `./test_ref --bench`: ``` 45833574649567 45833579989413 45833586770473 45833588397029 ... ``` `./test_cpy --bench`: ``` 42 42 42 42 ... ``` ### 終於找到 CPY 機制在 prefix-search 時效率比較好的原因 回想 tst.c 的 `tst_ins_del`: ```C void *tst_ins_del(tst_node **root, const char *s, const int del, const int cpy) { ... 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 機制在 insert 時,創出 leaf 之後會間接呼叫 `malloc` 來存字串,在這之前的最後一次 `malloc` 是爲了分配該 leaf 的空間。一直 `malloc` 時,heap 也會类似 stack 一样增長,所以像作業在建 tree 這種很規律的情況下,字串地址與 leaf 的地址很可能都很靠近。 也就是說,CPY 機制在一開始 `mov 0x8(%rax),%rax` (`p->eqkid`)後,字串就很大可能在 cache 裡面了,因此 cache miss 機率較低,這就是它相對來說在 prefix-search 上效率比較好的原因! ## CPY 機制與 REF 機制在建 tree 的效率比較 兩者建 tree 差異: + CPY 機制 + 讀檔時每次將地名暫時存入同一字串 `word` :+1: 只要 cache 沒被覆蓋就不會發生 cache miss + ternary search tree 的 terminal node 用 `strdup` 將地名存起來 :-1: 需要複製字串(影響小) :-1: 間接呼叫 `malloc`,可能發生 system call(影響大) + REF 機制 + 讀檔時每次將地名永久(直到 memory pool 被 free)依序存入 memory pool :-1: cache miss 次數 $\ge$ 使用的 memory pool 大小 / cache 一次載入的大小 + ternary search tree 的 terminal node 直接存 reference(指向 memory pool 的某個位置) :+1: 沒有明顯額外 overhead 還在嘗試用 perf 來分析 ## 缺陷補充 ### `tst_ins_del` 奇怪的參數 在閱讀 test_cpy.c 時發現以下程式碼: ```C int main(int argc, char **argv) { char word[WRDMAX] = ""; ... char *p = word; if (!tst_ins_del(&root, &p, INS, CPY)) { .... } ``` `p` 指向 `word` 的開頭後將 `p` 的地址傳入 `tst_ins_del` ?有什麼用處呢? 發現 `void *tst_ins_del(tst_node **root, char *const *s, const int del, const int cpy)` 沒直接操作 `s`,也不會改到 `*s` 的值。 且 test_cpy.c 中有使用到 `char *p` 的其他所有地方也無意義。同樣的,test_ref.c 也有這情況。 因此刪掉多餘的程式碼並將 `tst_ins_del` 參數中的 `char *cosnt *s` 改爲 `const char *s`。[git commit](https://github.com/siahuat0727/dict/commit/a51d66981456f61cc4b484e7a9c5c3551a0a495a) ### test_cpy.c 和 test_ref.c 的其他 bug 修復 ```C FILE *fp = fopen(IN_FILE, "r"); if (!fp) { /* prompt, open, validate file for reading */ fprintf(stderr, "error: file open failed '%s'.\n", argv[1]); return 1; } ``` 這裡的 `argv[1]` 應改爲 `IN_FILE` ```C int main() { ... bloom_t bloom = bloom_create(TableSize); ... for(;;) { ... switch(*word) { ... quit: case 'q': tst_free_all(root); return 0; ... } } bloom_free(bloom); return 0; } ``` 這裡 `case 'q'` 會直接 `return 0`,bloom filter 被 create 卻沒 free,造成 memory leak。 [修改後](https://github.com/siahuat0727/dict/commit/feb8eaa76b2ae66f2f0c248febf647184abd347d): ```C int main() { ... case 'q': goto quit; ... quit: tst_free_all(root); bloom_free(bloom); return 0; } ``` >後來發現還有其他地方,待修改 ### Delete 不存在的資料會導致插入 以下爲 `tst_ins_del` 簡化版的 pseudo code: ```C void *tst_ins_del(tst_node **root, const char *s, const int del, const int cpy) { 在樹中尋找字串 s,若發現已存在,執行對應的插入(增加 ref 數)或刪除後返回 // 這裡應加上:若 動作 爲刪除時直接返回表示刪除失敗,不要進行插入 插入字串 s } ``` 由於原程式中 return 值爲 `NULL` 表示刪除成功,我想這裡可以學習 `sbrk()` 使用 `(void*) -1` 表示刪除失敗。(剛好 `test_cpy.c` 和 `test_ref.c` 中用 return 值 `res` => `if (res)` 來判斷是否刪除失敗,因此不用特別做修改。) [git commit](https://github.com/siahuat0727/dict/commit/194d134952dae16b2df02c7ef97284a213b86968) ### Search A 會發生 Segmentation fault [問題定位可參考這位同學](https://hackmd.io/nLSb34t0SE2V-56Gxi7ErA?view#%E5%B0%8B%E6%89%BE-search-%E2%80%9CA%E2%80%9D-%E7%99%BC%E7%94%9F-Segfault-%E7%9A%84%E5%8E%9F%E5%9B%A0) `tst_sugggest` 多處 call 到自己,因此應做以下[修改](https://github.com/siahuat0727/dict/commit/f7c31f754e6edd555c70afd49dfa2512de14267a): ```C= 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) // 改 *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) // 需要再次確保 *n < max,因爲 *n 可能改變了 a[(*n)++] = (char *) p->eqkid; tst_suggest(p->hikid, c, nchr, a, n, max); } ``` 第 13 行較不影響效能的解決方法是把第 10 行的搜索改到第 14 行與第 15 行之間(這能確保 `*n` 值與進入 function 時一樣),但這會造成結果不會按字母順序排列,在這裡大概得不償失。 ### 城市名後面跟着逗號 將參考[< `ChiuYiTang` >同學的解決思路](https://hackmd.io/s/ByeocUhnZ#%E9%87%9D%E5%B0%8D%E5%90%84%E5%9C%8B%E5%9F%8E%E5%B8%82%E7%9A%84-prefix-search-%E6%9C%89%E7%84%A1%E7%BC%BA%E9%99%B7%E5%91%A2%EF%BC%9F%E6%AF%94%E6%96%B9%E8%AA%AA%E7%84%A1%E6%B3%95%E5%8D%80%E5%88%86%E5%9F%8E%E5%B8%82%E5%92%8C%E5%9C%8B%E5%AE%B6%EF%BC%8C%E4%B8%A6%E6%8F%90%E5%87%BA%E5%85%B7%E9%AB%94%E7%A8%8B%E5%BC%8F%E7%A2%BC%E7%9A%84%E4%BF%AE%E6%AD%A3),以逗號爲提示,區分城市名和國家名。另外,記錄城市名時不應該把逗號也存下來。 ### Bloom filter 空間浪費 整個 table 都把 byte 當 bit 來用,浪費 1/8 的 table 空間。 ```C void bloom_add(bloom_t filter, const void *item) { struct bloom_hash *h = filter->func; uint8_t *bits = filter->bits; while (h) { unsigned int hash = h->func(item); hash %= filter->size; bits[hash] = 1; // 每個 uint8_t 不是 1 就是 0 h = h->next; } } ``` ### 潛在危機 ```C if (argc > 1 && strcmp(argv[1], "--bench") == 0) // a for auto strcpy(word, argv[2]); ``` 直接 `strcpy` 有可能發生 buffer overflow ## Reference [prefix-search by `rex662624`](https://hackmd.io/s/SJbHD5sYM) [prefix-search by `ChiuYiTang`](https://hackmd.io/s/ByeocUhnZ#) [Perf -- Linux下的系统性能调优工具](https://www.ibm.com/developerworks/cn/linux/l-cn-perf1/) [x86 General registers ](http://www.eecg.toronto.edu/~amza/www.mindsec.com/files/x86regs.html) [Linux内存分配小结--malloc、brk、mmap](https://blog.csdn.net/gfgdsg/article/details/42709943) [认真分析mmap:是什么 为什么 怎么用](https://www.cnblogs.com/huxiao-tee/p/4660352.html) [glibc内存管理ptmalloc源代码分析](https://paper.seebug.org/papers/Archive/refs/heap/glibc%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86ptmalloc%E6%BA%90%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90.pdf)