--- tags: linux2023 --- # 2023q1 Homework3 (quiz3) contributed by < [`paul90317`](https://github.com/paul90317) > > [作業要求](https://hackmd.io/@sysprog/linux2023-quiz3) ### 測驗 `1` 觀察 [原始碼](https://gist.github.com/jserv/920766d1fd893d0efd66ac7fdaf23401) ```c typedef struct __node { uintptr_t color; struct __node *left, *right; struct __node *next; long value; } node_t __attribute__((aligned(sizeof(long)))); ``` 會將顏色與親節點指標的值存在 `color` 成員裡面,用於存取的程式碼如下 ```c #define rb_parent(r) ((node_t *) (AAAA)) #define rb_color(r) ((color_t) (r)->color & 1) ``` 由於指標後兩位(64 bits 系統是三位)都是 0,且我們只會用到其中一位,`AAAA` 是 `(r)->color & ~1` :::warning `AAAA` 不可以是 `(r)->color & ~1u` 因為在做位元且之前會將 `~1u` 從 32 位元擴展到 64 位元,如果是無號擴展再位元且,將會遺失指標高位位元。 ::: ```c #define rb_is_red(r) (!rb_color(r)) #define rb_is_black(r) (rb_color(r)) #define rb_set_parent(r, p) \ do { \ (r)->color = rb_color(r) | (uintptr_t) (p); \ } while (0) #define rb_set_red(r) \ do { \ BBBB; \ } while (0) #define rb_set_black(r) \ do { \ CCCC; \ } while (0) ``` 從以上程式碼看出紅色是 0,黑色是 1,`BBBB` 應該是 `(r)->color &= ~1`,而 `CCCC` 應該是 `(r)->color |= 1`。 觀察 `cmap_insert` 函式的程式碼片段 ```c if (res < 0) { if (!cur->left) { cur->left = node; rb_set_parent(node, cur); cmap_fix_colors(obj, node); break; } DDDD; } else { if (!cur->right) { cur->right = node; rb_set_parent(node, cur); cmap_fix_colors(obj, node); break; } EEEE; } ``` 根據該函式的意圖,`DDDD` 與 `EEEE` 應是在 `cur` 不是 internal node 的情況下,存取下一個節點,故 `DDDD` 是 `cur = cur->left` 而 `EEEE` 是 `cur = cur->right`。 觀察 `tree_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 = FFFF; } node_t *node = cmap_first(map), *first = node; for (; node; node = cmap_next(node)) { *list = node; list = GGGG; } HHHH; *record = first; free(map); } ``` 可以看出 `while` 迴圈想要將 `list` 所有節點插入 `map`,所以 `FFFF` 應是 `&(*list)->next` 以存取下一個要插入的節點。 `for` 迴圈想要用 `cmap_next` 走訪節點並構造新的數列,所以 `GGGG` 應該要是 `&(*list)->next`。 :::info 順便一提指標的指標讓 `list` 可以直接在從 `map` 拿到 `node` 後再給予值,否則程式碼將變成如下 ```c node_t *node = cmap_first(map), *first = node, *last = NULL; for (; node; node = cmap_next(node)) { if (last != NULL) { last->next = node; } last = node; } ``` 將於每次判斷 `node` 是否是第一個節點,或是是以下 ```c node_t *last = cmap_first(map), *node = cmap_next(map), *first = last; for (; node; node = cmap_next(node)) { last->next = node; last = node; } ``` 將第一個節點分開判斷。 ::: `HHHH` 是 `*list = NULL` 對新數列進行收尾。 ### 測驗 `2` 觀察 [原始碼](https://gist.github.com/jserv/d03098d57fec03c89fdb2a449d7f6be2) ```c struct avl_node { unsigned long parent_balance; struct avl_node *left, *right; } AVL_NODE_ALIGNED; enum avl_node_balance { AVL_NEUTRAL = 0, AVL_LEFT, AVL_RIGHT, }; static inline struct avl_node *avl_parent(struct avl_node *node) { return (struct avl_node *) (IIII); } static inline enum avl_node_balance avl_balance(const struct avl_node *node) { return (enum avl_node_balance)(JJJJ); } ``` 在以上程式碼中,由於 AVL 樹的節點有三個狀態,須給予兩個位元,故 `IIII` 是 `node->parent_balance & ~3` 而 `JJJJ` 是 `node->parent_balance & 3` ```c static void avl_set_parent(struct avl_node *node, struct avl_node *parent) { node->parent_balance = (unsigned long) parent | (KKKK); } static void avl_set_balance(struct avl_node *node, enum avl_node_balance balance) { node->parent_balance = (LLLL) | balance; } ``` 在上述程式碼中,`KKKK` 應是原本的狀態,故是 `avl_balance(node)`,`LLLL` 是 `avl_parent(node)`。 觀察 `avl_insert_balance` 的程式碼片段 ```c /* compensate double left balance by rotation * and stop afterwards */ switch (avl_balance(node)) { default: case AVL_LEFT: case AVL_NEUTRAL: MMMM(node, parent, root); break; case AVL_RIGHT: NNNN(node, parent, root); break; } ``` 以上程式碼發生於當 `node` 是 `parent` 左邊的節點 當樹向左邊傾斜時,應該將 `parent` 向右旋轉,故 `MMMM` 是 `avl_rotate_right`, 在 `node` 是向右邊傾斜的情況,`NNNN` 應該先將`node` 向左轉再將 `parent` 向右轉,故是 `avl_rotate_leftright`。 ### 測驗 `3` 觀察 [原始碼](https://gist.github.com/jserv/15c6ef4383d5d010326cdc4382ffdd1d) ```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 _ }; #undef _ ``` 在查找表 `log_table_256`,輸入 0 得到 -1,輸 1 得 0,輸 2、3 得 1,輸 $n$ 得 $\lfloor{log_2n}\rfloor$,最高到輸入 255 得 7。 255 所佔用的位元會是輸出值 $\lfloor{log_2255}\rfloor+1$,也就是 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 = AAAA + log_table_256[t]; } else { r = BBBB + log_table_256[tt]; } } else { t = ttt >> 8; if (t) { r = CCCC + log_table_256[t]; } else { r = DDDD + log_table_256[ttt]; } } } else { tt = v >> 16; if (tt) { t = tt >> 8; if (t) { r = EEEE + log_table_256[t]; } else { r = FFFF + log_table_256[tt]; } } else { t = v >> 8; if (t) { r = GGGG + log_table_256[t]; } else { r = HHHH + log_table_256[v]; } } } return r; } ``` 以上程式碼我覺得是有點類似遞迴的作法,只是為了更好的效率所以將遞迴函式展開,於是我將該程式傳回遞迴,如下 ```c uint64_t _log2_64(uint64_t v, uint8_t shift) { if (shift == 4) // 改 !(v >> 8) 剪枝 (pruning) 會有更好的效率 return log_table_256[v] if (v >> shift) return _log2_64(v >> shift, shift >> 1) + t; else return _log2_64(v, shift >> 1); } uint64_t log2_64(uint64_t v) { _log2_64(v, 32) } ``` 對照兩者後可以得出 * `AAAA` 是 32 + 16 + 8 是 56 * `BBBB` 是 32 + 16 是 48 * `CCCC` 是 32 + 8 是 40 * `DDDD` 是 32 * `EEEE` 是 16 + 8 是24 * `FFFF` 是 16 * `GGGG` 是 8 * `HHHH` 是 0 ```c static unsigned int N_BUCKETS; static unsigned char N_BITS; void set_N_BUCKETS(unsigned int n) { N_BUCKETS = n; } void set_N_BITS() { N_BITS = log2_64(N_BUCKETS); } /* n == number of totally available buckets, so buckets = \{0, ...,, n-1\} * ASSUME n < (1 << 32) */ 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 & IIII)) + ((1 - leq) * ((x >> (N_BITS + 1)) & JJJJ)); /* 'x >> (N_BITS + 1)' : take different set of bits -> better uniformity. * '... & mask011' guarantees that the result is less or equal N_BUCKETS. */ } ``` 舉一個例子,假設 `N_BUCKETS` 是 256,`N_BITS` 就是 8,`mask111` 是 511,將這個數字對 x 做位元且可能會超出桶數。 當 `leq` 為 1 時,`bucket_number` 應輸出正常的桶號,也就是 `x & mask111`,故 `IIII` 是 `mask111`。 反之,根據註解,應當要取較少的位元,故 `JJJJ` 是 `mask011` ### 測驗 `4` ```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 (KKKK) + 1; } ``` 因為 $\lceil log_2n\rceil=\lfloor log_2(n-1)\rfloor+1$,先將程式碼改寫成方便理解的形式,如下。 ```c= inline int floor_log2(uint32_t x) { uint32_t r, shift; // 若 x 超過 16 位元,向右位移 16 個位元 r = (x > 0xFFFF) << 4; x >>= r; // 若 x 超過 8 位元,向右位移 8 個位元 shift = (x > 0xFF) << 3; x >>= shift; r |= shift; // 視為加法 // 若 x 超過 4 位元,向右位移 4 個位元 shift = (x > 0xF) << 2; x >>= shift; r |= shift; // 視為加法 // 若 x 超過 2 位元,向右位移 2 個位元 shift = (x > 0x3) << 1; x >>= shift; return (KKKK); } int ceil_log2(uint32_t x) { return ceil_log2(x - 1) + 1; } ``` 經過 21 行的位移後,x 最多只剩 2 個位元,於是 `KKKK` 就是前一次的結果 `r | shift` 加上左邊數來第二個位元的結果 `x >> 1` 故 `KKKK` 是 `r | shift | x >> 1`。 :::success **2. 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless** 程式碼會在 `x--` 的時候出問題,最簡單的作法就是使用測驗 3 `bucket_number` 最後一行的作法,將 `ceil_log2` 改寫如下 ```c inline int _floor_log2(uint32_t x) { uint32_t r, shift; // 若 x 超過 16 位元,向右位移 16 個位元 r = (x > 0xFFFF) << 4; x >>= r; // 若 x 超過 8 位元,向右位移 8 個位元 shift = (x > 0xFF) << 3; x >>= shift; r |= shift; // 視為加法 // 若 x 超過 4 位元,向右位移 4 個位元 shift = (x > 0xF) << 2; x >>= shift; r |= shift; // 視為加法 // 若 x 超過 2 位元,向右位移 2 個位元 shift = (x > 0x3) << 1; x >>= shift; return r | shift | x >> 1; } int floor_log2(uint32_t x) { unsigned char iszero = !x; return -iszero + (1 - iszero) * _floor_log2(x); } int ceil_log2(uint32_t x) { unsigned char iszero = !x; return -iszero + (1 - iszero) * (_floor_log2(x - 1) + 1); } ``` :::