# concurrent B+ tree contributed by <`vic85821`>, <`snoopy831002`>, <`ruby0109`>, <`jkrvivian`> ###### tags: `B+tree` `database` ## 研究動機 提出一個並行化 (concurrent) 的 B+ tree 實作,並且探討其中資料結構設計、read-write lock,以及該如何延展為一個高效能的資料庫系統 ## 預期目標 * 延續春季班 [concurrent B+ tree](https://embedded2016.hackpad.com/concurrent-B-tree-p459m7tm2Ea) 成果,回頭研究程式行為和深入效能分析 * 提出 lock contention 的解決機制 ## 產出 * [程式碼](https://github.com/sysprog-Concurrent-BTree/bplus-tree) * [影片](https://www.youtube.com/watch?v=cmou2DCU4PI&index=2&list=PLjavDKFcyCOkeIJbh1vJKk8KFh5EyymV_) * [共同討論區](https://hackmd.io/JwdgJgDAHFDMBMBaARgYyhRAWAhgNh0SkiQEYBTKUgMxGVIlmWCA?view) ## What is a concurrent B+ tree ### B-tree * 相關名詞[^閱讀與整理]: * **node** :節點, 包含不同數目的 key * **leaf**:沒有 child 的 node(External node) * **Internal node**: 最少有一個 child 的 node * B-tree 特性[^B-tree] * 二元樹:每個節點最多有兩個子樹的樹結構 * 平衡的搜尋樹 : 所有的 leaf 在同一個階層上 * B-tree 的設計取決於兩個參數 M 和 L,分別是根據 data records 的大小和 block 的大小找到效能最好的值 * internal node 的 children 個數要介於 $\lceil$$\frac{M}{2}$$\rceil$~M * leaf 內 data records 個數要介於 $\lceil$$\frac{L}{2}$$\rceil$~L * 除了 M, B-tree 也會用 branch factor B 表示 B <= number of children < 2B B-1 <= number of keys < 2B-1 * **Self-rebalancing** : 避免 skew (傾斜), 如果傾斜, 尋找的時間可能比 linked list 還長 * B-tree 存的為 data records, 每個 data record 都有一個獨特的 key (e.g. integer) plus additional data,可以依循 key 在樹中搜尋[^Lecture_16_Btree_property] * M=4, L=3, B-tree範例 ![](https://i.imgur.com/UKU9iqV.png) * 一個 leaf 存有 L/2 到 L 個data records (inclusive) * 一個 internal node 有 M/2 到 M 個children (inclusive) * 每個 internal node 一定存有 2, 3, or 4 children * 操作: * Search: * 從 root node 開始依照 key 值比較大小,依序往下尋找 * 操作模式: (1) 從 disk 讀取 root node 到記憶體中,先尋找此 key 屬於哪個 internal node (2) 進入此 internal node (3) load 整個 internal node 所屬的 leaf 的 key 進入記憶體中 (4) 在此 leaf 裡面做搜尋(可以使用 binary search) * Insertion * 範例圖: ![](https://upload.wikimedia.org/wikipedia/commons/3/33/B_tree_insertion_example.png) * 步驟解釋: * 找尋新的 key 在 leaf node 應該插入的位置 * 插入的 node 如果沒有滿(插入後 key 小於等於 2B-1 個),直接插入 * 如果滿了 * 從要插入的 node 中和新的 key 選一個中間值送到 parent node 中 * 如果 parent node 也滿了, 從 parent node 中和要傳進去的 key 值選中間值, 並把中間值繼續往上傳 * 重複步驟上一步直到每個 internal node 皆符合規則 * 如果沒有 parent node 則另建一個 parent node, 原本的 parent node 則依值大小 split * Deletion[^B-tree_deletion] ![](https://www.cpp.edu/~ftang/courses/CS241/notes/images/trees/b-tree15.bmp) ![](https://www.cpp.edu/~ftang/courses/CS241/notes/images/trees/b-tree16.bmp) 圖片參考[^deletion_pic] * **從 leaf node 刪除** * 若無 underflow (刪除後 key 大於 B-1 個) 則直接刪除 * 若有 underflow, 要 rebalance * **從 internal node 刪除** * 原本的 internal node 中的 key 值是底下的 seperator * 若刪去則要從 child node 中找右子樹的最小值或是左子樹的最大值補原本的 internal node key 值 * 一直從下往上補到 leaf node, 若 leaf node underflow, 一樣要 rebalance * **Rebalance after deletion** * 如果 deficient node ( underflow 的 node ) 的右子樹存在而且有超過最少數量的key 值則左旋 * 把右子樹的最小值轉到 parent node 中 * 如果 deficient node ( underflow 的 node ) 的左子樹存在而且有超過最少數量的 key 值則右旋 * 把左子樹的最大值轉到 parent node 中 * 如果左子樹和右子樹原本都只有最少個數的 key 值 * 左子樹, deficient node 和右子樹 merge * 為什麼要使用 B-tree? * 存取 disk 中的資料需要耗費很長的時間 * key 可以幫助尋找資料, 可是大量資料的 key 會太大, 需要有效率的方法搜尋 key * 相較於 Binary tree, B-tree 有許多 branch 因此深度不深, 可以減少往下找尋的時間( B+ tree 通常只有三到四層 level) ### B+ tree * B+ tree特性[^b+tree] * B+ tree 的 internal node 只有放 index * 資料都放在 leaf node * leaf node 彼此之間以 linked list 串連 * 所有 leaf node 在同一個 level * B+ tree 的 leaf node 會有 internal node 的 key 值 * B+ tree 的 order, 或稱 branching factor, 控制 children node 的最大個數 摘自Wikipedia圖如下: ![](https://i.imgur.com/C03sPgH.png) * Time Complexity: $O(logn)$ * B tree 與 B+ tree * 參考[^comparison] ![](http://i.stack.imgur.com/l6UyF.png) * B tree 資料直接放在 internal node, 所以 leaf node 不會有 internal node 的 key 值; B+ tree 的 leaf node 則會有 internal node 的 key 值 * 操作: * Search: * 概念大致和 B-tree 相同 * 不過因為資料都在 leaf node, 所以都要走到最下層 * Insertion: * 概念大致和 B-tree 相同 * 不過如果 node 滿了, leaf node 是把中間值複製傳到 parent node ; internal node 則是把中間值直接移動到 parent node * Deletion * 概念大致和 B-tree 相同 * 不過因為 internal node 是 leaf node 的 copy, 所以 rebalance 方法不太一樣 * 為什麼要使用 B+ tree? * 因為 B+ tree 的 internal node 沒有放置資料, 所以一個 page memory 可以放更多的 key 值, 可以有較少的 cache miss。 * 當需要走訪所有資料時, 因為 leaf node 彼此相連接所以直接線性的走訪 (leaf nodes 之間是一條 linked list); B tree 則需要走訪 tree 的每一個 level, 可能會有較多的cache miss * 不過 B tree 因為 internal 可以連接到資料, 常用到的 node 會比較接近 root, search 時可能可以較快找到 * B+ tree 應用 * 通常用於資料庫和作業系統的檔案系統中 * 核心數量增加,throughput 也有可能增加 ### B+ tree implementation in C B+ tree 程式有不同實作的 benchmark 測試,包含 `bench-basic`, `bench-bulk`,`bench-multithreaded`,以下為測試結果 * 安裝所需的套件 * `$ sudo apt-get install automake` * `$ sudo apt-get install libtool` * 編譯 * `$ make MODE=release` * Benchmarks * 執行 bench-basic * 結果 ``` -- basic benchmark -- 0 items in db write : 41056.296394 ops/sec read : 176397.953784 ops/sec 20000 items in db write : 37656.698938 ops/sec read : 170695.798750 ops/sec 40000 items in db write : 35896.589106 ops/sec read : 172093.343429 ops/sec 60000 items in db write : 33229.325544 ops/sec read : 130221.686143 ops/sec 80000 items in db write : 29975.569911 ops/sec read : 124544.013232 ops/sec 100000 items in db write : 31257.667897 ops/sec read : 121469.907349 ops/sec 120000 items in db write : 29781.448838 ops/sec read : 126126.921521 ops/sec 140000 items in db write : 30671.083303 ops/sec read : 122893.528120 ops/sec 160000 items in db write : 30173.285177 ops/sec read : 126731.201010 ops/sec 180000 items in db write : 30057.786094 ops/sec read : 124198.454226 ops/sec 200000 items in db write : 27118.276362 ops/sec read : 127897.384440 ops/sec 220000 items in db write : 26180.032228 ops/sec read : 122599.351449 ops/sec 240000 items in db write : 30706.070744 ops/sec read : 127651.530959 ops/sec 260000 items in db write : 29897.779492 ops/sec read : 129733.837133 ops/sec 280000 items in db write : 29910.612136 ops/sec read : 122661.258668 ops/sec 300000 items in db write : 29868.756683 ops/sec read : 129111.338760 ops/sec 320000 items in db write : 29684.821409 ops/sec read : 126493.365795 ops/sec 340000 items in db write : 29846.425219 ops/sec read : 128427.910382 ops/sec 360000 items in db write : 29220.926274 ops/sec read : 130370.984359 ops/sec 380000 items in db write : 26437.087003 ops/sec read : 128875.404226 ops/sec 400000 items in db write : 29018.268451 ops/sec read : 130044.769460 ops/sec 420000 items in db write : 28444.848993 ops/sec read : 131498.412456 ops/sec 440000 items in db write : 26472.885147 ops/sec read : 131911.177895 ops/sec 460000 items in db write : 29744.022939 ops/sec read : 131058.092045 ops/sec 480000 items in db write : 25876.367889 ops/sec read : 95427.040779 ops/sec compact : 9.484728s read_after_compact : 134711.957545 ops/sec read_after_compact_with_os_cache : 134900.517613 ops/sec remove : 49887.643050 ops/sec ``` * 執行 bench-bulk * 結果 ``` -- bulk set benchmark -- 0 items in db bulk : 130260.064218 ops/sec 20000 items in db bulk : 134681.041623 ops/sec 40000 items in db bulk : 133731.854259 ops/sec 60000 items in db bulk : 134107.581102 ops/sec 80000 items in db bulk : 135155.225777 ops/sec 100000 items in db bulk : 134989.200864 ops/sec 120000 items in db bulk : 134148.059213 ops/sec 140000 items in db bulk : 134671.065921 ops/sec 160000 items in db bulk : 134590.407742 ops/sec 180000 items in db bulk : 134418.539005 ops/sec 200000 items in db bulk : 134908.161269 ops/sec 220000 items in db bulk : 134883.595457 ops/sec 240000 items in db bulk : 134897.242026 ops/sec 260000 items in db bulk : 134239.900126 ops/sec 280000 items in db bulk : 135067.601334 ops/sec 300000 items in db bulk : 134938.198305 ops/sec 320000 items in db bulk : 134256.120401 ops/sec 340000 items in db bulk : 134908.161269 ops/sec 360000 items in db bulk : 134975.535684 ops/sec 380000 items in db bulk : 133141.609416 ops/sec 400000 items in db bulk : 134733.665227 ops/sec 420000 items in db bulk : 134433.898852 ops/sec 440000 items in db bulk : 134024.901827 ops/sec 460000 items in db bulk : 134015.921091 ops/sec 480000 items in db bulk : 134883.595457 ops/sec ``` * 執行 bench-multithread-get * 結果 ``` -- multi-threaded get benchmark -- get : 208163.874929 ops/sec ``` ## Phonebook 程式以 B+tree 實作 將 b+ tree 套用到作業一的 phonebook 程式,直接引用所需的函式庫並測試效能。測試資料為 phonebook 中的 `words.txt` ### 實作版本: basic basic 版本為每讀入一筆資料就 append 到 b+ tree 中。 * 在原本的 phonebook 程式中加入下列主要操作 ```clike= /* 打開存放資料庫的檔案 BP_FILE */ bp_open(&db, BP_FILE); /* append 資料建樹 */ bp_sets(&db, index, line); /* findName */ bp_gets(&db, input, &foundName); ``` #### 效能比較 使用不同的資料形式進行比較 * **排序的 `words.txt`** B+ tree 在 append() 的時期費了相當多的時間,findName() 的時間縮短近 146 倍,充分顯現 B+ tree 的查詢效能 ![](https://i.imgur.com/mh1VUy3.png) 上圖為 B+ tree basic 與 linked list 效能比較圖 > TODO: 圖應該重繪,且應加入 unsorted 一同比較 > [name=vivian][color=red] ### 實作版本: bulk[^bulk] 先排序部分資料,再一起插入樹中。由於資料已依大小排序,因此每次插入都往樹的右邊填入。 ![](https://i.imgur.com/b0DvpDO.png) ![](https://i.imgur.com/nYwHsNl.png) 上圖為 bulk 插入資料的示意圖 * 把資料分批放進 buffer 中再一起插入,每 20000 筆為一批[^ktvexe作法] ```clike= char *bulk_buffer, *bulk_data[BULK_SIZE]; /* buffer 裡的資料數 */ int bulk_data_count = 0; bulk_buffer = (char*) malloc(BULK_SIZE * MAX_LAST_NAME_SIZE); for(i=0; i < BULK_SIZE; i++) { bulk_data[i] = bulk_buffer + MAX_LAST_NAME_SIZE * i; } strcpy(bulk_data[bulk_data_count++],line); /* 一次插入 20000 筆 */ if(bulk_data_count == BULK_SIZE) { bp_bulk_sets(&db, bulk_data_count, (const char**) bulk_data, (const char**) bulk_data); bulk_data_count = 0; } /* 檢查 buffer 內的資料是否全數插入 */ if(bulk_data_count != 0){ bp_bulk_sets(&db, bulk_data_count, (const char**) bulk_data, (const char**) bulk_data); bulk_data_count = 0; } ``` * bulk with mmap 推測直接一次完成資料插入會縮短 append() 時間。 ```clike= #if !defined(BULK) /* check file opening */ fp = fopen(DICT_FILE, "r"); if (fp == NULL) { printf("cannot open the file\n"); return -1; } #else /* Align data file to MAX_LAST_NAME_SIZE one line*/ file_align(DICT_FILE, ALIGN_FILE, MAX_LAST_NAME_SIZE); int fd = open(ALIGN_FILE, O_RDONLY | O_NONBLOCK); off_t fs = fsize(ALIGN_FILE); #endif ``` 上面程式碼為 align file,使每筆資料的大小都相同 ```clike= /* mmap for data file*/ char *map = (char*) mmap(NULL, fs, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); assert(map && "mmap error"); char *bulk_data[BULK_SIZE]; int bulk_data_count = fs / MAX_LAST_NAME_SIZE; for(i=0; i < bulk_data_count; i++) { bulk_data[i] = map + MAX_LAST_NAME_SIZE * i; } bp_bulk_sets(&db, bulk_data_count, (const char**) bulk_data, (const char**) bulk_data); ``` #### 效能比較 使用 mmap 插入 append() 反而變慢 >TODO: 圖要重畫 未標明使用sorted 或 unsorted 資料[color=red][name=Vivian] * With snappy ![](https://i.imgur.com/LoXEGEp.png) * Without Snappy ![](https://i.imgur.com/SkZoVsz.png) * with Snappy ![](https://i.imgur.com/ew95gak.png) ----- ==以下還在更新中 未統整== ## B+ Tree 效能比較與探討 ### 紅黑樹 **動機** 閱讀春季班的實驗結果發現,紅黑樹的時間比 B+ tree 的時間還短,又紅黑樹和 B+ tree 的時間複雜度都是 O(log n),為何選擇使用 B+ tree 而非紅黑數? **紅黑樹簡介** * 特性: * 平衡的二元樹 * 平衡樹:左右兩個子樹的高度差絕對不超過 1 * 每個 node 不是黑色就是紅色 * root 是黑色 * leaves 是黑色 * 如果某個 node 是紅色,那麼他的小孩全部都會是黑色 * 保證每個 node 從自己到 leaf node 會經過一樣多的黑色 node * $bh(x)$: black-height,從 x 到任何一個它的子孫葉子 node 遇到的 black node 個數,不包含 x * 整棵 rb-tree 的 bh() 為 root 的 bh ### range search ### Compact 在`./test/original_test/bench-basic.cc`中,關於compact時間的計算 ```c== BENCH_START(compact, 0) bp_compact(&db); BENCH_END(compact, 0) ``` 先了解 `BENCH_START` , `BENCH_END` 的實作 * BENCH_START ```c== #define BENCH_START(name, num) \ timeval __bench_##name##_start; \ gettimeofday(&__bench_##name##_start, NULL); ``` * BENCH_END ```c== #define BENCH_END(name, num) \ timeval __bench_##name##_end; \ gettimeofday(&__bench_##name##_end, NULL); \ double __bench_##name##_total = \ __bench_##name##_end.tv_sec - \ __bench_##name##_start.tv_sec + \ __bench_##name##_end.tv_usec * 1e-6 - \ __bench_##name##_start.tv_usec * 1e-6; \ if ((num) != 0) { \ fprintf(stdout, #name " : %f ops/sec\n", \ (num) / __bench_##name##_total); \ } else { \ fprintf(stdout, #name " : %fs\n", \ __bench_##name##_total); \ } ``` 透過linux `sys/time.h` 提供的 `int gettimeofday(struct timeval *tv, struct timezone *tz)` 在`bp_compact(&db);`前後各取時間, 然後因為struct timeval的結構包含`tv_sec`及`tv_usec`, 因此計算上也要一併考慮 ```c== struct timeval { time_t tv_sec; /* seconds */ suseconds_t tv_usec; /* microseconds */ }; ``` 參考資料 : [Linux Programmer's Manual](http://man7.org/linux/man-pages/man2/gettimeofday.2.html) * bp_compact > 看不太懂怎麼實作壓縮的, 還在研究 [name=vic85821] ## 建立 server/client 模擬真實世界資料庫 * server * 根據字典檔的資料建立資料庫( b+tree ) * 監聽是否有 client 進入 * 當有新的 client 進入時,以 pthread 去接受每個 client 的 request ```c== c_fd = accept(s_fd, (struct sockaddr *) &c_addr, &c_addr_len); if(c_fd > 0) { printf("client request\n"); int tmp = thread_num; pthread_create(&tid[tmp], NULL, serve_request, (void *)c_fd); thread_num = (thread_num++) % MAX_CLIENT_NUM; } ``` * client * default : IP **127.0.0.1** port **12345** * 隨機產生 FIND/INSERT/REMOVE 的 request,並傳送給server * request 類別 (case_sensetive) * FIND : 查詢 Data 是否存在於資料庫中 * INSERT : 插入單筆資料 * REMOVE : 移除單筆資料 ## 改善B+ Tree ### 導入真實世界人名 * 我們藉由在 github 上的開源專案取得真實世界的人名資料,總共 160 萬筆人名且沒有重複。 * 此時必須更改 main.c 中所尋找的名字` char input[MAX_LAST_NAME_SIZE] = "資料中的人名";` * 可藉由亂數排列增加實驗的準確性 * 對 35 萬筆資料 (words.txt) 做sort 資料輸入 指令:`uniq words.txt | sort -R > input.txt` `uniq` 可以去掉資料中重複的部份,而`sort`則可以-R(random)做隨機排序。`sort -R` 也可用 `shuf` 代替 * 結果 * 執行時間 ![](https://i.imgur.com/DGBredB.png) 由實驗結果,在 b+tree 中使用 bulk 可以大幅減少 append 的時間。只不過將會消耗很多記憶體(使用空間換取時間) * cache miss * original ``` 408,341 cache-misses # 62.186 % of all cache refs( +- 0.74% ) 656,647 cache-references ( +- 1.66% ) 900,837,598 instructions # 1.65 insns per cycle ( +- 0.02% ) 544,609,222 cycles ( +- 0.39% ) 0.188644139 seconds time elapsed ( +- 0.42% ) ``` * opt ``` 80,260 cache-misses # 36.833 % of all cache refs ( +- 0.73% ) 217,901 cache-references ( +- 0.64% ) 397,626,962 instructions # 1.87 insns per cycle ( +- 0.00% ) 212,462,379 cycles ( +- 0.72% ) 0.073879177 seconds time elapsed ``` * bptree ``` 44,390,741 cache-misses # 12.859 % of all cache refs ( +- 0.18% ) 345,217,948 cache-references ( +- 9.43% ) 861,488,647,030 instructions # 1.75 insns per cycle ( +- 5.57% ) 493,026,082,909 cycles ( +- 4.63% ) 206.798540293 seconds time elapsed ``` * bulk ``` 300,646,774 cache-misses # 11.745 % of all cache refs ( +- 2.61% ) 2,559,844,765 cache-references ( +- 15.13% ) 4,620,076,805,164 instructions # 1.83 insns per cycle ( +- 61.48% ) 2,524,078,084,786 cycles ( +- 60.01% ) 1345.465512628 seconds time elapsed ``` 此次的名字查找是以 dataset 中的 "May" 作為實驗對象。由此次結果可以知道 b+ tree 花了很多時間在建立樹,而在查找名字的時候效能也不是最快的。但是,如果使用 b+ tree 或是 bulk 可以顯著的減少 cache-miss 的發生。這個實驗結果與預期符合,b + tree 在查找資料時因為只要引入資料的 index 而不是資料本身,故可以減少 cache-miss 的產生。 * TODO: - [ ] 嘗試尋找不同的名子,比較bptree與其他之效能上的差異 ` ## lock contention解決機制 * 什麼是lock contention? * 參考[Lock Contention](https://www.jetbrains.com/help/profiler/10.0/Lock_Contention.html)和[What is thread contention?](http://stackoverflow.com/questions/1970345/what-is-thread-contention) * lock contention 是 thread 因為等待 lock 被 block 的時間, 換句話說也是 waiting 的時間, 要注意的是 contention 不限於 lock * 在[What is thread contention?](http://stackoverflow.com/questions/1970345/what-is-thread-contention)提到 若 atomic 的 operation 中有經常使用的參數, threads 的 core 的 cache 間會競爭那個參數的 memory holding 持有權, 因此也會變慢 * 用 mutrace 看 test-threaded-rw 的 lock-contention: $ mutrace --all test/original_test/test-threaded-rw ``` mutrace: Showing 101 most contended mutexes: Mutex # Locked Changed Cont. tot.Time[ms] avg.Time[ms] max.Time[ms] Flags 100 402201 394865 30226 12505.551 0.031 0.497 Wx...r 89 1 0 0 0.005 0.005 0.005 Wx...r 90 1 0 0 0.004 0.004 0.004 Wx...r 91 1 0 0 0.004 0.004 0.004 Wx...r 92 1 0 0 0.004 0.004 0.004 Wx...r 54 1 0 0 0.004 0.004 0.004 Wx...r 93 1 0 0 0.004 0.004 0.004 Wx...r 59 1 0 0 0.003 0.003 0.003 Wx...r 25 1 0 0 0.003 0.003 0.003 Wx... ``` 有lock contention的mutex共 lock 402201 次, 而且lock持有權的轉換有394865次, 造成thread等待的次數則有30226次, waiting的時間總共12505.551(ms)。 * 如何減少 lock contention? [Resolving lock contention](http://www.ibm.com/support/knowledgecenter/SS3KLZ/com.ibm.java.diagnostics.healthcenter.doc/topics/resolving.html)簡介減少 lock contention 的方式, 包括: * 減少 lock 持有的時間 * 移動不會用到共同資源的程式碼到lock外 * 移動造成 blocking 的 process 到 lock 外 * 縮小 lock 保護的資料範圍, 才不會有太多 thread 一起 access 到同一個 lock * [10 ways to reduce lock contention](http://www.thinkingparallel.com/2007/07/31/10-ways-to-reduce-lock-contention-in-threaded-programs/) * 本程式用的 lock - pthread_rwlock * 什麼是 readers-writer lock? 參考[Readers–writer lock](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock) * multiple threads can read the data in parallel but an exclusive lock is needed for writing or modifying data * When a writer is writing the data, all other writers or readers will be blocked until the writer is finished writing. * 先找出是哪個lock造成contention: * addr2line: * 輸入 $ addr2line -e bench-bulk 0x25 * 結果 ??:0 * 後來Vivian 大大發現是應該要輸入中括號中的地址ˊˇˋ ``` Mutex #100 (0x0x7ffe42eaf2f8) first referenced by: /usr/lib/mutrace/libmutrace.so(pthread_rwlock_init+0xd0) [0x7f70b12aacb0] test/original_test/test-threaded-rw(bp_open+0x25) [0x409923] test/original_test/test-threaded-rw(main+0xe8) [0x40953a] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f70b07bcf45] ``` 轉換後: ``` bplus-tree_bulktest/src/bplus.c:12 bplus-tree_bulktest/test/original_test/test-threaded-rw.cc:71 ``` pthread_rwlock(&tree->rwlock,...)造成contention * 先只看test-threaded-rw中的reader和writer 的 lock contention ``` Mutex # Locked Changed Cont. tot.Time[ms] avg.Time[ms] max.Time[ms] Flags 0 400001 381593 5198 7299.340 0.018 33.485 Wx...r |||||| /||||| Object: M = Mutex, W = RWLock /|||| State: x = dead, ! = inconsistent /||| Use: R = used in realtime thread /|| Mutex Type: r = RECURSIVE, e = ERRRORCHECK, a = ADAPTIVE /| Mutex Protocol: i = INHERIT, p = PROTECT / RWLock Kind: r = PREFER_READER, w = PREFER_WRITER, W = PREFER_WRITER_NONREC mutrace: Note that the flags column R is only valid in --track-rt mode! mutrace: Total runtime is 12860.705 ms. ``` * 用grof 下去看各函式執行時間: * 未更改前reader和writer ``` Each sample counts as 0.01 seconds. % cumulative self self total time seconds seconds calls us/call us/call name 17.57 0.23 0.23 399876 0.58 1.09 bp__page_save 13.75 0.41 0.18 39584322 0.00 0.00 htonll 13.36 0.59 0.18 29578508 0.01 0.01 ntohll 11.46 0.74 0.15 304389 0.49 1.13 bp__page_read 9.16 0.86 0.12 9323000 0.01 0.01 bp__default_compare_cb 8.40 0.97 0.11 612972 0.18 0.94 bp__page_search 7.64 1.07 0.10 295594 0.34 0.34 bp__page_destroy 4.58 1.13 0.06 799792 0.08 0.08 bp__writer_write 3.82 1.18 0.05 6784627 0.01 0.01 bp__kv_copy 2.29 1.21 0.03 199993 0.15 0.23 bp__value_save 2.29 1.24 0.03 198997 0.15 0.27 bp__page_shiftl 1.53 1.26 0.02 295568 0.07 0.07 bp__writer_read 0.76 1.27 0.01 399780 0.03 0.03 bp__compute_hash 0.76 1.28 0.01 199997 0.05 0.68 bp__page_save_value 0.76 1.29 0.01 199880 0.05 0.22 bp__tree_write_head 0.76 1.30 0.01 199400 0.05 5.36 bp_sets 0.76 1.31 0.01 149736 0.07 1.62 bp_gets 0.38 1.31 0.01 199885 0.03 0.08 bp__compute_hashl 0.00 1.31 0.00 599835 0.00 0.00 bp__compress 0.00 1.31 0.00 599829 0.00 0.00 bp__max_compressed_size 0.00 1.31 0.00 339619 0.00 0.00 bp__uncompressed_length 0.00 1.31 0.00 335393 0.00 0.00 bp__uncompress 0.00 1.31 0.00 296420 0.00 0.00 bp__page_create 0.00 1.31 0.00 294996 0.00 1.17 bp__page_load 0.00 1.31 0.00 200025 0.00 0.12 bp__page_shiftr 0.00 1.31 0.00 199985 0.00 5.07 bp__page_insert 0.00 1.31 0.00 199062 0.00 5.31 bp_updates 0.00 1.31 0.00 198997 0.00 0.27 bp__page_remove_idx 0.00 1.31 0.00 198928 0.00 5.32 bp_update 0.00 1.31 0.00 108897 0.00 2.14 bp__page_get 0.00 1.31 0.00 102639 0.00 2.27 bp_get 0.00 1.31 0.00 7393 0.00 0.07 bp__page_load_value 0.00 1.31 0.00 6583 0.00 0.08 bp__value_load 0.00 1.31 0.00 29 0.00 3.46 bp__page_split 0.00 1.31 0.00 1 0.00 0.34 bp__destroy 0.00 1.31 0.00 1 0.00 0.29 bp__init 0.00 1.31 0.00 1 0.00 3.80 bp__page_split_head 0.00 1.31 0.00 1 0.00 0.00 bp__writer_create 0.00 1.31 0.00 1 0.00 0.00 bp__writer_destroy 0.00 1.31 0.00 1 0.00 0.29 bp__writer_find 0.00 1.31 0.00 1 0.00 0.34 bp_close 0.00 1.31 0.00 1 0.00 0.29 bp_open 0.00 1.31 0.00 1 0.00 0.00 bp_set_compare_cb ``` 去掉-pg 的執行時間:Execution time: 11.953273 sec 可以看出ntohll和htonll佔了相當多時間, 可是這兩個函式看程式碼並沒有涉及到共同資源, 包含ntohll和htonll的bp__page_read和bp__page_save也佔了相當多時間。換句話說,如果可以把這兩個函式拿出lock鎖住的資源, 就可以省掉相當多時間。 可是實作上我發現若把lock 拆開, 可能會讓原本的資料被改掉, 原本應該在某一個函式中完成的程序結果卻會改變, 而且頻繁的lock unlock 還會增加時間。 因此試試看老師說的不要鎖住整個tree, 先分析key, value放入B+ plus tree的過程 * bp__sets >> bp__page_update ```clike= ret = bp__page_insert(tree, tree->head.page, key, value, update_cb, arg); if (ret == BP_OK) { ret = bp__tree_write_head((bp__writer_t*) tree, NULL); } ``` 從head.page insert * bp__page_insert * search 用 type == kload帶入, 把 page 從disk帶到記憶體中 ```clike= ret = bp__page_search(t, page, key, kLoad, &res); ``` * bp__page_load ```clike if (type == kLoad) { ret = bp__page_load(t, page->keys[i].offset, page->keys[i].config, &child); if (ret != BP_OK) return ret; ``` * 如果不是 leaf (底下有child), 把key 和 value 往child page 帶, 如果找到地方插入可是 page 滿了則split ```clike= if (res.child == NULL) { ... } else { /* Insert kv in child page */ ret = bp__page_insert(t, res.child, key, value, update_cb, arg); /* kv was inserted but page is full now */ if (ret == BP_ESPLITPAGE) { ret = bp__page_split(t, page, res.index, res.child); } else if (ret == BP_OK) { /* Update offsets in page */ page->keys[res.index].offset = res.child->offset; page->keys[res.index].config = res.child->config; } ``` 找到一篇相關的論文: [Performance of B + Tree Concurrency Control Algorithms ](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.217.4343&rep=rep1&type=pdf) 先閱讀之前春季班和lock contention相關的論文 > 我之前打的亂七八糟, 很可能有錯的紀錄在[這裡](https://hackmd.io/MwFgjARgHAhgbAdgLQwMZhEkAGC2kSoJxIJQAmM8FATAgKwhA===?both) > [name=Ruby] * 參考資料 * [Measuring Lock Contention](http://0pointer.de/blog/projects/mutrace.html) * [Locks Aren't Slow; Lock Contention Is](http://preshing.com/20111118/locks-arent-slow-lock-contention-is/) * [Helgrind: a thread error detector](http://valgrind.org/docs/manual/hg-manual.html#hg-manual.data-races) ## Cache Craftiness for Fast Multicore Key-Value Storage筆記 > 程式想不出來的時候一邊看個~ > [name=Ruby][color=lightblue] * 程式碼:[masstree](https://github.com/rmind/masstree) * 論文連結:https://pdos.csail.mit.edu/papers/masstree:eurosys12.pdf * [閱讀筆記](https://hackmd.io/BwFg7MAMBMBGDMBaYBGApmRJbQJyIENgxpEAzAgvAs6NWCIA) ## 閱讀資料與整理 >> 善用 `- [ ]` 而避免提高縮排的深度 [name=jserv] >>> 加了~ >>> [name=Ruby] [color=lightblue] * B-tree 基礎知識參考來源與整理 * [R2. 2-3 Trees and B-Trees](https://www.youtube.com/watch?v=TOb1tuEZ2X4) * 裡面一開始提到了為什麼要使用 B-tree 是因為 memory hierarchy, 可是還是不太明白, 所以又找到了下面的投影片 * [Lecture 16](http://cseweb.ucsd.edu/~kube/cls/100/Lectures/lec16.btree/lec16.pdf) - [ ] Sophisticated cost analysis - [ ] self-adjusting 的結構在一個 operation 下 worst-case 的效能可能很差, 可是在一連串 operation 的總表現很好。 - [ ] Amortized cost analysis 在這種情況下較適合拿來分析 - [ ] 在普通的演算法分析中,往往假設 memory access 時間相同, 可是這樣的方法在實際上可能行不通。 - [ ] Memory hierarchy - [ ] Access 一個參數的時間取決參數存在memory hierarchy的哪裡 - [ ] Consequences of the memory hierarchy - [ ] 因為有 cache, 被存取過的參數再被存取時會快很多 - [ ] Accesssing data on disk - [ ] 資料在 disk 中是以 block 為單位 - [ ] 因為讀取整個 block 和讀取部份 block 的時間是相仿的 - [ ] 所以當我們要存取一個 block 中的某個參數時, operating system 會把整個 block 搬到 semiconductor memory 中(cache 就是一種高速的 semiconductor memory) - [ ] 因為讀取 disk 的時間比讀取 cache 慢很多, 所以要減少讀取 disk 的次數 - [ ] B-tree 可以讓 disk 的存取很有效率 ## 其他參考資料 * 名詞補充 [Tree (data structure)](https://en.wikipedia.org/wiki/Tree_(data_structure)#Leaf) * [Trie](https://en.wikipedia.org/wiki/Trie) * [B+ tree time complexity](https://www.cs.helsinki.fi/u/mluukkai/tirak2010/B-tree.pdf) * [rbtree](https://www.youtube.com/watch?v=axa2g5oOzCE) [^B-tree]:[B-tree Wikipedia](https://en.wikipedia.org/wiki/B-tree) [B-tree 視覺化網站](https://www.cs.usfca.edu/~galles/visualization/BTree.html) [^Lecture_16_Btree_property]:[Lecture_16_B-tree_property](http://cseweb.ucsd.edu/~kube/cls/100/Lectures/lec16.btree/lec16.pdf) [^B-tree_deletion]:[B-tree deletion](https://en.wikipedia.org/wiki/B-tree#Deletion) [B-Tree | Set 3 (Delete)](http://www.geeksforgeeks.org/b-tree-set-3delete/) [^deletion_pic]:[CS241 -- Lecture Notes: B-Trees](https://www.cpp.edu/~ftang/courses/CS241/notes/b-tree.htm) [^b+tree]:[B+ tree Wikipedia](https://en.wikipedia.org/wiki/B%2B_tree#cite_note-Navathe-1) [Data Structures: B+ Trees](https://www.youtube.com/watch?v=2q9UYVLSNeI) [視覺化網站](https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html) [^comparison]: [Differences between B trees and B+ trees-Stackoverflow](http://stackoverflow.com/questions/870218/differences-between-b-trees-and-b-trees) [^bulk]:[ECS Database System Implementation Lecture](http://web.cs.ucdavis.edu/~green/courses/ecs165b-s10/Lecture6.pdf) [^ktvexe作法]:[ktvexe同學](https://github.com/ktvexe/bplus-tree) [^閱讀與整理]:[閱讀與整理](https://hackmd.io/EYTgJiCs4AwLQDZgDNJwCwEYCG24gGMBmYOYAdmQQXWAuDAKA===?both#閱讀資料與整理)