# 2024q1 Homework2 (quiz1+2) contributed by < `max890808` > ## [第一周測驗題](https://hackmd.io/@sysprog/linux2024-quiz1) ### 測驗一 參考 [Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/) 實作非遞迴 (non-recursive; iterative) 的快速排序法 #### 解釋程式碼運作原理 ```c node_t *begin[max_level], *end[max_level]; node_t *left = NULL, *right = NULL; ``` `begin` 和 `end` 用來紀錄比較的範圍 `left` 存放比 `pivot` 的 value 小的 node_t `right` 存放比 `pivot` 的 value 大的 node_t **圖解** 假設有一串列 [2, 0, 4, 1, 3] ,此時 i 為0 ,`left` 和 `right` 皆為NULL ```graphviz digraph struct1 { rankdir=TD; pivot [fontcolor="red", shape=plaintext]; L [fontcolor="blue", shape=plaintext]; R [fontcolor="blue", shape=plaintext]; A[label = "2", shape=box]; B[label = "0", shape=box]; C[label = "4", shape=box]; D[label = "1", shape=box]; F[label = "3", shape=box]; G[label = "NULL", shape=plaintext]; {rank=same A B C D F G} {rank=same pivot L R } A->B->C->D->F->G; pivot->A L->A R->F } ``` 遍歷整個串列,比 `pivot` 的 value 小的會被放入 `left` 串列,反之放入 `right` 串列 ```diff while (p) { node_t *n = p; - p = CCCC; + p = p->next; list_add(n->value > value ? &right : &left, n); } ``` ```graphviz digraph struct1 { rankdir=TD; left [fontcolor="blue", shape=plaintext]; pivot [fontcolor="red", shape=plaintext]; right [fontcolor="blue", shape=plaintext]; A[label = "2", shape=box]; B[label = "0", shape=box]; C[label = "4", shape=box]; D[label = "1", shape=box]; F[label = "3", shape=box]; {rank=same left pivot right} {rank=same D A F} {rank=same B C} left->D->B; pivot->A; right->F->C; } ``` 更新 `begin` 和 `end` ```diff begin[i] = left; - end[i] = DDDD; + end[i] = list_tail(&left); begin[i + 1] = pivot; end[i + 1] = pivot; begin[i + 2] = right; - end[i + 2] = EEEE; + end[i + 2] = list_tail(&right); ``` ```graphviz digraph struct2 { rankdir=TD; begin [fontcolor="blue", shape=plaintext]; end [fontcolor="blue", shape=plaintext]; A[label = "2", shape=box]; B[label = "0", shape=box]; C[label = "4", shape=box]; D[label = "1", shape=box]; F[label = "3", shape=box]; E[label = "2", shape=box]; {rank=same begin end} {rank=same A E} {rank=same F C} begin->D->A->F end->B->E->C } ``` 進入下一次迭代,此時 i = 2 ```graphviz digraph struct3 { rankdir=TD; pivot [fontcolor="red", shape=plaintext]; L [fontcolor="blue", shape=plaintext]; R [fontcolor="blue", shape=plaintext]; NULL [shape=plaintext]; C[label = "4", shape=box]; F[label = "3", shape=box]; {rank=same pivot L R} {rank=same C F NULL} F->C->NULL pivot->F L->F R->C } ``` ```graphviz digraph struct4 { rankdir=TD; begin [fontcolor="blue", shape=plaintext]; end [fontcolor="blue", shape=plaintext]; A[label = "2", shape=box]; B[label = "0", shape=box]; C[label = "NULL", shape=box]; D[label = "1", shape=box]; F[label = "NULL", shape=box]; E[label = "2", shape=box]; G[label = "3", shape=box]; H[label = "3", shape=box]; I[label = "4", shape=box]; J[label = "4", shape=box]; {rank=same begin end} {rank=same A E} {rank=same F C} {rank=same G H} {rank=same I J} begin->D->A->F->G->I end->B->E->C->H->J } ``` 此時 `begin[4]` = 4 和 `end[4]` = 4, 不滿足 if 的條件,因此進行 else 的動作,透過 `list_add` 將 `begin[4]` 放入 `result` ,並將 `i` 減一 ```c if (L) list_add(&result, L); i--; ``` 當索引 i 扣到小於 0 時,while 條件不成立跳出迴圈,完成排序,將 result 指派給 list ```graphviz digraph struct1 { rankdir=TD; label="list"; A[label = "0", shape=box]; B[label = "1", shape=box]; C[label = "2", shape=box]; D[label = "3", shape=box]; F[label = "4", shape=box]; G[label = "NULL", shape=plaintext]; {rank=same A B C D F G} A->B->C->D->F->G; } ``` #### 使用Linux 核心風格的 List API 改寫上述程式碼 > commit [7ff3f48](https://github.com/max890808/Linux-homework/commit/7ff3f488bc2052605ed9ac7fa611a458335a1e9d) 將原本的結構體改為使用 `list_head`,詳細修改可以看 commit 紀錄 ```diff! typedef struct __node { - struct __node *left, *right; - struct __node *next; + struct list_head list; long value; } node_t; ``` #### 改進方案 * **max_level 大小** 原本的 `max_level` 為 `2 * list_legnth(list)` ,但最差的情況下只需要 `list_legnth(list) + 1` ,因此作以下修正: ```diff - int max_level = 2 * n; + int max_level = n + 1; ``` * **移除 end[]** `end[i]` 可由 `begin[i]->prev` 取得,因此不需要使用額外的儲存空間 ```diff - node_t *L = begin[i], *R = end[i]; + struct list_head *L = begin[i]->next, *R = begin[i]->prev; ``` #### 避免最差狀況的快速排序 > commit [4644839](https://github.com/max890808/Linux-homework/commit/464483992175f2dd934526ea805f8a68b806ab8f) 最差的狀況會發生在 `Pivot` 恰好是該資料陣列的最小值或最大值,這裡的解決方式為 `Randomized Quick Sort` ,用亂數選取的方式,隨機挑一個值作為 `Pivot` ,並使用 `<time.h>` 比較最差狀況的結果 | 資料總數 | `quick_sort` 第一次 (s) | `quick_sort` 第二次 (s) | `quick_sort` 第三次 (s) | `quick_sort` 平均 (s) |`random_quick_sort` 第一次 (s) | `random_quick_sort` 第二次 (s) | `random_quick_sort` 第三次 (s) | `random_quick_sort` 平均 (s) | | ------- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | ----- | | 1000 | 0.009 | 0.008 | 0.009 | 0.009 | 0.001 | 0.001 | 0.001 | 0.001 | | 10000 | 0.600 | 0.621 | 0.578 | 0.600 | 0.005 | 0.006 | 0.006 | 0.006 | | 100000 | 55.475 | 55.576 | 57.474 | 56.175 | 0.057 | 0.057 | 0.070 | 0.061 | ### 測驗二 Timsort 定義一個 `minrun` 控制每個 run 的長度,使 run 的數量等於或者略小於 2 的冪,這樣可以提高合併的效率。並且採用一組堆疊 (stack) 來暫存 run,避免在一開始就掃描整個資料範圍來產生 run,從而降低記憶體開銷。此外,透過 `merge_collapse` 函式確保 run 的長度,函式策略如下: * A > B + C * B > C 當不符合上述條件時,則會判斷 A 、C 大小,小的跟 B 合併。在合併兩個 run 時,Timsort 會有一個機制決定是否要啟動 `Galloping mode` 。 #### 解釋程式碼運作原理 ```c do { /* Find next run */ struct pair result = find_run(priv, list, cmp); result.head->prev = tp; tp = result.head; list = result.next; stk_size++; tp = merge_collapse(priv, cmp, tp); } while (list); ``` 不斷呼叫 `find_run` 函式找出下一個 run,並根據 run 的長度和目前的堆疊狀態,呼叫 `merge_collapse()` 合併不滿足條件的 run。 ```c struct list_head *stk0 = tp, *stk1 = stk0->prev; while (stk1 && stk1->prev) stk0 = stk0->prev, stk1 = stk1->prev; if (stk_size <= FFFF) { build_prev_link(head, head, stk0); return; } merge_final(priv, cmp, head, stk1, stk0); ``` 使用 `build_prev_link()` 使原本單向串列成為環狀雙向鏈結串列,並透過 `marge_final()` 將最後兩段 run 進行合併。 #### 改進方案 * 實作 galloping mode 並加入 `MIN_GALLOP` 判斷 `galloping mode` 和 `one-pair-at-a-time mode`的使用時機 **TODO :** * 實作改進方案 * 將改進過的 Timsort 實作整合進 sysprog21/lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。 ## [第二周測驗題](https://hackmd.io/@sysprog/linux2024-quiz2) ### 測驗一 Linux 核心的 hash table 與典型的雙向環狀鏈結串列不同,hash table 所使用的 pprev 宣告為指標的指標,這樣的好處為 `hlist_head` 僅需要保存一個指標,當 bucket 很大時,就能減少記憶體開銷。示意圖如下: ```graphviz digraph G { rankdir=LR; node[shape=record]; map [label="hlist_head.first |<ht0> |<ht1> |<ht2> |<ht3> |<ht4> |<ht5> |<ht7> |<ht8> "]; node[shape=none] null1 [label=NULL] null2 [label=NULL] subgraph cluster_1 { style=filled; color=lightgrey; node [shape=record]; hn1 [label="hlist_node | {<prev>pprev | <next>next}"]; label="hash_key 1" } subgraph cluster_2 { style=filled; color=lightgrey; node [shape=record]; hn2 [label="hlist_node | {<prev>pprev | <next>next}"]; label="hash_key 2" } subgraph cluster_3 { style=filled; color=lightgrey; node [shape=record]; hn3 [label="hlist_node | {<prev>pprev | <next>next}"]; label="hash_key 3" } map:ht1 -> hn1 hn1:next -> hn2 hn2:next -> null1 hn2:prev:s -> hn1:next:s map:ht5 -> hn3 hn3:next -> null2 hn1:prev:s -> map:ht1 hn3:prev:s -> map:ht5 } ``` #### 解釋程式碼的運作 **`find`** ```diff static int find(int num, int size, const struct hlist_head *heads) { struct hlist_node *p; int hash = (num < 0 ? -num : num) % size; - hlist_for_each (p, BBBB) { + hlist_for_each (p, &heads[hash]) { - struct order_node *on = CCCC(p, struct order_node, node); + struct order_node *on = list_entry(p, struct order_node, node); if (num == on->val) return on->idx; } return -1; } ``` 透過雜湊值和 `list_entry` 找出目標結構體的位置,回傳相對應 `idx` 值 **`node_add`** ```diff static inline void node_add(int val, int idx, int size, struct hlist_head *heads) { struct order_node *on = malloc(sizeof(*on)); on->val = val; on->idx = idx; int hash = (val < 0 ? -val : val) % size; - hlist_add_head(&on->node, DDDD); + hlist_add_head(&on->node, &heads[hash]); } ``` 計算雜湊值判斷要將節點加入到雜湊表的哪一位置 **`dfs`** ```c static struct TreeNode *dfs(int *preorder, int pre_low, int pre_high, int *inorder, int in_low, int in_high, struct hlist_head *in_heads, int size) { if (in_low > in_high || pre_low > pre_high) return NULL; struct TreeNode *tn = malloc(sizeof(*tn)); tn->val = preorder[pre_low]; int idx = find(preorder[pre_low], size, in_heads); tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder, in_low, idx - 1, in_heads, size); tn->right = dfs(preorder, pre_high - (in_high - idx - 1), pre_high, inorder, idx + 1, in_high, in_heads, size); return tn; } ``` 透過 `find` 函式找到目前 root 的 `idx` 值,分別將 preorder 和 inorder 切成 left, root 和 right 三部分,不斷的遞迴重複上述步驟直到 preorder 或 inorder 陣列長度為一 #### 測試程式 > commit [781cdee](https://github.com/max890808/Linux-homework/commit/781cdeec50a5173c5dd55f56d65186a1e93d7599) 修正版 > commit [30b0614](https://github.com/max890808/Linux-homework/commit/30b0614a2ecec78a043f1b732e18c38b768cc0db) ```diff - preOrderTraversal(tree, check_pre); + preOrderTraversal(check_tree, check_pre); - inOrderTraversal(tree, check_in); + inOrderTraversal(check_tree, check_in); ``` 透過 `generateRandomTree()` 隨機生成一顆樹 ```c struct TreeNode* generateRandomTree(int height, int *number) { if (height <= 0) { return NULL; } struct TreeNode* root = createNode(rand() % 100); number += 1; root->left = generateRandomTree(height - 1, number); root->right = generateRandomTree(height - 1, number); return root; } ``` `preOrderTraversal()` 和 `inOrderTraversal()` 負責找出前序和中序的陣列 ```c void preOrderTraversal(struct TreeNode* root, int* pre) { if (root != NULL) { *pre = root->val; preOrderTraversal(root->left, pre+1); preOrderTraversal(root->right, pre+1); } } void inOrderTraversal(struct TreeNode* root, int* in) { if (root != NULL) { inOrderTraversal(root->left, in+1); *in = root->val; inOrderTraversal(root->right, in+1); } } ``` 之後將前序和中序傳入 `buildTree()` 得到所要測試的樹,再透過 `preOrderTraversal()` 和 `inOrderTraversal()` 得到所要測試的樹的前序和中序陣列,最後比較一開始的前序和中序陣列是否相等就能完成測試 **TODO:** 1. 指出其中可改進之處並實作 2. 在 Linux 核心的 cgroups 相關原始程式碼,找出 pre-order walk 程式碼並探討 ### 測驗二 LRU(Least Recently Used Cache) 是一種快取的實做方式,概念是會儲存最近用過的內容,會透過 Hash Map 與 Double Linked List 來搭配實做,如果資料愈常被使用,資料會被擺在 List愈前方的位置,如果快取滿了,則會從 List 最末端元素開始移除。 因為 List 會從第一筆逐一查詢,所以查詢的時間複雜度為 O(N),為了降低查詢時間,所以才搭配HashMap,在 HashMap中設置 key,讓 key 的內容對應到 List 中的元素,就可以讓查詢時間降低到 O(1) #### 解釋程式碼的運作 **`LRUCache`** ```c typedef struct { int capacity; int count; struct list_head dhead; struct hlist_head hhead[]; } LRUCache; ``` `capacity:` 紀錄 cache 能存放的最大資料數 `count:` cache 當前的資料數 `dhead:` 資料的頭節點 `hhead[]:` 雜湊表 **`lRUCacheGet`** ```diff int lRUCacheGet(LRUCache *obj, int key) { int hash = key % obj->capacity; struct hlist_node *pos; hlist_for_each (pos, &obj->hhead[hash]) { - LRUNode *cache = list_entry(pos, LRUNode, HHHH); + LRUNode *cache = list_entry(pos, LRUNode, node); if (cache->key == key) { - list_move(IIII, &obj->dhead); + list_move(&cache->link, &obj->dhead); return cache->value; } } return -1; } ``` 計算 hash 值後,在對應的 `hhead[hash]` 所指向的鍵結串列進行搜索,若找到該節點回傳對應的值,並且透過 `list_move` 更新 `dhead` **`lRUCachePut`** ```diff void lRUCachePut(LRUCache *obj, int key, int value) { LRUNode *cache = NULL; int hash = key % obj->capacity; struct hlist_node *pos; hlist_for_each (pos, &obj->hhead[hash]) { - LRUNode *c = list_entry(pos, LRUNode, JJJJ); + LRUNode *c = list_entry(pos, LRUNode, node); if (c->key == key) { - list_move(KKKK, &obj->dhead); + list_move(&c->link, &obj->dhead); cache = c; } } if (!cache) { if (obj->count == obj->capacity) { cache = list_last_entry(&obj->dhead, LRUNode, link); list_move(&cache->link, &obj->dhead); hlist_del(&cache->node); hlist_add_head(&cache->node, &obj->hhead[hash]); } else { cache = malloc(sizeof(LRUNode)); hlist_add_head(&cache->node, &obj->hhead[hash]); list_add(&cache->link, &obj->dhead); obj->count++; } cache->key = key; } cache->value = value; } ``` 將新的資料放入快取當中,若資料已經在快取中,則使用 `list_move` 更新 `dhead`。若資料不在快取中且快取空間已滿,則會刪除最久沒被使用的資料,加入新的節點。若資料不在快取中且快取空間還沒滿,則配置新的記憶體空間並加入新節點。 **TODO:** * 撰寫完整的測試程式,指出其中可改進之處並實作 * 在 Linux 核心找出 LRU 相關程式碼並探討 ### 測驗三 #### 解釋程式碼的運作 **`hweight_long`** ```c #define __const_hweight8(w) \ ((unsigned int) ((!!((w) & (1ULL << 0))) + (!!((w) & (1ULL << 1))) + \ (!!((w) & (1ULL << 2))) + (!!((w) & (1ULL << 3))) + \ (!!((w) & (1ULL << 4))) + (!!((w) & (1ULL << 5))) + \ (!!((w) & (1ULL << 6))) + (!!((w) & (1ULL << 7))))) #define __const_hweight16(w) (__const_hweight8(w) + __const_hweight8((w) >> 8)) #define __const_hweight32(w) \ (__const_hweight16(w) + __const_hweight16((w) >> 16)) #define __const_hweight64(w) \ (__const_hweight32(w) + __const_hweight32((w) >> 32)) static inline unsigned long hweight_long(unsigned long w) { return __const_hweight64(w); } ``` 計算 `w` 當中有幾個位元被設定 **`__ffs`** ```diff static inline unsigned long __ffs(unsigned long word) { int num = 0; #if BITS_PER_LONG == 64 - if ((word & AAAA) == 0) { + if ((word & 0xffffffff) == 0) { num += 32; word >>= 32; } #endif if ((word & 0xffff) == 0) { num += 16; word >>= 16; } if ((word & 0xff) == 0) { num += 8; word >>= 8; } if ((word & 0xf) == 0) { num += 4; word >>= 4; } if ((word & 0x3) == 0) { num += 2; word >>= 2; } if ((word & 0x1) == 0) num += 1; return num; } ``` 找 `word` 第一個被設定的 bit 的位置 **`fns`** ```c static inline unsigned long fns(unsigned long word, unsigned int n) { while (word) { unsigned int bit = __ffs(word); if (n-- == 0) return bit; __clear_bit(bit, &word); } return BITS_PER_LONG; } ``` 透過 `__ffs` 函式得到第 N 個被設定的 bit 的位置 **`find_nth_bit`** ```c static inline unsigned long find_nth_bit(const unsigned long *addr, unsigned long size, unsigned long n) { if (n >= size) return size; if (small_const_nbits(size)) { unsigned long val = *addr & GENMASK(size - 1, 0); return val ? fns(val, n) : size; } return FIND_NTH_BIT(addr[idx], size, n); } ``` 透過 `small_const_nbits` 巨集判斷 `size` 的大小,若 `size` 小於 `BITS_PER_LONG` ,則運用 `fns` 函式找出第 N 個被設定的 bit 的位置,反之則會進入到 `FIND_NTH_BIT` 巨集 **`FIND_NTH_BIT`** ```diff #define FIND_NTH_BIT(FETCH, size, num) \ ({ \ unsigned long sz = (size), nr = (num), idx, w, tmp; \ \ for (idx = 0; (idx + 1) * BITS_PER_LONG <= sz; idx++) { \ if (idx * BITS_PER_LONG + nr >= sz) \ goto out; \ \ tmp = (FETCH); \ w = hweight_long(tmp); \ if (w > nr) \ goto found; \ \ nr -= w; \ } \ \ - if (sz CCCC BITS_PER_LONG) \ + if (sz % BITS_PER_LONG) \ tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz); \ found: \ sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz); \ out: \ sz; \ }) ``` 每次迭代都計算 `FETCH` 的 `BITS_PER_LONG` 長度有幾個位元被設定,透過 `found` 找出目標答案 **TODO:** * 在 Linux 核心原始程式碼找出 find_nth_bit 的應用案例,並予以解說和分析,需要涵蓋 CPU affinity 相關的原始程式碼。