# 2023q1 Homework3 (quiz3) contributed by < `LiChiiiii`> >測驗題目:[quiz3](https://hackmd.io/@sysprog/linux2023-quiz3) ## 測驗一 #### 紅黑樹 研讀 [treesort.c](https://gist.github.com/jserv/920766d1fd893d0efd66ac7fdaf23401) ,考慮一個 Tree sort 的實作,搭配 Red–black tree 作為排序所需的自我平衡樹。 定義一個列舉型別 `color_t` ,包含兩個值,分別為 `CMAP_RED` 與 `CMAP_BLACK`,代表紅色節點和黑色節點。 ```c typedef enum { CMAP_RED = 0, CMAP_BLACK } color_t; ``` 定義一個結構 `node_t` ,其中 `color` 代表節點顏色, `left` 和 `right` 分別代表紅黑樹的左節點和右節點,`next` 代表在原始陣列中下一個節點, `value` 代表節點的值。 ```c typedef struct __node { uintptr_t color; struct __node *left, *right; struct __node *next; long value; } node_t __attribute__((aligned(sizeof(long)))); ``` 為了節省記憶體空間,這裡把父節點的位址存在 `uintptr_t color` 的最高位中。在 64 位元的系統中,考慮到 alignment,記憶體以 8 bytes 為一個單位進行管理,也就是說 `node_t` 的位址的最後 3 個 bits 一定為 0 ,因此只要對給定的引數 `(r)` 清除後面 3 個 bits 就可以得到父節點的位址,並且在引數 `(r)` 最後一個 bit 存放顏色信息, `(r)->color & 1` 取出最後一個 bit 。 ```c #define rb_parent(r) ((node_t *) ((r)->color & ~7)) #define rb_color(r) ((color_t) (r)->color & 1) ``` 在設定顏色時,以 bitwise 操作,若節點是紅色則設定最後一個bit 為 0 ,反之。 ```c #define rb_set_red(r) do { (r)->color &= ~1; } while (0) #define rb_set_black(r) do { (r)->color |= 1; } while (0) ``` `cmap_insert()` 是在紅黑樹中插入節點的功能,若比較結果 `res` 小於零代表往左邊走,否則往右邊走,直到走訪到的節點為 NULL,或迴圈結束,即插入節點。 ```c static bool cmap_insert(cmap_t obj, node_t *node, void *value) { /* Just insert the node in as the new head. */ /* Calibrate the tree to properly assign pointers.*/ ... /* 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) { 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; } ``` `tree_sort()` 會建立一個 map 配置 key 和 element 足夠的空間,接著在 while 迴圈將 list 中的每個節點一個一個插入 map 中。定義 `*node` 為 map 中擁有最小值的節點,透過 for 迴圈,從最左子開始在紅黑樹找到下一個節點(`cmap_next(node)` ),由小到大放入 list 中,完成 sort 功能。 ```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 ; } 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); } ``` ## 測驗二 #### AVL 樹 定義一個結構 `avl_node` ,其中 `parent_balance` 最低位 2 bits 紀錄 node 的平衡狀態,高位紀錄父節點的位址,`left` 和 `right` 分別代表 AVL 樹的左節點和右節點。 ```c struct avl_node { unsigned long parent_balance; struct avl_node *left, *right; } AVL_NODE_ALIGNED; ``` 因此想取出得到父節點就將 `parent_balance` 的高位取出,因為是 64 位元的系統,所以 `avl_node` 的位址的最後 3 個 bits 一定為 0,因此只要把最後 3 bits 設為 0 ,即可得到父節點的位址。 ```c static inline struct avl_node *avl_parent(struct avl_node *node) { return (struct avl_node *) ((node)->parent_balance & ~7); } ``` 定義一個列舉型別 `avl_node_balance` ,包含三個值,分別為 `AVL_NEUTRAL` 、 `AVL_LEFT` 和 `AVL_RIGHT` 。 - `AVL_NEUTRAL` : 左子樹和右子樹的深度一樣 - `AVL_LEFT` : 左子樹的深度 - 右子樹的深度 = 1 - `AVL_RIGHT` : 右子樹的深度 - 左子樹的深度 = 1 ```c enum avl_node_balance { AVL_NEUTRAL = 0, AVL_LEFT, AVL_RIGHT, }; ``` 因為 `parent_balance` 最低位 2 bits 存放節點的平衡狀態,利用 `& 3` 將最低位 2 bits 取出。 ```c static inline enum avl_node_balance avl_balance(const struct avl_node *node) { return (enum avl_node_balance)((node)->parent_balance & 3); } ``` 透過 `node->parent_balance & 3` 保留原本的 balance 資訊,利用 or 設定 parent。 透過 `node->parent_balance & ~7` 保留原本的 parent 資訊,利用 or 設定 balance。 ```c static void avl_set_parent(struct avl_node *node, struct avl_node *parent) { node->parent_balance = (unsigned long) parent | (node->parent_balance & 3); } static void avl_set_balance(struct avl_node *node, enum avl_node_balance balance) { node->parent_balance = (node->parent_balance & ~7) | balance; } ``` 插入新節點後需要確認 tree 是否平衡。換句話說,會先檢查插入節點是左子還是右子,再檢查 parent 的 balance 狀態在插入新節點後是否平衡。 如果插入的新節點是左子,就去檢查 parent 的 balancd 狀態,會出現三種可能的狀態:`AVL_RIGHT` 、 `AVL_NEUTRAL` 、 `AVL_LEFT`,當parent 是 `AVL_LEFT` 的狀態時表示左子樹高度比右子樹高 1 ,接著檢查插入節點的 balance ,如果是 `AVL_LEFT` 或 `AVL_NEUTRAL`,那麼就進行右旋操作(第42行),如果是 `AVL_RIGHT` ,那麼就進行左右旋操作,先將右子樹向左旋轉,再將原節點向右旋轉(第45行)。 ```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 */ ... case AVL_NEUTRAL: /* mark balance as right and continue upwards */ ... case AVL_LEFT: /* new right child + left leaning == balanced * nothing to propagate upwards after that */ ... } else { switch (avl_balance(parent)) { default: case AVL_RIGHT: /* new left child + right leaning == balanced * nothing to propagate upwards after that */ ... case AVL_NEUTRAL: /* mark balance as left and continue upwards */ ... 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; } } ``` ## 測驗三 這段程式碼是一個長度為 256 的表格,被稱為「對數表」(log table),用於計算二進制數字的位數(也就是 `log_table_256[x]` = $log_{2}x$ )。例如, log_table_256[6] 的值為 2 ,代表著二進位表示法中的 `00000110` ,第一個 1 位元出現在位置 2 。 ```c static const char log_table_256[256] = { #define _(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n -1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, _(4), _(5), _(5), _(6), _(6), _(6), _(6), _(7), _(7), _(7), _(7), _(7), _(7), _(7), _(7), #undef _ }; ``` 在程式碼中,它首先檢查最高的 32 位元,如果它們不是 0 ,則將它們向右移動 32 位元,並繼續檢查下一個 16 位元和 8 位元。找到的最高位元的對數與某些固定的值相加,以計算整個 64 位元的對數。因為表格中的值是以 2 為底的對數,所以它們的值域在 0 到 7 之間,可以直接用來計算每個 8 位元組的對數,而不需要進行實際的對數運算。 ```c /* ASSUME x >= 1 * returns smallest integer b such that 2^b = 1 << b is >= x */ 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; } ``` 這段程式碼是用來將一個 64 位元整數 x 根據 N_BITS 和 N_BUCKETS 的設定,將其分配到對應的桶(bucket)中。其中,mask111 和 mask011 分別是用來產生全為 1 的二進位數字,mask111的長度為 N_BITS + 1, mask011 的長度為 N_BITS 。 ```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)); } ``` ## 測驗四 ### 運作原理 考慮 `ceil_log2` 可針對給定無號 32 位元數值 x,找出 $⌈log2(x)⌉$,舉例來說: * `ceil_log2(7)` = 3 * `ceil_log2(8)` = 3 * `ceil_log2(9)` = 4 主要概念是找到最高位元的 `1` 是在 32 bits 中的哪一個 bit,因為可以根據最高位元的 `1` 來觀察這個數值是介於哪兩個 2 的次方數之間。 程式碼運作方式如下: 1. 變數 `r` 初始值為 0 ,目的是儲存計算後的對數結果。變數 `shift` 初始值為 0 ,目的是用來儲存計算每次 `x` 的偏移量。 2. 將 `x` 先減 1 ,確保四捨五入可以到最近的 2 的次方數 3. 檢查 `x` 是否大於 `0xFFFF` ,若是的話, `r` 會被設為 `16` ,否和 `r` = 0 ,並將 `x` 右移 `16` bits; 檢查 `x` 是否大於 `0xFFF` ,若是的話, `shift` 會被設為 `8`,否則 `shift` = 0 ,並將 `x` 右移 `8` bits,接著 `r` 和 `shift` 做結合,以此類推檢查 `0xF` 和 `0x3`。 4. 最後 `r` 會和最後剩下的偏移量 `shift`(可能是 0、1 或 2 個位元)及其移位後的結果結合,得出最終的對數結果。該結果最後會加 `1`,以取得向上取整後的對數值。 從 `0xFFFF` 開始檢查,是因為當輸入值大於 `0xFFFF` 時,右移 16 位元可以將輸入值的高 16 位元右移到低 16 位元,這樣可以把輸入值的位數減少一半,從而加快計算速度。接著,再根據輸入值的大小,從 `0xFF`、`0xF`、`0x3` 分別開始檢查,進行對應的右移操作,最後計算出以 2 為底數的對數的位數。 ```c 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; } ``` Trace `ceil_log2(0x0FFFFFFF)` 的執行過程。 ```c shift = 0 r = 0 x = 0x0FFFFFFF x = 0x0FFFFFFE x > 0xFFFF r = 0x10 x = 0x00000FFF x > 0xFF shift = 0x08 x = 0x0000000F r = 0x18 x > 0xF shift = 0x04 x = 0 r = 0x1C x < 0x3 shift = 0 return (0x1C | 0 | 0 ) + 1 = 0x1D //以 2 為底數的對數的位數 ```