owned this note
owned this note
Published
Linked with GitHub
# 2023q1 Homework3 (quiz3)
contributed by < [cin-cout](https://github.com/cin-cout) >
[題目](https://hackmd.io/@sysprog/linux2023-quiz3)
## 測驗一
### 延伸問題:
1. 指出 treesort.c 程式碼的缺失並改進
2. 利用 Linux 核心的 list.h 標頭檔改寫並整合進第一次作業的實驗程式碼中,探討 Tree sort 的效率
3. 研讀 Linux 核心的 Red–black tree 程式碼,比較給定的 treesort.c 實作手法,指出後者的改進空間並予以實作
### Question
考慮一個 Tree sort 的實作,搭配 Red–black tree 作為排序所需的自我平衡樹,原始程式碼可見 [treesort.c](https://gist.github.com/jserv/920766d1fd893d0efd66ac7fdaf23401)。部分程式碼已遮蔽,請補完程式碼,使其運作符合預期。
先簡單介紹一下 [treesort.c](https://gist.github.com/jserv/920766d1fd893d0efd66ac7fdaf23401) 的內容,主要是利用紅黑樹去實作map。
資料結構如下:
tree node 的方面, `color` 紀錄其顏色、`left` `right` 則記錄其左右子節點 `next` 是用來記錄在原本 array 中其下一個位置在 RB tree 中 node 的位置、`value` 則是記錄其值。
```c
typedef struct __node {
uintptr_t color;
struct __node *left, *right;
struct __node *next;
long value;
} node_t __attribute__((aligned(sizeof(long))));
```
map 的資料結構則會記錄 RB tree 的 root,以及其他 map 之中的一些特質(ex size)。
```c
struct cmap_internal {
node_t *head;
/* Properties */
size_t key_size, element_size, size;
cmap_iter_t it_end, it_most, it_least;
int (*comparator)(void *, void *);
};
```
#### AAAA
這邊用了一個特殊的方法來儲存顏色。因為現在電腦多數都是 64 bits (8 bytes),所以其位置二進位最後三位必為 0。所以這邊就用了 node 的 parent 的位址的第一個 bit 來儲存 node 之顏色,如此一來找 parent 只需要對其顏色做 bit wise 的處理,不須額外花費記憶體空間,紅色為 0 黑色為 1。
由上述邏輯可知要找 node 的 parent 只需把 node 的 color 前三個(或第一個) bits 設為 0 ,故 AAAA = `((r)->color & ~7)`。
```c
typedef enum { CMAP_RED = 0, CMAP_BLACK } color_t;
#define rb_parent(r) ((node_t *) (AAAA))
#define rb_color(r) ((color_t) (r)->color & 1)
```
#### BBBB CCCC
同 AAAA 提到的原理,透過更改其 parent 的 last bit 來記錄顏色,在 set color 時,紅色為 0 ,黑色為 1。
故 BBBB 為 `(r)->color &= ~1`。
故 CCCC 為 `(r)->color |= 1`。
```c
#define rb_set_red(r) \
do { \
BBBB; \
} while (0)
#define rb_set_black(r) \
do { \
CCCC; \
} while (0)
```
#### DDDD EEEE
rb tree insert node 的程式碼如下,DDDD、EEEE 前的部分是在處理 create root 的狀況,為解答出 DDDD、EEEE,我們著重於 for 迴圈的部分。
for 迴圈處的邏輯跟 binary tree insert 的方式類似,即若小於此處的 node 則往左走,反之大於則往右走,直到走到 null 的位置即為要插入的位置。
故 DDDD 為 cur = cur->left。
故 EEEE 為 cur = cur->right。
另外,插入時會需要先 set parent,而 `cmap_fix_colors` 則會對於 ll lr rl rr 的情況去平衡紅黑數。
```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) {
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;
}
}
cmap_calibrate(obj);
return true;
}
```
#### FFFF GGGG HHHH
最後則是實際使用 map 的情況,輸入會是一個 list 而 map 則會使用紅黑樹將其排序。
FFFF 是為將 list 內的值一一傳入 map 做 insert,故在每次 while 時都必須將 *list 改下一個值的位置。
故 FFFF = &(*list)->next
for 迴圈的部分是將排序完成後的結果放回 list。每次迴圈都會更新到下一個 node ,GGGG 則是在耕莘 list 的位置。
故 GGGG 同為 &(*list)->next。
將 list 更新完畢以後,必須確保最後一個值的 next 為 null, list 在 for 結束後會落在,tail->next 的位置。
故 HHHH 為 *list = NULL。
```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);
}
```
### Answer
:::success
AAAA = `((r)->color & ~7)`
BBBB = `(r)->color &= ~1`
CCCC = `(r)->color |= 1`
DDDD = `cur = cur->left`
EEEE = `cur = cur->right`
FFFF = `&(*list)->next`
GGGG = `&(*list)->next`
HHHH = `*list = NULL`
:::
## 測驗二
### 延伸問題:
1. 解釋上述程式碼運作原理,並比較用紅黑樹實作的 priority queue,應提供完整的測試程式和效能分析
2. Linux 核心也有 AVL tree 的身影,但陸續被紅黑樹替代,請揣摩 Linux 核心開發者的考慮因素,並搭配 git log 來解讀
見 commit b145425
3. 研讀 Linux 核心原始程式碼,指出曾經/現在使用 AVL tree 的部分,對照 Linux 核心的 rbtree.h,探究是否可用後者替換,從而爭取到貢獻程式碼到 Linux 核心的機會
### Question
考慮一個使用 AVL tree 實作的 priority queue。
其中 `avltree.h` 的程式碼可見 [avltree.h](https://gist.github.com/jserv/d03098d57fec03c89fdb2a449d7f6be2)。部分程式碼已遮蔽,請補完程式碼,使其運作符合預期。
AVL tree 是嚴格的平衡樹,每一個節點的左子樹高度跟右子樹的高度不差超過 1 。
先講解 AVL tree node 的資料結構:
`left` `right` 是紀錄其左右子節點的位址,`parent_balance` 跟上一題 rb tree node 用了類似的手法,原因以及好處可見上題。後三位 bits 用來儲存此節點 balance 的狀況,高位元用來儲存父節點的位址。
```c
struct avl_node {
unsigned long parent_balance;
struct avl_node *left, *right;
} AVL_NODE_ALIGNED;
```
balance 的狀況有三種,分別為:
`AVL_NEUTRAL (0)` 左右子樹高度相等。
`AVL_LEFT (1)` 左子樹高 1 。
`AVL_RIGHT (2)` 右子樹高 1 。
```c
enum avl_node_balance { AVL_NEUTRAL = 0, AVL_LEFT, AVL_RIGHT, };
```
#### IIII
如同上述的解釋,在找 parent 時,即抓取 parent_balance 的高位元。
故 IIII 為 (node)->parent_balance & ~7。
```c
static inline struct avl_node *avl_parent(struct avl_node *node)
{
return (struct avl_node *) (IIII);
}****
```
#### JJJJ
找 balance 也一樣,即抓取 parent_balance 的最低三位元,因為 balance 只有 0 1 2,所以只會用到最低二位,可以只取最低兩位,放 & 7 也可以。
故 JJJJ 為 (node)->parent_balance & 3。
```c
static inline enum avl_node_balance avl_balance(const struct avl_node *node)
{
return (enum avl_node_balance)(JJJJ);
}
```
#### KKKK
更新 parent 必須要考慮到 parent 跟 balance , set parent 只要更新父節點的位置,balance 保持一樣。函式中已經有更新 parent 的實作,缺少保留 balance 的實作部分。
故 KKKK 為 (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);
}
```
#### LLLL
反之,在更新 balance 時,也必須保持原 parent 的狀況。
故 LLLL 為 (node)->parent_balance & ~7。
```c
static void avl_set_balance(struct avl_node *node,
enum avl_node_balance balance)
{
node->parent_balance = (LLLL) | balance;
}
```
#### MMMM NNNN
最後則是在插入新的節點後平衡的處理。
簡單講解 AVL 樹的平衡方式,首先檢查它是其父節點的左子節點還是右子節點。對於右子節點,它根據父節點的 balance 做出不同的處理。
如果父節點的 balance 是 AVL_LEFT,意味著它的左子樹比右子樹高,這樣 node 插入右側就沒什麼問題。
若是 AVL_RIGHT,意味著它的右子樹比左子樹高,這樣 node 插入右側就則需要做旋轉的處理,處理方式則是依照 node 本身的 balance來決定,來保持 AVL 樹的平衡性。
若是 AVL_NEUTRAL,則需要繼續往上檢查到上面的節點需不需要旋轉。
對於左子節點,它的處理方式類似,只是 balance 的方向相反。
故 MMMM 為 avl_rotate_right。
故 NNNN 為 avl_rotate_leftright。
```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:
MMMM(node, parent, root);
break;
case AVL_RIGHT:
NNNN(node, parent, root);
break;
}
parent = NULL;
break;
}
}
if (!parent)
break;
node = parent;
}
}
```
### Answer
:::success
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`
:::
## 測驗三
### 延伸問題:
1. 解釋 Linear feedback shift register (LFSR) 的運作原理,搭配在 Linux 核心原始程式碼中,用 git log 和 grep 找出 LFSR 的應用案例
2. 解釋上述程式碼運作的原理
3. 在 Linux 核心原始程式碼中找到 log2(x) 的實作 (整數),要涵蓋不同的整數型態寬度,以及是否能運用微處理器提供的指令來加速。也從 Linux 核心找出整數 log2(x) 的應用案例並解說。
4. lab0-c 提供類似的實作 log2_lshift16.h,解釋其原理並嘗試改進其效能
### Question
考慮一個運用 LFSR 來產生隨機數,並使其均勻分佈於 64 位元無號整數的有效空間 (distribution of uniformly arbitrarily large uint64_t)。完成 [bucket_uniform.c](https://gist.github.com/jserv/15c6ef4383d5d010326cdc4382ffdd1d) (部分程式碼遮蔽)。
#### AAAA ~ HHHH
先看 log2_64 這個函式,此函式的目的是找到 64 bits 數的 log2 floor int。在測驗中,這個函式的目的是找到一個 64 bits 的 FSB (第一個 1 的) 位置。
使用的方法為,使用不斷的 shift 的方式,找到 64 bits FSB 在哪一個 bytes,使用這個 bytes 去查表找到其 log2 值,最後要加上前面 shift 的數量。
```
舉例:
0x0100000000000000
在例子中 shift 了 48 次。
使用 1 這個 bytes 去查表時,得到的結果為 1 。
所以最後 r 為 49。
```
其意義為,因為找 log2 floor 意及找 FSB 的位置,在這個例子中,我們用查表得到了 1 的 log2 (意及其在這 8 bytes 中 FSB 的位置),但前面 shift 掉了 1 後面的 48 個 bits,所以最終 FSB 的位置必須加上 48。
故 AAAA 為 56 。
故 BBBB 為 48 。
故 CCCC 為 40 。
故 DDDD 為 32 。
故 EEEE 為 24 。
故 FFFF 為 16 。
故 GGGG 為 8 。
故 HHHH 為 0 。
```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 = 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;
}
```
#### IIII JJJJ
IIII JJJJ 在 `bucket_number` 這個函式中,但是在理解怎麼做此函式前,必須知道其用處。
所以我們先解釋,`fill_buckets` 這個函式:
步驟為:
1. while 迴圈會利用多次的 lfsr 找到一個 64 bits 的隨機數 x。
2. 利用這個隨機數,放入 `bucket_number` 得到一個小於設定的 N_BUCKETS 的數,所以得出來的 tmp_bucket 也可以視為 1 隨機數。
以一套這樣的方法,就可以產生固定範圍的隨機數了!
而 for 迴圈只是重複做多次上數的步驟,用來統計產生出來的隨機數。
(lfsr 原理請見題目)
```c
void fill_buckets(unsigned int *buckets, unsigned int iterations)
{
uint64_t x = 0x98765421b16b00b5;
unsigned char lfsr_iter = (N_BITS << 1);
for (uint64_t i = 0; i < iterations; i++) {
unsigned int tmp_bucket = bucket_number(x);
*(buckets + tmp_bucket) = *(buckets + tmp_bucket) + 1;
// 'turn handle' on LFSR lfsr_iteration-times!
unsigned char ell = 0;
while (ell < lfsr_iter) {
lfsr(&x);
ell++;
}
}
}
```
如此一來就可以來探討 `bucket_number` 這個函式了,這個函式的目的即為將傳入的 64 bits 整數轉換為一規定範圍內的數字。
首先,有兩種 mask,配合上面 log2_64 的意義,找到一個 64 bits 的 FSB (第一個 1 的) 位置,不難理解其意義:
mask111:包含 FSB 之下的 bits 全為 1。
mask011:不包含 FSB 之下的 bits 全為 1。
之後 leq 的意義為判斷 64 bits 的 x 是否有大於我們設定的 N_BUCKET。
最後的 return 則分為兩種狀況:
1. 若傳入的 x 就已經比 N_BUCKET 小了,直接回傳 x。
2. 若傳入的 x 大於等於 N_BUCKET ,取 x 的高位元段回傳。
第一個很好理解是直接對 mask111 做 and,而第二個因為確保高位元段的值一定比 N_BUCKET 小,所以是跟 mask011 做 and 。
故 IIII 為 mask111。
故 JJJJ 為 mask011。
```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 & 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.
*/
}
```
### Answer
:::success
AAAA = `56`
BBBB = `48`
CCCC = `40`
DDDD = `32`
EEEE = `24`
FFFF = `16`
GGGG = `8`
HHHH = `0`
IIII = `mask111`
JJJJ = `mask011`
:::
## 測驗四
### 延伸問題:
1. 解釋上述程式碼運作原理
2. 改進程式碼,使其得以處理 x = 0 的狀況,並仍是 branchless
3. 在 Linux 核心原始程式碼找出 ceil 和 log2 的組合 (類似的形式),並解說應用案例,例如在 Linux 核心排程器就有。善用 lxr 和 git log
### Question
已知輸入必為大於 0 的數值 (即 x > 0),以下程式碼可計算
⌈log2(x)⌉,也就是 ceil 和 log2 的組合並轉型為整數:
```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;
}
```
#### KKKK
邏輯跟上一題的 log2 floor 類似,只是不是用 control flow 的方式找到 FSB 所在的位置,而是無論是哪一種情況都能成功計算 FSB 的位置。
接著解釋程式碼:
```c
int ceil_log2(uint32_t x)
{
uint32_t r, shift;
/*
因為是求 ceil ,最後結果都會加 1 ,
但是對於本來就是 2 冪次的數反而會多加 1 ,
所以一開始先減掉 1 避免此問題。
*/
x--;
/*
如果 x 前 16 bits 有值,r = 16 反之 r = 0
r 的意義跟上題類似是用來記錄目前統計到 FSB 後的值。
*/
r = (x > 0xFFFF) << 4;
/*
x shift r 次,即後面 16 位已討論完,繼續討論前 16 位。
若 shift 0 次,即前 16 位沒值,直接討論後 16 位。
*/
x >>= r;
/*
同理,只是改為處理 x 前 16 bits 或後 16 bits,而非 32 bits。
*/
shift = (x > 0xFF) << 3;
/*
因為 r 是代表累計的 shift 數量所以,改為用 shift 紀錄。
*/
x >>= shift;
/*
因為 shift 是從 16 -> 8 -> 4 -> 2 討論。
所以可以利用 r |= shift 代替 r += shift。
*/
r |= shift;
/*
同理。
*/
shift = (x > 0xF) << 2;
x >>= shift;
r |= shift;
shift = (x > 0x3) << 1;
x >>= shift;
/*
最後因為要直接 return 了,所以將 r |= shift ,
直接留在 KKKK 做,而此時 x 還剩下 2 位,所以最後
累計的 shift 還要在 + x>>1 (例如 x = 0b10)。
故 KKKK 為 r | shift | x >> 1。
最後的 + 1 是因為是求 ceil。
*/
return (KKKK) + 1;
}
```
### Answer
:::success
KKKK = `r | shift | x >> 1`
:::