--- tags: linux2023 --- # 2023q1 Homework4 (quiz4) contributed by < [chiangkd](https://github.com/chiangkd) > ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz CPU family: 6 Model: 158 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 9 CPU max MHz: 3800.0000 CPU min MHz: 800.0000 BogoMIPS: 5599.85 ``` ## 測驗 `1` **延伸問題**: 1. 摘錄上述紅黑樹新增和刪除節點操作的程式碼,解釋其原理。不用列出完整程式碼,但要推敲其設計思維 >提示: 善用 [TreeComparison](https://github.com/tinfoilhater/TreeComparison) 製圖 2. 運用你在[第 3 週測驗題](https://hackmd.io/@sysprog/linux2023-quiz3)提及的延伸問題所開發出的效能評比程式,分析不同 rbtree 實作的效能落差 3. [jemalloc](https://github.com/jemalloc/jemalloc) 專案的 [test/unit/rb.c](https://github.com/jemalloc/jemalloc/blob/dev/test/unit/rb.c) 對 rbtree API 有更完整的涵蓋,其中 summarize/filter 能夠特化查詢範圍,請自 jemalloc 移植更豐富的 rbtree API 及其實作,並撰寫相關的測試程式 (含效能評比) 4. 解讀 Linux 核心的 [include/linux/rbtree.h](https://github.com/torvalds/linux/blob/master/include/linux/rbtree.h), [include/linux/rbtree_types](https://github.com/torvalds/linux/blob/master/include/linux/rbtree_types.h), [lib/rbtree.c](https://github.com/torvalds/linux/blob/master/lib/rbtree.c) 運作原理,並探討能否運用上述手法,以縮減 rbtree 節點的佔用空間 在原本的實作的結構體中,包含親代節點、左子、右子節點,且使用 `__attribute__((aligned(sizeof(long))))` 使此結構體對齊 `sizeof(long)` 也就是讓最後兩個 bit 維持為 `0`,在日後便可利用這兩個 bit 中其一用以標示顏色 ```c struct rb_node { unsigned long __rb_parent_color; struct rb_node *rb_right; struct rb_node *rb_left; } __attribute__((aligned(sizeof(long)))); ``` 而改寫後的結構體如下,此時親代節點已經不保存於 `rb_node` 中 ```c /* Node structure */ #define rb_node(x_type) \ struct { \ x_type *left, *right_red; \ } ``` ### rb_gen ```c #define rb_gen(x_attr, x_prefix, x_rbt_type, x_type, x_field, x_cmp) ``` 在測試程式中, `xprefix` 為 `tree_`,在經歷一連串的展開、串接後就會拿到具有 `tree_` 系列 prefix 的函式定義,例如測試程式中使用的 `tree_new`、`tree_insert`、`tree_search` 等等 ### 新增以及刪除節點操作 **老師貼文以及 [wanghanchi 同學筆記](https://hackmd.io/@wanghanchi/linux2023-quiz4)** >王漢祺 同學研究給定的紅黑樹實作,他著手理解為何描述節點的結構體只包含指向左側和右側的指標,卻沒有親代節點的指標,這是因為既然紅黑樹的樹高增長較慢,與其在描述節點的結構體中多佔用一個指標的空間 (與 WORD_SIZE 等寬,亦即 64 位元或 32 位元,取決於微處理器架構),不如挪動節點的走訪成為路徑。 以下圖為例,即將要插入的節點 key 為 90,於是節點走訪的過程可表示為: >- 先對 50 比較,得到 cmp = 1 後,就會往右側進行下一層的尋找,並紀錄 cmp >- 對 70 比較,一樣得到了 cmp = 1 後,就會往右邊進行下一層的尋找,並紀錄 cmp >- 再對 80 比較,得到 cmp = 1 後,就會往右邊進行下一層的尋找,這時會得到 NULL,並紀錄 cmp >- 跳出 for 迴圈,將 pathp 指向節點,亦即指向即將插入的節點 > >如此一來,即使描述節點的結構體不存在親代節點的指標,但運用上述路徑走訪,依然可完成插入的操作,也因此程式碼將這個特別的陣列命名成 path。 **新增節點** 在測試程式中 `tree_insert` 會透過建立好的隨機 `nodes` 陣列將其中的元素插入 `tree` 中。 ```c for (int i = 0; i < NNODES; ++i) tree_insert(&tree, nodes + i); ``` 查看其對應註解 ```shell * static void * ex_insert(ex_t *tree, ex_node_t *node); * Description: Insert node into tree. * Args: * tree: Pointer to an initialized red-black tree object. * node: Node to be inserted into tree. ``` 以測試程式為例 - `x_attr` $\rightarrow$ `static` - `x_prefix` $\rightarrow$ `tree_` - `x_rbt_type` $\rightarrow$ `tree_t` - `x_type` $\rightarrow$ `node_t` - `x_field` $\rightarrow$ `link` (name of rb tree node linkage) - `x_cmp` $\rightarrow$ `node_cmp` 其中 `node_cmp` ```c static int node_cmp(const node_t *a, const node_t *b) { int ret = (a->key > b->key) - (a->key < b->key); if (ret == 0 && a != b) assert(0 && "Duplicates are not allowed in the tree"); return (ret); } ``` - `key` is search key,如果 a > b 則回傳 `1` , a < b 則回傳 `-1`,duplicate ,相等則回傳 `0` 在 `tree_insert` 中會先根據名稱定義的 `tree_path_entry_t` 型別的 `path`,其最大長度為 `RB_MAX_DEPTH` ```c #define RB_MAX_DEPTH (sizeof(void *) << 4) ``` - 在一般 64 位元的系統上, `void *` 為 8 bytes ,故 `RB_MAX_DEPTH` 將樹的最大深度限制在 $8 * 16=128$ bytes **Wind** 在 `tree_insert` 中的 `node` 為要插入的節點,在第一個 for loop 中 (`for (pathp = path; pathp->node; pathp++)`) 會去比較 `node` 和當前 tree 中的節點 `pathp->node` ,如果有相同的 key 則觸發 `assert`。接著 1. 如果 `cmp < 0`,即代表要放在左側作為左子節點,藉由 `rbtn_left_get` 獲取 `pathp->node->link->left` - 這邊的 `link` 由 `rb_node(node_t)` 產生,其結構體包含 `node_t *` 型別的 `left` 以及 `right_red` 2. 如果 `cmp > 0`,即代表要放在右側作為右子節點,藉由 `rbtn_right_get` 獲取該節點指標,但是因其後兩位 bit 作為顏色標示為 `1` (紅色),故額外使用 `$ ~3` 將其清除。 將走訪過的節點 accessor 紀錄於 `pathp[1]` 中,且 cmp 的結果紀錄於 `path->cmp` 中,最後走訪完成後將待插入的節點 `node` 置於 `pathp->node`。 利用兩個 assert 確認該 `node` 左右皆為 `NULL` ```c assert(!rbtn_left_get(x_type, x_field, node)); assert(!rbtn_right_get(x_type, x_field, node)); ``` **Unwind** 接著在第二個 for loop 中倒著走訪回來 `for (pathp--; (uintptr_t)pathp >= (uintptr_t)path; pathp--)` 在迴圈中 `cnode` 為走訪中的 `node`,接著照順序查找剛剛紀錄下來的 `cmp`,如果 `cmp < 0` ```c if (pathp->cmp < 0) { \ x_type *left = pathp[1].node; \ rbtn_left_set(x_type, x_field, cnode, left); \ if (!rbtn_red_get(x_type, x_field, left)) \ return; \ x_type *leftleft = rbtn_left_get(x_type, x_field, left); \ if (leftleft && rbtn_red_get(x_type, x_field, leftleft)) { \ /* Fix up 4-node. */ \ x_type *tnode; \ rbtn_black_set(x_type, x_field, leftleft); \ rbtn_rotate_right(x_type, x_field, cnode, tnode); \ cnode = tnode; \ } \ } ``` - 透過 `rbtn_left_set` 將 `cnode` 的左子節點設定為 `left` ,若其 `rbtn_red_get(x_type, x_field, left)` 不存在,也就是其右節點是黑色的則直接 return,代表結束 - 如果沒有結束,則代表其右節點是紅色的,接著判斷該節點的左節點 (`leftleft`) 是否存在且右節點是否為紅色的,若真,則將 `leftleft` 塗黑並對著 `cnode` 進行右旋,在將 `cnode` 設定為 `tnode` ,也就是原來的親代節點的左子節點,作為新的親代節點 (迭代中的位置)。 **刪除節點** 同樣地在測試程式中根據 `NNODES` 數量進行刪除 ```cpp for (int i = 0; i < NNODES; ++i) { tree_remove(&tree, nodes + i); } ``` 查看其對應註解 ```shell * static void * ex_remove(ex_t *tree, ex_node_t *node); * Description: Remove node from tree. * Args: * tree: Pointer to an initialized red-black tree object. * node: Node in tree to be removed. ``` **Wind** 以下圖為例,刪除 49 節點 ![](https://i.imgur.com/9woVD3L.png) 同樣的先走訪 `tree`,紀錄其 `cmp` 值,在 `else` 區塊有判斷 `cmp` 是否為 `0`,也就是找到要刪除的節點。 | node | `48` | `60` | `50` | `49` | | ---- | ---- | ---- | ---- | ------------------------------ | | cmp | 1 | -1 | -1 | 0 => 1 (Find node's successor) | 在這邊也用了一個 `assert` 來檢查 `nodep->node` 是否指向要刪除的節點 `node` ```c assert(nodep->node == node); ``` `pathp--` 後 `pathp` 指向 `49` 因其只有一個節點且沒有子節點,會進到條件中 ```c if (rbtn_red_get(x_type, x_field, pathp->node)) { \ /* Prune red node, which requires no fixup. */ \ assert(pathp[-1].cmp < 0); \ rbtn_left_set(x_type, x_field, pathp[-1].node, NULL); \ return; \ } ``` 判斷到刪除的節點 `49` 為紅色節點,直接遺除掉就結束了 刪除完成 ![](https://i.imgur.com/7TtJS4B.png) 繼續下去此時若刪除節點 `50`,一樣從 root 開始逐個進行 `cmp`,建立表如下, | node | `48` | `60` | `50` | `51` | NULL | | ---- | ---- | ---- | ----------------------------- | ---- | ---- | | cmp | 1 | -1 | 0 => 1(Find node's successor) | -1 | | 在比對到 `cmp == 0` 時 (`50`) ```c if (cmp == 0) { \ /* Find node's successor, in preparation for swap. */ \ pathp->cmp = 1; \ nodep = pathp; \ for (pathp++; pathp->node; pathp++) { \ pathp->cmp = -1; \ pathp[1].node = rbtn_left_get(x_type, x_field, pathp->node); \ } \ break; \ } ``` 一進入時的 `pathp` 指向 `50`,並將其 `cmp` 設定為 `1`,`path++` 後會指向 `51`,並將其 `cmp` 設定為 `-1`,接著透過 `rbtn_left_get()` 將 `path` 最後一個填入 `NULL` 此時 `pathp` 指向 `NULL`, `nodep` 指向 `node` 也就是 `50` 透過 `pathp--` 此時 `pathp` 指向 `51`,進入條件 `if (pathp->node != node) {` 中,將 `tred` 為 `1` - `rbtn_color_set` 將 `51` 設定為**黑色** - `rbtn_left_set` 將 `51` 的左子節點設定為 `50` 的左子節點,也就是 `NULL` 接下來 - `rbtn_right_set` 會將 `51` 的右子節點設定為 `50` 的右子節點,也就是 `51` 自己 - `rbtn_color_set` 會將 `50` 設定成**紅色** 此處有註解 ```c /* If node's successor is its right child, the following code */ \ /* will do the wrong thing for the right child pointer. */ \ /* However, it doesn't matter, because the pointer will be */ \ /* properly set when the successor is pruned. */ ``` 正好符合現在的情況,他的 successor 就是他的右子節點 (`51`) 接著把 `nodep` 和 `pathp` 指向的 `node` 交換,故此時 `nodep->node` 指向 `51`, `pathp->node` 指向 `50`,此時因 `nodep[-1].cmp` 為 `-1` 故將其左子節點設定為 `nodep->node`,也就是將 `60` 的左子節點設定成 `51` 在此處把 out-of-date 的 `pathp[-1]` (`50`) 的左子節點設定為 NULL ```c if (rbtn_red_get(x_type, x_field, pathp->node)) { \ /* Prune red node, which requires no fixup. */ \ assert(pathp[-1].cmp < 0); \ rbtn_left_set(x_type, x_field, pathp[-1].node, NULL); \ return; \ } ``` 刪除完成 ![](https://i.imgur.com/zhKKGG3.png) 此時在接著刪除 `51` 節點 **Wind** 部份與前述相似 | node | `48` | `60` | `51` | NULL | | ---- | ---- | ---- | ----------------------------- | ---- | | cmp | 1 | -1 | 0 => 1(Find node's successor) | | 迭代完後來到刪除黑色節點,所以需要 **Unwind** 直到恢復平衡狀態,首先因刪除節點 (`51`) 的前一個為 `60`,其紀錄的 `cmp` 為 `-1`,且在本例中最後會進到這個 case (和註解長的不太一樣?) ```c else { \ /* || */ \ /* pathp(b) */ \ /* // \ */ \ /* (b) (b) */ \ /* / */ \ /* (b) */ \ x_type *tnode; \ rbtn_red_set(x_type, x_field, pathp->node); \ rbtn_rotate_left(x_type, x_field, pathp->node, tnode); \ pathp->node = tnode; \ } ``` 在 `rbtn_rotate_left` 中,會將 `pathp->node` (60) 的右子節點設定為 80 的左子節點,並將 `r_node` (80) 的左子節點設定為 `pathp_node` (60) 做完這一步後會長這樣 ![](https://i.imgur.com/SXHkisV.png) 但從上方這張圖片到下方刪除完成後的結果似乎沒有實作? **刪除完成結果應長的如下** ![](https://i.imgur.com/OwXPmQN.png) 突然注意到在註解中有提到標頭檔中的巨集實作為 **left-leaning 2-3 red-black tree** ```c /* C macro implementation of left-leaning 2-3 red-black trees. Parent * pointers are not used, and color bits are stored in the least significant * bit of right-child pointers, thus making node linkage as compact as is * possible for red-black trees. */ ``` 如此一來舉上面這個例子就不太正確了,在[簡報](https://sedgewick.io/wp-content/uploads/2022/03/2008-09LLRB.pdf)中有提到左傾紅黑樹,3-nodes 的轉換必須是左傾的,而因為是 2-3 tree,所以暫不考慮 4-node 的情況 ![](https://i.imgur.com/qoo3MlE.png) 在傳統的紅黑樹下這樣的樹的樣子是可以接受的,但在左傾的情況下不行 ![](https://i.imgur.com/GZZ2jMW.png) 左傾紅黑樹必須確保左節點是紅色的,也就是左子樹要比右子樹還要高或相等,在[簡報](https://sedgewick.io/wp-content/uploads/2022/03/2008-09LLRB.pdf)中也有提到一個 **"surprise"**,也就是根據顏色交換 (color flip) 的位置不同可以將實作從 2-3-4 trees 變為 2-3 trees ![](https://i.imgur.com/qQH1WeA.png) 這段也可以對應到在 `insert` 函式中的 ```c if (left && rbtn_red_get(x_type, x_field, left)) { \ /* Split 4-node. */ \ rbtn_black_set(x_type, x_field, left); \ rbtn_black_set(x_type, x_field, right); \ rbtn_red_set(x_type, x_field, cnode); } ``` 在[論文](https://sedgewick.io/wp-content/themes/sedgewick/papers/2008LLRB.pdf)中也有相對應的敘述 >**2-3 trees** Remarkably, moving the color flip to the end in the top-down 2-3-4 tree implementation just described yields an implementation for 2-3 trees. We split any 4-node that is created by doing a color flip, passing a red link up the tree, and dealing with the effects of doing so in precisely the same way as we move up the tree. ## 測驗 `2` **延伸問題**: 1. 解釋上述程式碼運作原理,不用列出完整程式碼,但要推敲其設計思維,應參照論文〈[On the Worst-Case Complexity of TimSort](https://arxiv.org/abs/1805.08612)〉和〈[Merge Strategies: from Merge Sort to TimSort](http://nicolas.louvet.pages.univ-lyon1.fr/meef-nsi/m1-algoprog/TD7-tris/merge_strategies.pdf)〉 2. BONUS: 針對 Linux 核心原始程式碼的 lib/list_sort.c 和 lib/sort.c,提出 hybrid sorting 的引入和改進方案,並予以實作 → 之後可提交成果到 LKML 3. F9 microkernel 的 [kernel/lib/sort.c](https://github.com/f9micro/f9-kernel/blob/master/kernel/lib/sort.c) 用非遞迴的方式實作快速排序,請評估導入 [introsort](https://en.wikipedia.org/wiki/Introsort) 或更新的 hybrid sort 的效益 ## 測驗 `3` **延伸問題** 1. 解釋上述程式碼運作原理,不用列出完整程式碼,但要推敲其設計思維 2. 將測驗 `1` 對節點的紅色和黑色標注的技巧嵌入到指標變數中 3. Linux 核心的 rbtree 具備 cache 機制 (參見 [Red-black Trees (rbtree) in Linux](https://www.kernel.org/doc/Documentation/rbtree.txt)),探討其原理,並嘗試在測驗 1 或測驗 3 的基礎之上實作 ## Reference - [Wanhanchi](https://hackmd.io/@wanghanchi/linux2023-quiz4) - [Red/Black Tree](https://www.cs.usfca.edu/~galles/visualization/RedBlack.html)