Try   HackMD

2017q3 Homework2 (prefix-search)

contributed by <yusihlin>

Reviewed by zhanyangch

  • 建立 TST 的時間的圖中,REF 有過大的離群值,像是此圖,有一次的執行時間高達0.5左右,希望同學能夠解釋原因,如果是誤差,應在畫圖時將其去除,可以使圖片要呈現的資訊更明確。
  • 有提到 Circular buffer 部份功能未使用,可以針對 Circular buffer 與 memory pool 作比較,提出其分別適合哪些應用場景。

Ternary search tries

  • 每個節點儲存一個 character 、一個 value 和 三個指向 character 的指針
  • 每個節點有三個子樹:low (left), equal (middle), high (right)
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

搜尋

  • 從根節點開始檢查,若資料中第一個 character 小於根節點,則調用 low link,大於根節點調用 high link,相等則為 equal link 並繼續檢查下一個 character
  • 找到最後會有兩種情況
    • Search hit : 找到一個 value
    • Search miss : 找不到 value 或找到一半就沒有 link
      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

插入

  • 先檢查要新增的資料是否已在 TST 上,若無則新增節點,查找過程中如有 equal link 時刪除資料的第一個 character

test_cpy 測試

  • zhanyangch 發現當進行 prefix-search 時產生 Segmentation fault,使用 GDB debugging
$ gdb test_cpy
...
(gdb) run
...
choice: s
find words matching prefix (at least 1 char): A

Program received signal SIGSEGV, Segmentation fault.
0x0000000000401d81 in tst_suggest (p=0x12e1a30, c=65 'A', nchr=1, 
    a=0x7fffffffbc80, n=0x7fffffffbc40, max=1024) at tst.c:330
330	        a[(*n)++] = (char *) p->eqkid;
  • 到 tst.c 查看,當 tst_suggest 執行時多次進行遞迴呼叫,但當 *n = 1024 時執行到 a[(*n)++] = (char *) p->eqkid 對 a[1024] 存取,導致寫入不合法的記憶體位置,*n 又會繼續增加並寫入
    • 修正 (!p *n == max) 為 (!p *n >= max) 進行 1024 次查找
    • a[(*n)++] = (char *) p->eqkid 添加 *n != max 的判斷不寫入 a[1024]

tst_ref 修正

  • insert reference to each string : 進行 prefix searching 時使用 REF,將 tst_ins_del(&root, &p, INS, CPY) 改為 tst_ins_del(&root, &p, INS, REF)

測試 'q'

