--- tags: linux kernel --- # 2023q1 Homework3 (quiz3) contributed by < `hankTaro` > ## 測驗一 :::success 延伸問題: 1. 指出 `treesort.c` 程式碼的缺失並改進 2. 利用 Linux 核心的 [list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 標頭檔改寫並整合進第一次作業的實驗程式碼中,探討 [Tree sort](https://en.wikipedia.org/wiki/Tree_sort) 的效率 3. 研讀 Linux 核心的 [Red–black tree](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree) 程式碼,比較給定的 `treesort.c` 實作手法,指出後者的改進空間並予以實作 ::: <!-- :::warning 文字訊息不要用圖片展現,尊重視覺障礙者閱讀的權益。 :notes: jserv ::: --> 此紅黑樹節點架構將顏色與親代節點的資料存在同個空間,藉由透過 `__attribute__((aligned(sizeof(long))))` 對齊到 `sizeof(long)`,這樣指標最低 3 個位元是沒有使用到的,便可將此位置用於儲存顏色(但是儲存顏色只需要用到 1 bit) > long 所需的位元數為 4,所以對齊後最小的三位元會固定不動 > e.g. `00001000` -> `00010000` -> `00011000` -> `00100000` -> `00101000` > [你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory) :::info 這裡對將變數名取為 `color` 頗為不滿,若將其命名為 `parent_color` 會更容易閱讀理解,並避免混淆 ::: ```c typedef struct __node { uintptr_t color; struct __node *left, *right; struct __node *next; long value; } node_t __attribute__((aligned(sizeof(long)))); ``` 並且定義巨集以便取出親代節點的位置與節點顏色 ```c #define rb_parent(r) ((node_t *) (r)->color & ~7) #define rb_color(r) ((color_t) (r)->color & 1) #define rb_set_parent(r, p) \ do { \ (r)->color = rb_color(r) | (uintptr_t) (p); \ } while (0) #define rb_set_red(r) \ do { \ (r)->color & ~7; \ } while (0) #define rb_set_black(r) \ do { \ (r)->color | 1; \ } while (0) ``` ```c void tree_sort(node_t **list) { node_t **record = list; cmap_t map = cmap_new(sizeof(long), sizeof(NULL), cmap_cmp_int); while (*list) { cmap_insert(map, *list, NULL); list = &(*list)->next; } // 找出 tree 中最小的值(也就是不停的往左側尋訪) node_t *node = cmap_first(map), *first = node; for (; node; node = cmap_next(node)) { *list = node; list = &(*list)->next; } *list = NULL; *record = first; free(map); } ``` `int res = obj->comparator(&node->value, &cur->value);` 中的 res ,是 `obj->comparator` (一個比較函數,由 `cmap_new`, assign 進去的),當 `&node->value` 大於 `&cur->value` 時回傳 1,反之回傳 -1 ,相等回傳 0 ,隨後便利用 res 辨別相對大小,將此 node 放在 NIL 的位置(leaf), ```c static bool cmap_insert(cmap_t obj, node_t *node, void *value) { cmap_create_node(node); obj->size++; if (!obj->head) { /* Just insert the node in as the new head. */ obj->head = node; rb_set_black(obj->head); /* Calibrate the tree to properly assign pointers. */ cmap_calibrate(obj); return true; } /* Traverse the tree until we hit the end or find a side that is NULL */ for (node_t *cur = obj->head;;) { int res = obj->comparator(&node->value, &cur->value); if (!res) /* If the key matches something else, don't insert */ assert(0 && "not support repetitive value"); if (res < 0) { //若 node 比現在這個點小,則檢查 cur 左側有沒有節點,若沒有就將 node 放入 cur 的左側,反之則將 cur 變為其左側節點重複判斷 if (!cur->left) { cur->left = node; rb_set_parent(node, cur); cmap_fix_colors(obj, node); break; } cur->cur->left; } else { if (!cur->right) { cur->right = node; rb_set_parent(node, cur); cmap_fix_colors(obj, node); break; } cur->cur->right; } } cmap_calibrate(obj); return true; } ``` cmap_new 的必要性,因為只有在 tree_sort 中呼叫過一次,內容也只是將固定傳入數值 assign 進結構中 ### 引入 list.h 將 `struct __node` 中的 `struct __node *next;` 用 `struct list_head list;` 替換 ```c typedef struct __node { uintptr_t color; struct __node* left, * right; struct list_head list; long value; } node_t __attribute__((aligned(sizeof(long)))); ``` 將 `main` 中的 `list_make_node` 改為使用 `list_add` ```c int main(int argc, char** argv) { size_t count = 100; int* test_arr = malloc(sizeof(int) * count); for (int i = 0; i < count; ++i) test_arr[i] = i; shuffle(test_arr, count); /*node_t* list = NULL;*/ LIST_HEAD(list); while (count--) { list_make_node(list, test_arr[count]); } tree_sort(&list); assert(list_is_ordered(list)); list_free(&list); free(test_arr); return 0; } ``` ```c static inline void list_make_node(struct list_head* head, int n) { node_t* node = malloc(sizeof(node_t)); node->value = n; list_add(node->list, head); } ``` 使用 `list_for_each_entry` 與 `list_move_tail` 省去使用 `list = &(*list)->next;`,並且更改 `cmap_next` 與 `cmap_first` 資料回傳型態 ```c void tree_sort(struct list_head **list) { struct list_head **record = list; cmap_t map = cmap_new(sizeof(long), sizeof(NULL), cmap_cmp_int); node_t *it; list_for_each_entry(it, *list, list) { cmap_insert(map, it, NULL); } struct list_head* node = cmap_first(map); for (; node; node = cmap_next(node)) { list_move_tail(node, *list); } free(map); } ``` --- ## 測驗二 :::spoiler 答案 `IIII = node->parent_balance & ~7` `JJJJ = node->parent_balance & 3` `KKKK = node->parent_balance & 3` `LLLL = node->parent_balance & ~7` `MMMM = avl_rotate_right` `NNNN = avl_rotate_leftright` ::: ### 程式碼講解 AVL tree 這裡也利用 `AVL_NODE_ALIGNED` 做 data alignment ,將 balance 值存於 adress 的末兩位中,剛好 balance 只有三種可能,平衡、左傾或右傾,故可以使用 2 bits 空間進行保存 > 要注意的是,這裡左傾與右傾是指,兩邊最大高度差為 1,因為當兩邊高度差為 2 時,就會進行平衡,並且求出新的 balance ,所以這邊 [Balance Factor](https://josephjsf2.github.io/data/structure/and/algorithm/2019/06/22/avl-tree.html) 只會有 -1, 0, 1 的狀況 ```c // 用於對應是平衡、左傾還是右傾(Balance Factor 為 0, -1, 1) enum avl_node_balance { AVL_NEUTRAL = 0, AVL_LEFT, AVL_RIGHT, }; ``` ```c #define AVL_NODE_ALIGNED __attribute__((aligned(sizeof(unsigned long)))) ... struct avl_node { unsigned long parent_balance; struct avl_node *left, *right; } AVL_NODE_ALIGNED; ``` 呼叫 `avl_insert` 的情況下,是已經找到此 node 在樹中的位置,並且 parent 已將此節點設為他的 child,故呼叫 `avl_link_node` 建立出一個 `left` `right` 都是 NULL 的新節點,並且是平衡的,再將 parent 的位置存入 ```c static inline void avl_link_node(struct avl_node *node, struct avl_node *parent, struct avl_node **avl_link) { avl_set_parent_balance(node, parent, AVL_NEUTRAL); node->left = NULL; node->right = NULL; *avl_link = node; } static inline void avl_insert(struct avl_node *node, struct avl_node *parent, struct avl_node **avl_link, struct avl_root *root) { avl_link_node(node, parent, avl_link); avl_insert_balance(node, root); } ``` 當 node 插入後,便開始沿著 parent 的路徑依序平衡,可以將情況分為一下幾種: * case 1: * node 平衡,在 parent 的左側,parent 右傾 * node 平衡,在 parent 的右側,parent 左傾 * case 2: * node 平衡,parent 平衡 * case 3: * node 不平衡 在 case 1 中只需要將 parent 設為平衡 在 case 2 中只需要依據 node 是插入在左側還是右側,將 parent 設為右傾或左傾 case 3 需要進行旋轉以平衡兩側高度,並且要同時對 node 與 parent 做判斷,可以分為 4 種類型 * LL 型: * 狀況: parent 與 node 均為左傾 * 處理方法: 對 parent 進行右旋 * LR 型: * 狀況: parent 左傾,node 右傾 * 處理方法: node 進行左旋後,對 parent 進行右旋 * RR 型: * 狀況: parent 與 node 均為右傾 * 處理方法: parent 進行左旋 * RL 型: * 狀況: parent 右傾,node 左傾 * 處理方法: node 進行右旋後,對 parent 進行左旋 :::warning 要注意插入新節點還沒平衡前,新節點的 parent 的 balance 尚未更新,並且新節點必定平衡,所以第一次平衡不可能會進行旋轉 新節點的 parent 只受新節點在其左還右,以改變其平衡狀態 node 為平衡或左傾時,都算做 L ::: LL 型: ![](https://i.imgur.com/9JrQRjk.png) <!-- 上圖中 B 點是左傾,故右旋後會平衡 ![](https://i.imgur.com/I8qXqCB.png) --> LR 型: ![](https://i.imgur.com/wgnxcPY.png) RR 型: ![](https://i.imgur.com/zAoVi3Y.png) RL 型: ![](https://i.imgur.com/m9xv8pS.png) ```c void avl_insert_balance(struct avl_node *node, struct avl_root *root) { struct avl_node *parent; /* go tree upwards and fix the nodes on the way */ while ((parent = avl_parent(node))) { if (avl_is_right_child(node)) { switch (avl_balance(parent)) { default: case AVL_RIGHT: /* compensate double right balance by rotation * and stop afterwards */ switch (avl_balance(node)) { default: case AVL_RIGHT: case AVL_NEUTRAL: avl_rotate_left(node, parent, root); break; case AVL_LEFT: avl_rotate_rightleft(node, parent, root); break; } parent = NULL; break; case AVL_NEUTRAL: /* mark balance as right and continue upwards */ avl_set_balance(parent, AVL_RIGHT); break; case AVL_LEFT: /* new right child + left leaning == balanced * nothing to propagate upwards after that */ avl_set_balance(parent, AVL_NEUTRAL); parent = NULL; break; } } else { switch (avl_balance(parent)) { default: case AVL_RIGHT: /* new left child + right leaning == balanced * nothing to propagate upwards after that */ avl_set_balance(parent, AVL_NEUTRAL); parent = NULL; break; case AVL_NEUTRAL: /* mark balance as left and continue upwards */ avl_set_balance(parent, AVL_LEFT); break; case AVL_LEFT: /* compensate double left balance by rotation * and stop afterwards */ switch (avl_balance(node)) { default: case AVL_LEFT: case AVL_NEUTRAL: avl_rotate_right(node, parent, root); break; case AVL_RIGHT: avl_rotate_leftright(node, parent, root); break; } parent = NULL; break; } } if (!parent) break; node = parent; } } ``` 而進入旋轉後,需要根據親代節點的平衡狀況去設置調整後的平衡狀況 依序分別為: * LL 型: 右旋 * LR 型: 先左旋再右旋 * RR 型: 左旋 * RL 型: 先右旋再左旋 ```c static struct avl_node *avl_rotate_right(struct avl_node *node, struct avl_node *parent, struct avl_root *root) { enum avl_node_balance balance_parent, balance_node; struct avl_node *tmp; switch (avl_balance(node)) { case AVL_NEUTRAL: balance_parent = AVL_LEFT; balance_node = AVL_RIGHT; break; default: /* AVL_RIGHT is not allowed */ case AVL_LEFT: balance_parent = AVL_NEUTRAL; balance_node = AVL_NEUTRAL; break; } /* rotate right */ tmp = parent->left; parent->left = tmp->right; tmp->right = parent; avl_rotate_switch_parents(tmp, parent, parent->left, root, balance_node, balance_parent); return tmp; } ``` 此方法對應的是 LL 型,由於此情況 node 不可能右傾,因為右傾則會成為 LR 型, 故只需要考慮 node 為平衡以及左旋的狀況,當 node 為平衡時,代表其左右側都不為 NIL(因為第一次平衡不可能會進行旋轉,故發生選轉的節點必定有子節點),所以其右側節點會成為其親節點的左側節點 node 為紅色節點 ```graphviz graph graphname { label="NODE 為平衡時" n01 [label="A"] ; n01 -- n02 ; n01 -- n03 ; n02 [label="B",color="red"] ; n02 -- n04 ; n02 -- n05 ; n03 [label="C"] ; n04 [label="D"] ; n05 [label="E"] ; } ``` ```graphviz graph graphname { label="旋轉後" n01 [label="B",color="red"] ; n01 -- n02 ; n01 -- n03 ; n02 [label="D"] ; n03 -- n05 ; n03 -- n04 ; n03 [label="A"] ; n04 [label="C"] ; n05 [label="E"] ; } ``` 故 node 在旋轉後會右傾,而 parent 會左傾,故更新平衡狀態,隨後進行節點的移動與連結 而 RL/LR 型則是先對 node 做相對應的旋轉,使其變為 RR 或 LL 型,再用同樣的方式對 parent 旋轉,並更新平衡狀態,概念相同這裡就不多做贅述 ### Linux 核心 rb_tree 替代 AVL_tree 之因素 目前Linux 核心中的 AVL tree ,逐漸被紅黑樹替代,AVL tree 相較於 RBT 的劣勢在於,當節點數量較大時,可能需要一直旋轉直到 root ,在大多數情況下,RBT 的效率會比 AVL tree 好 在考慮是否要用 RBT 取代 AVL tree 以前,必須先行了解 linux 核心的同步機制之一 [RCU](https://hackmd.io/@sysprog/linux-rcu),以及 linux 的鎖定機制 [seqlock(short for sequence lock)](https://en.wikipedia.org/wiki/Seqlock),因為要將 AVL tree 用 RBT 取待,需要確保 RBT 支援 RCU ,否則在多執行上效率不彰 在使用 RCU 的狀況下,RBT 可使用較少的函式達成,同時使用較少的變數 ### (TODO)利用 rb_tree 替代 AVL_tree 之實作 :::success 延伸問題: 1. 解釋上述程式碼運作原理,並比較用紅黑樹實作的 [priority queue](https://en.wikipedia.org/wiki/Priority_queue),應提供完整的測試程式和效能分析 2. Linux 核心也有 AVL tree 的身影,但陸續被紅黑樹替代,請揣摩 Linux 核心開發者的考慮因素,並搭配 git log 來解讀 > 見 commit [b145425](https://github.com/torvalds/linux/commit/b145425f269a17ed344d737f746b844dfac60c82) 3. 研讀 Linux 核心原始程式碼,指出曾經/現在使用 AVL tree 的部分,對照 Linux 核心的 `rbtree.h`,探究是否可用後者替換,從而爭取到貢獻程式碼到 Linux 核心的機會 ::: --- ## 測驗三 ```c uint64_t log2_64(uint64_t v) { unsigned r; uint64_t t, tt, ttt; ttt = v >> 32; if (ttt) { tt = ttt >> 16; if (tt) { t = tt >> 8; if (t) { r = 56 + log_table_256[t]; } else { r = 48 + log_table_256[tt]; } } else { t = ttt >> 8; if (t) { r = 40 + log_table_256[t]; } else { r = 32 + log_table_256[ttt]; } } } else { tt = v >> 16; if (tt) { t = tt >> 8; if (t) { r = 24 + log_table_256[t]; } else { r = 16 + log_table_256[tt]; } } else { t = v >> 8; if (t) { r = 8 + log_table_256[t]; } else { r = 0 + log_table_256[v]; } } } return r; } ``` ```c unsigned int bucket_number(uint64_t x) { uint64_t mask111 = (1 << (N_BITS + 1)) - 1; uint64_t mask011 = (1 << (N_BITS)) - 1; /* one 1 less */ unsigned char leq = ((x & mask111) < N_BUCKETS); /* leq (less or equal) is 0 or 1. */ return (leq * (x & mask111)) + ((1 - leq) * ((x >> (N_BITS + 1)) & mask011)); /* 'x >> (N_BITS + 1)' : take different set of bits -> better uniformity. * '... & mask011' guarantees that the result is less or equal N_BUCKETS. */ } ``` 用 `__builtin_clzl` 求出左側0數量就可以求出小於 x 的 $n=log_2x$ 最大位元 - 左側0數量 - 1 = 右側到最大 1 之間的 0 數量 = n :::success 延伸問題: 1. 解釋 [Linear feedback shift register](https://en.wikipedia.org/wiki/Linear-feedback_shift_register) (LFSR) 的運作原理,搭配在 [Linux 核心原始程式碼](https://github.com/torvalds/linux)中,用 `git log` 和 `grep` 找出 LFSR 的應用案例 2. 解釋上述程式碼運作的原理 3. 在 Linux 核心原始程式碼中找到 $\log_2(x)$的實作 (整數),要涵蓋不同的整數型態寬度,以及是否能運用微處理器提供的指令來加速。也從 Linux 核心找出整數 $\log_2(x)$ 的應用案例並解說。 4. `lab0-c` 提供類似的實作 [log2_lshift16.h](https://github.com/sysprog21/lab0-c/blob/master/log2_lshift16.h),解釋其原理並嘗試改進其效能 ::: --- ## 測驗四 ### 運作原理 此程式欲求出 $\log_2(x)$ 並向上取整 ```c=1 int ceil_log2(uint32_t x) { uint32_t r, shift; x--; r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; return (r | shift | x >> 1) + 1; } ``` 取 $\log_2(x)$ 的改念就是看最高有效位元的位置,因為在二進位中,位元的位置為冪運算的上標,所以只要知道 x 最左側的 1 後有幾個位元,就可以求出$\log_2(x)$ 在第 5 行中,將 `x--` 是為了將 $2^n$ 型態的數值最大值降低一位元,確保無條件進位時的答案正確 6~15 行都是在求最高有效位的位置,最後在 `return (r | shift | x >> 1) + 1;` 無條件向上取整 ### 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless 由於 $\log_2(x)$ 需要求出最高有效位元右側有幾個位元,便可使用 `__builtin_clz` 函式,求出最高有效位元左側有幾個 `0` ,在用最大位元數減去,便可得到其值共佔多少位元,由於要向上取整,故在運算前 `x - 1`,這樣 $2_n$ 型態算出的值會剛好是其減 1 前右側位元數,而不是$2_n$ 型態算出的值會是無條件進位後的位元數 另外輸入有機會為 0,故使用 `assert` 檢查,並達成 branchless ```c int ceil_log2(uint32_t x) { assert(x != 0 && "log2(0) is undefined !\n"); return 32 - __builtin_clz(x - 1); } ``` ### (TODO)Linux 核心中 ceil 和 log2 的組合案例 :::success TODO: 1. 解釋上述程式碼運作原理 2. 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless 3. 在 Linux 核心原始程式碼找出 ceil 和 log2 的組合 (類似的形式),並解說應用案例 (提示: 在 Linux 核心排程器就有) :::