# 2018q3 Homework3(dict)
contributed by < `type59ty` >
###### tags: `sysprog2018`, `hw3`
---
- 實驗環境
```
System: Linux pop-os 4.15.0-34-generic
gcc version: 7.3.0
memory: 15.5G
cpu: Intel® Core™ i7-4770 CPU @ 3.40GHz × 8
```
## 預期目標
- [ ] 學習 [ternary search tree](https://en.wikipedia.org/wiki/Ternary_search_tree) 作為 [auto-complete](https://en.wikipedia.org/wiki/Autocomplete) 和 prefix search 的實作機制
- [ ] 學習 [Bloom filter](https://en.wikipedia.org/wiki/Bloom_filter) 和 [ternary search tree](https://en.wikipedia.org/wiki/Ternary_search_tree) 的整合議題
- bit-wise operation 的實踐
- [ ] 設計效能評比 (benchmark) 的程式框架
- [ ] 學習 GNU make 和 `Makefile` 撰寫
- [ ] 複習機率統計和對應的數學基礎
## Trie (字典樹) 和 Ternary search tree (TST)
### Trie
- 定義: 字典樹(Trie),也被稱為單詞搜尋樹,是一種很特別的樹狀資料結構,可以快速的依照字母插入、尋找字串,特別**適用於大量字串出現**的時候。
- 優點: 跟 binary search tree 相比, Trie 不把整個字串包在同一個 node 中 ,而是根據 node 的位置決定代表的字串為何。也就是一個看結果(最終 node),一個看過程 (經過的 node)。因此利用 trie,可以**快速的找出有相同 prefix 的字串**。
- 缺點: 若是**資料非常大量,而且幾乎沒有相同的 prefix**,將會非常**浪費儲存空間**。
### Ternary search tree
- 定義:Ternary search tree (TST),這種樹的每個節點最多只會有 3 個 children,分別代表小於,等於,大於。
- 優點:解決 Trie 空間浪費的缺點,同時保有 Trie 的搜尋效率和 Binary search tree (BST) 的空間利用率優點
- 缺點: TST 插入的順序會影響比較的次數
- [Trie v.s Ternary search tree](https://www.cnblogs.com/rush/archive/2012/12/30/2839996.html)
## [ternary search tree](https://en.wikipedia.org/wiki/Ternary_search_tree)
- 從 github 上 fork 後 clone 下來
```c
forest@pop-os:~$ git clone https://github.com/type59ty/dict.git
```
- 測試:
```c
forest@pop-os:~/dict$ ./test_cpy
ternary_tree, loaded 259112 words in 0.101007 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:a
enter word to add: NCKU
NCKU - inserted in 0.000009536743 sec. (259113 words in tree)
choice:f
find word in tree: NCKU
found NCKU in 0.000003 sec.
choice: s
find words matching prefix (at least 1 char): Ta
Ta - searched prefix in 0.002033 sec
suggest[0] : Tañgo,
suggest[1] : Taşca,
suggest[2] : Taşköprü,
……
```
## 程式碼修正
觀察 test_cpy.c 和 test_ref.c 兩者的輸出,發現只有 test_ref 用到 bloom filter 的機制,需要在 test_cpy.c 中補上程式碼
### ./test_cpy
```c
find word in tree: Taiwan
found Taiwan in 0.000001 sec.
```
### ./test_ref
```c
find word in tree: Taiwan
Bloomfilter found Taiwan in 0.000001 sec.
Probability of false positives:0.009693
----------
Tree found Taiwan in 0.000001 sec.
```
### 宣告
- #include "bloom.h"
- #define TableSize 5000000
- #define HashNumber 2
- bloom_t bloom = bloom_create(TableSize);
- tst_ins_del 這個函式來自 tst.h , 當我們要在 TST 中插入/刪除一個單字的時候,函式會將單字一個個插入/刪除 TST
```c
while ((rtn = fscanf(fp, "%s", word)) != EOF) {
char *p = word;
if( !tst_ins_del(&root, &p, INS, CPY) ){
…
}
…
}
```
- 因為要加入 bloom filter 機制,所以讀檔時要把單字一併加入 bloom table,因此在底下加入:
```c
while ((rtn = fscanf(fp, "%s", word)) != EOF) {
char *p = word;
if( !tst_ins_del(&root, &p, INS, CPY) ){
…
}else { /* update bloom filter */
bloom_add(bloom, word);
}
idx++;
word += (strlen(word) + 1);
}
```
:::warning
- 先執行看看,結果得到錯誤訊息
```c
test_cpy.c:59:14: error: assignment to expression with array type
word += (strlen(word) + 1);
^~
```
:::
- 回頭看一下 word 的型態為 array,所以要改成:
```c
*word += (strlen(word) + 1);
```
- 其他部份也一併修改,完成 test_cpy.c
- 而 test_ref.c 中也缺少一些程式碼:
```c
if (argc == 2 && strcmp(argv[1], "--bench") == 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");
```
- 修改完成,執行 ./test_cpy --bench 測試
```c
forest@pop-os:~/dict$ ./test_cpy --bench
ternary_tree, loaded 259112 words in 0.139431 sec
```
- ./test_ref --bench
```c
forest@pop-os:~/dict$ ./test_ref --bench
ternary_tree, loaded 259112 words in 0.140784 sec
程式記憶體區段錯誤 (核心已傾印)
```
:::warning
雖然資料都有成功 load 到 bench_ref.txt,可是卻顯示 segmentation fault,這方面需要再研究一下...
:::
### bench.c
這個程式會 copy 每個單字的前3個字元( prefix ),使用 TST 搜尋並將花費時間紀錄下來
```c
char prefix[3] = "";
……
strncpy(prefix, word, 3);
……
```
但是字串的結尾都會擺一個 null character ,並佔用一個 char 空間,所以宣告是錯的,應該改成:
```c
char prefix[4] = "";
```
還有一個地方也有問題,就是關於時間的單位,一開始宣告 sec 為 10^-9^秒
```c
sec /= 1e9;
```
但最後紀錄時間時,後面乘上 10^6^ ,所以單位應該變成 micro sec (10^-3^ sec)
```c
fprintf(fp, "%d %f msec\n", idx, (t2 - t1) * 1000000);
```
## Makefile 修改
- 原本的 Makefile
```c
TESTS = test_cpy test_ref
TEST_DATA = s Tai
CFLAGS = -O0 -Wall -Werror -g
# Control the build verbosity
ifeq ("$(VERBOSE)","1")
Q :=
VECHO = @true
else
Q := @
VECHO = @printf
endif
GIT_HOOKS := .git/hooks/applied
.PHONY: all clean
all: $(GIT_HOOKS) $(TESTS)
$(GIT_HOOKS):
@scripts/install-git-hooks
@echo
OBJS_LIB = \
tst.o bloom.o
OBJS := \
$(OBJS_LIB) \
test_cpy.o \
test_ref.o
deps := $(OBJS:%.o=.%.o.d)
test_%: test_%.o $(OBJS_LIB)
$(VECHO) " LD\t$@\n"
$(Q)$(CC) $(LDFLAGS) -o $@ $^ -lm
%.o: %.c
$(VECHO) " CC\t$@\n"
$(Q)$(CC) -o $@ $(CFLAGS) -c -MMD -MF .$@.d $<
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
clean:
$(RM) $(TESTS) $(OBJS)
$(RM) $(deps)
rm -f bench_cpy.txt bench_ref.txt ref.txt cpy.txt caculate
-include $(deps)
```
- 修改 $make test
前面增加一行刪除 cpy.txt 和 ref.txt,如果 test 跑第二次以上,他的資料會不斷 append 到這兩個檔案,所以應該是每次 test 都要先清空
```c
test: $(TESTS)
rm -f cpy.txt ref.txt
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)
```
- 加入 $make plot
calculate.c 會讀取 cpy.txt 和 ref.txt,並分別計算執行100次 test 的平均值,寫入 output.txt 檔案,以供 runtimept.gp 繪製圖表
```c
plot:
gcc -o calculate calculate.c
./calculate
gnuplot scripts/runtime*.gp
eog runtime*.png
```
- 修改 $make clean
新增一段刪除 gnuplot 所產生的圖表
```c
clean:
$(RM) $(TESTS) $(OBJS)
$(RM) $(deps)
rm -f bench_cpy.txt bench_ref.txt ref.txt cpy.txt runtime*.png caculate
```
- 修改 $make bench
執行 prefix 效能評估
```c
bench:
./test_cpy --bench
./test_ref --bench
```
## Linux 效能分析工具: Perf
- 這邊使用分別執行100次 test_cpy --bench 和 test_ref --bench
並使用 perf 觀察其 cache-miss、cache-reference、instruction 和 clock cycle
```c
forest@pop-os:~/dict$ sudo make test
rm -f cpy.txt ref.txt
echo 3 | sudo tee /proc/sys/vm/drop_caches;
3
perf stat --repeat 100 \
-e cache-misses,cache-references,instructions,cycles \
./test_cpy --bench s Tai
ternary_tree, loaded 259112 words in 0.132259 sec
```
```c
Performance counter stats for './test_cpy --bench s Tai' (100 runs):
1,201,574 cache-misses # 44.844 % of all cache refs ( +- 0.31% )
2,679,453 cache-references ( +- 0.22% )
586,983,628 instructions # 0.97 insn per cycle ( +- 0.03% )
604,831,839 cycles ( +- 0.15% )
0.161409468 seconds time elapsed ( +- 0.19% )
```
```c
Performance counter stats for './test_ref --bench s Tai' (100 runs):
1,274,843 cache-misses # 44.349 % of all cache refs ( +- 0.87% )
2,874,558 cache-references ( +- 0.20% )
588,566,508 instructions # 0.95 insn per cycle ( +- 0.00% )
619,669,874 cycles ( +- 0.36% )
0.164743728 seconds time elapsed ( +- 0.37% )
```
## gnuplot
- $make plot
```c
plot:
gcc -o caculate calculate.c
./caculate
gnuplot scripts/runtime*.gp
eog runtime*.png
```
在 scripts 資料夾內有幾個 .gp 檔,是用來製作 gnuplot 用的,但其中有一些部份要改:
- runtime3.gp
觀察程式碼可以發現,這個程式會讀取 bench_cpy.txt 的資料,
其中 plot 這邊 x 軸要改成 247613 (因為有 0~247613筆資料), y 軸讓它自動設定,( x 軸如果也自動設定,它會跳到下一個級距,會空一塊在那邊)
且 y軸時間單位應改為 micro sec ( ms )
```c
reset
set xlabel 'prefix'
set ylabel 'time( ms )'
set title 'perfomance comparison'
set term png enhanced font 'Verdana,10'
set output 'runtime3.png'
set format x "%10.0f"
set xtic 30000
set xtics rotate by 45 right
plot [:247614][:]'bench_cpy.txt' using 1:2 with points title 'cpy',\
```
- runtimept.gp 也一樣
```c
reset
set xlabel 'prefix'
set ylabel 'time( ms )'
set title 'perfomance comparison'
set term png enhanced font 'Verdana,10'
set output 'runtime2.png'
set format x "%10.0f"
set xtic 30000
set xtics rotate by 45 right
plot [0:247613][:]'bench_cpy.txt' using 1:2 with points title 'cpy',\
'bench_ref.txt' using 1:2 with points title 'ref',\
```
最後結果:
![](https://i.imgur.com/uYT2Aop.png)
![](https://i.imgur.com/GuXB4p4.png)
![](https://i.imgur.com/GyPzT0A.png)