choice: q
*** Error in `.../test_ref': free(): invalid pointer: 0x00007fffffffdd10 ***
  • 執行 quit 會執行 tst_free_all,觀察內容發現它會 free tst_node 所儲存的字串

  • 從 tst_ins_del 觀察 CPY 和 REF 的差異

    • CPY : 使用 strdup(),功能為先用 maolloc() 配置與參數 s 字串相同的空間大小,然後將 s 字串的內容複製到分配好的位址並回傳位址,此地址可以被 free
    • REF : 直接將 *s 的位置 assign 給 curr->eqkid,變數的位址沒有另外配置空間,可能在外部(e.g. stack),不能被 free
  • tst_free_all 改為 tst_free

    case 'q':
        tst_free(root);
        return 0;
        break;
  • 測試 'a' 新增 word 後用 'q' 刪除同樣會有 invalid pointer 的問題,概念同上,修改 tst_ins_del 內呼叫 tst_del_word 傳入的參數,如此當傳入 REF 時字串存在外部不會被 free
...
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 */
...

測試 's'

choice: s
find words matching prefix (at least 1 char): Tain
  Tain - searched prefix in 0.000017 sec

suggest[0] : Tain
suggest[1] : Tain
suggest[2] : Tain
suggest[3] : Tain
suggest[4] : Tain
suggest[5] : Tain
suggest[6] : Tain
suggest[7] : Tain
suggest[8] : Tain
suggest[9] : Tain
suggest[10] : Tain
suggest[11] : Tain
  • 參考 st9007a 的共筆在 loading words 前預先配置一塊空間供存放,優點是一次分配所需的記憶體,但由於不知道 word 的大小給每個 word 都配置 WRDMAX=256 的空間,所以有些空間被浪費掉了
  1. 可考慮用 circular buffer
  2. Swoole 的 memory pool 實作探討: Swoole源码学习记录

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
jserv

enum { INS, DEL, WRDMAX = 256, STKMAX = 512, LMAX = 1024, CITYMAX = 260000 };

char (*city_space)[WRDMAX] = malloc(sizeof *city_space * CITYMAX);
  • loading words 時改存入剛剛配置好的空間
while ((rtn = fscanf(fp, "%s", city_space[idx])) != EOF) {
    char *p = city_space[idx];
    /* FIXME: insert reference to each string */
    if (!tst_ins_del(&root, &p, INS, REF)) {
        fprintf(stderr, "error: memory exhausted, tst_insert.\n");
        fclose(fp);
        return 1;
    }
    idx++;
}
  • 修正 add word 時改存到 city_space
case 'a':
    printf("enter word to add: ");
    if (!fgets(city_space[idx], sizeof city_space[idx], stdin)) {
        fprintf(stderr, "error: insufficient input.\n");
        break;
    }
    rmcrlf(city_space[idx]);
    p = city_space[idx];
...
  • 離開時釋放剛剛配置的空間
case 'q':
    tst_free(root);
    free(city_space);
    return 0;
    break;

效能評比

  • 修改 Makefile 以執行 bench 測試,參考 HMKRL 搭配 chrt, taskset 做測試
bench:
        echo 3 | sudo tee /proc/sys/vm/drop_caches
        perf stat --repeat 100 \
                -e cache-misses,cache-references,instructions,cycles \
                sudo chrt -f 99 taskset -c 0 ./test_cpy --bench
        echo 3 | sudo tee /proc/sys/vm/drop_caches
        perf stat --repeat 100 \
                -e cache-misses,cache-references,instructions,cycles \
                sudo chrt -f 99 taskset -c 0 ./test_ref --bench
  • 在 test_cpy 和 test_ref 加入接受 bench 的執行參數,並將loading words 和 prefix searching 測試的結果分別存進 txt 中
if (argc == 2 && strcmp(argv[1], "--bench") == 0) {
        /* build loading time txt */
        fp = fopen (BENCH_LOAD_OUT,"a+");
        if (!fp) {
            fprintf(stderr, "error: file open failed in '%s'.\n", BENCH_LOAD_OUT);
            return 1;
        }
        fprintf(fp, "%.6f\n",t2 - t1);
        fclose(fp);
        /* build searching time txt */
        int r = bench_test(root, BENCH_SEARCH_OUT, sgl, &sidx, LMAX);
        tst_free_all(root);
        return r;
    }
  • 直接將原本 loading words 的時間存入,然後進到 bench.c 中做 prefix searching,搜尋的方式從 A-Z, a-z
int bench_test(const tst_node *root, 
               char *search_out, 
               char **a, 
               int *n,
               const int max)
{
    char word[] = "A";
    int i;
    double t1, t2, search_time;

    FILE *fp = fopen (search_out, "a+");
    if (!fp) {
        fprintf(stderr, "error: file open failed in '%s'.\n", search_out);
        return 1;
    }
    for (i = 0; i < 25; i++) {
        t1 = tvgetf();
        tst_search_prefix(root, word, a, n, max);
        t2 = tvgetf();
        search_time += t2 - t1; 
        word[0]++;
    }
    word[0] = 'a';
    for (i = 0; i < 25; i++) {
        t1 = tvgetf();
        tst_search_prefix(root, word, a, n, max);
        t2 = tvgetf();
        search_time += t2 - t1; 
        word[0]++;
    }
    fprintf(fp, "%.6f\n",search_time);
    fclose(fp);

    return 0;
}
  • 搜尋 word 的方式參考 zhanyangch ,只不過我把要寫入的 search.txt 傳入 bench.c 與 loading 的做區隔
Performance counter stats for 'sudo chrt -f 99 taskset -c 0 ./test_cpy --bench' (100 runs):

               137      cache-misses              #   24.244 % of all cache refs      ( +- 11.26% )
               567      cache-references                                              ( +-  7.32% )
             1,852      instructions              #    0.40  insn per cycle           ( +-  8.18% )
             4,614      cycles                                                        ( +- 10.57% )

       0.207701012 seconds time elapsed                                          ( +-  3.43% )

Performance counter stats for 'sudo chrt -f 99 taskset -c 0 ./test_ref --bench' (100 runs):

               110      cache-misses              #   21.932 % of all cache refs      ( +-  2.52% )
               500      cache-references                                              ( +-  0.76% )
             1,676      instructions              #    0.45  insn per cycle         
             3,743      cycles                                                        ( +-  2.19% )

       0.225630593 seconds time elapsed                                          ( +-  3.05% )
  • 測試結果看到執行時間 CPY 比 REF 短,但 cache miss 較 REF 多,但這包含了建立 TST 和搜尋的時間,應該要分別獨立來看
    • 建立 TST 的時間
    • Prefix searching 的時間
  • 求平均和標準差我用了 gnuplot 裡的 fit command 利用原本的資料建立 data set,利用 fit 回傳 mean,使用 fit 時同時會設置兩個變數,一個為離均差平方和 FIT_WSSR,另一個是自由度 (degrees of freedom) FIT_NDF,所以可以用 sqrt(FIT_WSSR / (FIT_NDF + 1 )) 求標準差,因為自由度比樣本數少一
  • 建立 TST 時 CPY 會因字串長度分配記憶體,而記憶體通常是連續的,REF 先分配一塊足夠大的空間,每次分配的時候要去找位址,每個字的位置是不連續的,會多花點時間
  • 搜尋的時候差距就更大了,同樣是因為空間分配的原因,記憶體配置的問題之後可以試著使用 Circular Buffer 改善

forward declaration

  • 使用 forward declaration 的好處是可以減少不必要的 #include,減少重新編譯的時間,但是後面的宣告需採用指標的方式,還有個限制是如果需用到 struct 的大小、成員便不可用此方法
  • 應用在 phone 上,可以將 entry 使用 forward declaration 建立一個共同的 *.h,在實作上可以分別對不同的方法做 declaration

Circular buffer 測試

  • 參考環形緩衝區自己建立了一個有部份功能的 circular buffer
  • Circular buffer 是一種用於表示一個固定尺寸、投尾相連的緩衝區的資料結構,適合快取資料流
    • 一般含 4 個指標,長度、開始位置、結尾位置以及實際的 buffer
    • 先進先出,當一個資料元素被用掉後,其餘的資料元素不需要移動其儲存位置,假如 buffer滿了,從最開始的地方開始覆蓋
// Circular buffer object
typedef struct {
    int size; // maximum number of elements
    int start; // index of oldest element
    unsigned int end; // index at which to write new element
    char *elems; // vector of elements
} CircularBuffer;

void cbInit(CircularBuffer *cb, int size) {
    cb->size = size;
    cb->start = 0; 
    cb->end = 0;
    cb->elems = (char *)calloc(cb->size, sizeof(char));
}
  • 應用在 REF 之中也只是拿來做資料儲存用,跟 memory pool 很像,如果在此案例分配不足的空間 load words,將會覆蓋原本的儲存空間在後續 prefix searching 的處理上會產生錯誤 ( prefix searching 讀到後面覆蓋的資料 ),所以我還是讓空間比要寫入的資料大
  • 測試 Circular buffer 的性質我在分配空間後指向 CircularBufferSize 一半的位置,start, end 也移至相應的位置開始存放資料
/* start store from the middle */
buffer.start = CircularBufferSize >> 1;
buffer.end = CircularBufferSize >> 1;
buffer_head += (CircularBufferSize >> 1);

while ((rtn = fscanf(fp, "%s", buffer_head)) != EOF) {
    char *p = buffer_head;
    /* FIXME: insert reference to each string */
    if (!tst_ins_del(&root, &p, INS, REF)) {
        fprintf(stderr, "error: memory exhausted, tst_insert.\n");
        fclose(fp);
        return 1;
    }
    idx++;
    int word_len = strlen(buffer_head) + 1;
    if( WRDMAX + buffer.end >= CircularBufferSize) {
        buffer_head -= buffer.end;
        buffer.end = 0;
    } else {
        buffer.end = word_len + buffer.end;
        buffer_head += word_len;
    }     
}
  • 當空間存到不足以放 WRDMAX 時,則跳至空間起始開始存放,由 GDB 觀看 loading 完後的 buffer 狀況
Breakpoint 1,...
(gdb) p buffer.start
$1 = 1500000
(gdb) p buffer.end
$2 = 679425
(gdb) p buffer.elems+1500000
$3 = 0x7ffff789e370 "Shanghai,"
(gdb) p buffer.elems+2999750
$4 = 0x7ffff7a0c5d6 "Belaya"
(gdb) p buffer.elems
$5 = 0x7ffff7730010 "Gora,"
  • 我設置的 CircularBufferSize = 3000000,所以 cities 的第一筆 Shanghai 存在中間 buffer.elems+1500000,而 Belaya 之後的空間不足以放 WRDMAX 所以下一筆 Gora, 存在空間起始位置
  • 因為在此案例只是做儲存,所以 Circular buffer 部份功能沒使用到,也應該說在此案只需使用 memory pool 即可達到連續分配空間的效果

效能比較

Performance counter stats for 'sudo chrt -f 99 taskset -c 0 ./test_cpy --bench' (100 runs):

               116      cache-misses              #   22.543 % of all cache refs      ( +-  3.40% )
               513      cache-references                                              ( +-  1.56% )
             1,688      instructions              #    0.42  insn per cycle           ( +-  0.72% )
             4,036      cycles                                                        ( +-  3.10% )

       0.211468978 seconds time elapsed                                          ( +-  3.76% )

Performance counter stats for 'sudo chrt -f 99 taskset -c 0 ./test_ref --bench' (100 runs):

               126      cache-misses              #   23.132 % of all cache refs      ( +-  8.87% )
               543      cache-references                                              ( +-  5.52% )
             1,830      instructions              #    0.41  insn per cycle           ( +-  8.40% )
             4,471      cycles                                                        ( +-  8.84% )

       0.221327696 seconds time elapsed                                          ( +-  5.16% )
  • 跟原本使用 2D array 分別與 CPY 的差異在 cache miss 反而上升了,執行時間差異不大

  • 在 loading words 時 REF 的速度比 CPY 快了,因從不連續的空間儲存改成使用連續的空間會比 strdup 快一些, prefix searching 的差異跟原本的方法差不多,時間略為下降

參考資料

不要只貼網址,要把參照的文件/論文標題寫出來
"jserv"
已修正
"yusihlin"