---
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 核心排程器就有)
:::