# 2025q1 Homework2 (quiz1+2) contributed by < `liangchingyun` > ## 2025q1 第 1 週測驗題 ### 測驗 [`1`](https://hackmd.io/@sysprog/linux2025-quiz1#%E6%B8%AC%E9%A9%97-1) #### 延伸問題 1 > 解釋上方程式碼運作原理 --- 巨集定義 ```c #define my_assert(test, message) \ do { \ if (!(test)) \ return message; \ } while (0) #define my_run_test(test) \ do { \ char *message = test(); \ tests_run++; \ if (message) \ return message; \ } while (0) ``` * `my_assert()` : 如果測試條件 `test` 為 `false`,則返回 `message`。 * `my_run_test()` : 若測試失敗,則直接回傳錯誤訊息。 --- 重設鏈結串列 ```c static list_t *list_reset(void) { for (size_t i = 0; i < N; i++) { items[i].value = i; items[i].next = NULL; } l.head = NULL; return &l; } ``` * 將 `items[i]` 的 `value` 設為 `i`,並將 `next` 設為 `NULL`。 * `l.head = NULL`; 清空鏈結串列。 --- 測試函式 `test_list()` 包含三種測試情境 1. 在開頭插入 ```c /* Test inserting at the beginning */ list_reset(); my_assert(list_size(&l) == 0, "Initial list size is expected to be zero."); for (size_t i = 0; i < N; i++) list_insert_before(&l, l.head, &items[i]); my_assert(list_size(&l) == N, "Final list size should be N"); size_t k = N - 1; list_item_t *cur = l.head; while (cur) { my_assert(cur->value == k, "Unexpected list item value"); k--; cur = cur->next; } ``` * `l.head` 表示插入在最前面。 * 逐個檢查 `cur->value` 是否符合預期的逆序 (從 `N-1` 到 `0`)。 2. 在結尾插入 ```c /* Test inserting at the end */ list_reset(); my_assert(list_size(&l) == 0, "Initial list size is expected to be zero."); for (size_t i = 0; i < N; i++) list_insert_before(&l, NULL, &items[i]); my_assert(list_size(&l) == N, "Final list size should be N"); k = 0; cur = l.head; while (cur) { my_assert(cur->value == k, "Unexpected list item value"); k++; cur = cur->next; } ``` * `NULL` 表示插入在尾端 * 逐個檢查 `cur->value` 是否符合預期的正序 (從 `0` 到 `N-1`)。 3. 重設串列並插入 ```c /* Reset the list and insert elements in order (i.e. at the end) */ list_reset(); my_assert(list_size(&l) == 0, "Initial list size is expected to be zero."); for (size_t i = 0; i < N; i++) list_insert_before(&l, NULL, &items[i]); my_assert(list_size(&l) == N, "list size should be N"); ``` 這部分與 2. 類似,但主要是確認插入結果仍然正確。 --- 測試執行 ```c int tests_run = 0; static char *test_suite(void) { my_run_test(test_list); return NULL; } ``` `test_suite()` 執行 `test_list()`,並計算測試數量。 --- 主函式 ```c int main(void) { printf("---=[ List tests\n"); char *result = test_suite(); if (result) printf("ERROR: %s\n", result); else printf("ALL TESTS PASSED\n"); printf("Tests run: %d\n", tests_run); return !!result; } ``` * `test_suite()` 若發生錯誤,則 `result` 為錯誤訊息。 * 測試通過則顯示 `ALL TESTS PASSED`,否則輸出 `ERROR` 訊息。 --- `list_insert_before` 函式: ```c static inline void list_insert_before(list_t *l, list_item_t *before, list_item_t *item); { list_item_t **p; for (p = l->head; *p != before; p = &(*p)->next) ; *p = item; item->next = before; } ``` 以 `before = B` 為例: 1. `p` 初始指向鏈結串列的頭指標。 ```graphviz digraph G { rankdir=LR; node [shape=record]; head [label="head"]; A [label="A"]; B [label="B"]; C [label="C"]; D [label="D"]; p [label="p"]; head -> A; A -> B; B -> C; C -> D; p -> head; } ``` 2. `for` 迴圈會遍歷鏈結串列,直到找到 `before` 節點。 ```graphviz digraph G { rankdir=LR; node [shape=record]; head [label="head"]; A [label="A"]; B [label="B"]; C [label="C"]; D [label="D"]; p [label="p"]; head -> A; A -> B; B -> C; C -> D; p -> A[color=red]; } ``` ```graphviz digraph G { rankdir=LR; node [shape=record]; head [label="head"]; A [label="A"]; B [label="B"]; C [label="C"]; D [label="D"]; p [label="p"]; head -> A; A -> B; B -> C; C -> D; p -> B[color=red]; } ``` 3. `*p = item;` 將前一個節點的 `next` 指向 `item`,完成插入。 ```graphviz digraph G { rankdir=LR; node [shape=record]; head [label="head"]; X [label="X"]; A [label="A"]; B [label="B"]; C [label="C"]; D [label="D"]; p [label="p"]; head -> A; A -> B[style=dashed]; B -> C; C -> D; p -> B; A -> X[color=red]; } ``` 4. `item->next = before;` 讓 `item` 指向 `before`,確保鏈結不會斷開。 ```graphviz digraph G { rankdir=LR; node [shape=record]; head [label="head"]; X [label="X"]; A [label="A"]; B [label="B"]; C [label="C"]; D [label="D"]; p [label="p"]; head -> A; B -> C; C -> D; p -> B; A -> X; X -> B[color=red]; } ``` #### 延伸問題 2 > 在現有的程式碼基礎上,撰寫合併排序操作 ### 測驗 [`2`](https://hackmd.io/@sysprog/linux2025-quiz1#%E6%B8%AC%E9%A9%97-2) #### 延伸問題 1 > 解釋上方程式碼運作的原理,並嘗試補完程式碼,使其可獨立執行,應提供相關的測試程式碼 這段程式碼的功能是從二元搜尋樹 (BST) 中移除特定的節點,這個 BST 用於管理記憶體分配中的「空閒區塊 (free blocks)」。 --- 結構體 ```c typedef struct block { size_t size; struct block *l, *r; } block_t; ``` `block_t` 代表一個記憶體區塊,具有: * `size`:區塊大小 * `l`:指向左子節點(較小的區塊) * `r`:指向右子節點(較大的區塊) --- `remove_free_tree` 用來從 BST 中移除 `target` 節點,遵循標準的 BST 刪除操作。 1. 找到 `target` 節點 ```c /* Locate the pointer to the target node in the tree. */ block_t **node_ptr = find_free_tree(root, target); ``` * `find_free_tree()` 會在 BST 中找到 `target` 並返回它的指標。 * `node_ptr` 是指向 `target` 指標的指標,方便後續修改樹的結構。 2. 判斷 `target` 節點的子節點情況 * Case 1: `target` 有兩個子節點(同時擁有左、右子樹) ```c /* If the target node has two children, we need to find a replacement. */ if ((*node_ptr)->l && (*node_ptr)->r) { /* Find the in-order predecessor: * This is the rightmost node in the left subtree. */ block_t **pred_ptr = &(*node_ptr)->l; while (EEEE) pred_ptr = FFFF; /* Verify the found predecessor using a helper function (for debugging). */ block_t *expected_pred = find_predecessor_free_tree(root, *node_ptr); assert(expected_pred == *pred_ptr); /* If the predecessor is the immediate left child. */ if (*pred_ptr == (*node_ptr)->l) { block_t *old_right = (*node_ptr)->r; *node_ptr = *pred_ptr; /* Replace target with its left child. */ (*node_ptr)->r = old_right; /* Attach the original right subtree. */ assert(*node_ptr != (*node_ptr)->l); assert(*node_ptr != (*node_ptr)->r); } else { /* The predecessor is deeper in the left subtree. */ block_t *old_left = (*node_ptr)->l; block_t *old_right = (*node_ptr)->r; block_t *pred_node = *pred_ptr; /* Remove the predecessor from its original location. */ remove_free_tree(&old_left, *pred_ptr); /* Replace the target node with the predecessor. */ *node_ptr = pred_node; (*node_ptr)->l = old_left; (*node_ptr)->r = old_right; assert(*node_ptr != (*node_ptr)->l); assert(*node_ptr != (*node_ptr)->r); } } ``` 1. 找到前驅節點 `predecessor` ,就是 `target` 左子樹中最大(最右)的節點 2. 使用 `assert()` 來做驗證 `find_predecessor_free_tree()` 的,確保 `pred_ptr` 是正確的 3. 用 `pred_ptr` 替換 `target` * 如果 `pred_ptr` 就是 `target->l` * 用前驅節點取代 `target` * 接回原本的右子樹 * 如果 `pred_ptr` 在 `target->l` 更深的位置 * 移除前驅節點 * 用前驅節點取代 `target` * Case 2: `target` 只有一個子節點 ```c block_t *child = ((*node_ptr)->l) ? (*node_ptr)->l : (*node_ptr)->r; *node_ptr = child; ``` 直接用 `target` 唯一的子節點取代它。 * Case 3: `target` 沒有子節點 ```c *node_ptr = NULL; ``` 直接刪除該節點。 3. 清除 `target` 的指標 ```c /* Clear the removed node's child pointers to avoid dangling references. */ target->l = NULL; target->r = NULL; ``` 防止 `target` 仍然指向舊的子節點,避免潛在的 dangling pointer 問題。 範例: ``` [50] / \ [30] [70] / \ / \ [20] [40][60] [80] ``` Case 1:`20` 沒有子節點,直接刪除。 ``` [50] / \ [30] [70] \ / \ [40] [60] [80] ``` Case 2:`30` 只有一個右子節點 (`40`),讓 `40` 直接取代 `30`。 ``` [50] / \ [40] [70] / \ [60] [80] ``` Case 3:`50` 有左右子節點,找前驅節點 = `40`(左子樹的最大值)。用 `40` 替換 `50`,然後刪除 `40` ``` [40] \ [70] / \ [60] [80] ``` #### 延伸問題 2 > 參閱 [memory-allocators](https://github.com/mtrebi/memory-allocators),針對你補完 (及後續改進) 的記憶體配置器,進行效能評比,並解讀其效能表現 ### 測驗 [`3`](https://hackmd.io/@sysprog/linux2025-quiz1?stext=8296%3A5%3A0%3A1742717039%3AaNQwCv) #### 延伸問題 1 > 解釋上述程式碼運作原理 #### 延伸問題 2 > 研讀〈[A comparative study of linked list sorting algorithms](https://dl.acm.org/doi/pdf/10.1145/225890.225893)〉,分析針對雙向鏈結串列的排序演算法考量,並運用 Linux 核心風格的 List API 予以實作 ## 2025q1 第 2 週測驗題 ### 測驗 [`1`](https://hackmd.io/@sysprog/linux2025-quiz2?stext=497%3A5%3A0%3A1742053087%3AvGUhXy) Stable Sorting:在排序過程中,若兩個元素的值相同,它們的原始相對順序不會改變。 ```c { struct list_head list_less, list_greater; struct listitem *pivot; struct listitem *item = NULL, *is = NULL; if (list_empty(head) || list_is_singular(head)) return; INIT_LIST_HEAD(&list_less); INIT_LIST_HEAD(&list_greater); pivot = list_first_entry(head, struct listitem, list); list_del(&pivot->list); list_for_each_entry_safe (item, is, head, list) { if (cmpint(&item->i, &pivot->i) < 0) list_move_tail(&item->list, &list_less); else list_move_tail(&item->list, &list_greater); } list_quicksort(&list_less); list_quicksort(&list_greater); list_add(&pivot->list, head); list_splice(&list_less, head); list_splice_tail(&list_greater, head); } ``` 此程式碼的核心概念是 「分割」+「遞迴排序」+「合併」,假設我們有以下的鏈結串列: ```c [ 5 ] -> [ 3 ] -> [ 8 ] -> [ 2 ] -> [ 7 ] ``` 1. 選擇基準點(Pivot) * `list_first_entry(head, struct listitem, list)` 取得 `head` 串列中的第一個節點作為基準點 `pivot`。 ```c pivot = 5 ``` * `list_del(&pivot->list);` 從 `head` 中移除 `pivot`。 ```c [ 3, 8, 2, 7 ] ``` 2. 走訪串列並分割到 `list_less` 和 `list_greater` 遍歷 head 中的所有元素: * 如果 `item` 的數值 小於 pivot,則將 `item` 移動到 `list_less`。 ```c list_less = [ 3, 2 ] ``` * 否則,將 `item` 移動到 `list_greater`。 ```c list_greater = [ 8, 7 ] ``` 3. 使用 `list_quicksort` 函式,遞迴對 `list_less` 和 `list_greater` 進行排序 * 遞迴排序 `list_less` ```c Pivot = 3 list_less = [2] list_greater = [] ``` * 遞迴排序 `list_greater` ```c Pivot = 8 list_less = [7] list_greater = [] ``` 4. 合併排序後的結果 * 將 `pivot` 放回 `head`: `list_add(&pivot->list, head);` 將 `pivot` 放到 `head` 串列的開頭 * 合併 `list_less`(已排序)到 `head` * 合併 `list_greater`(已排序)到 `head` 的尾端 ```c [ 2 ] -> [ 3 ] -> [ 5 ] -> [ 7 ] -> [ 8 ] ``` #### 延伸問題 1 > 解釋上方程式碼運作原理,改進 `random_shuffle_array` 也探討 `list_for_each_entry_safe` 建構的迴圈中,若將 `list_move_tail` 換為 `list_move`,為何會無法滿足 stable sorting #### 延伸問題 2 > 將程式碼整合進 [lab0](https://hackmd.io/@sysprog/linux2025-lab0) 提及的 `qtest`,並探討環狀雙向鏈結串列的排序效能,並善用 [perf](https://hackmd.io/@sysprog/linux-perf) 一類的效能分析工具,探討包含 Linux 核心 [list_sort](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 在內排序實作的效能表現並充分解釋 ### 測驗 [`2`](https://hackmd.io/@sysprog/linux2025-quiz2?stext=4504%3A5%3A0%3A1742130777%3AeEZNJj) #### 理論分析 > [參考](http://hackmd.io/l4--gpsZQBiEI5LKS1lDvg?view#%E6%AA%A2%E8%A8%8E%E7%AC%AC%E4%BA%8C%E9%80%B1%E6%B8%AC%E9%A9%97%E9%A1%8C) 二進位系統: $(b_nb_{n-1}\dots b_1b_0)_2^2 = (b_n\times 2^n + b_{n-1}\times 2^{n-1}+\dots b_ 1\times2^{1} +b_0\times2^0)^2$ 從最高位元開始,依序決定 $b_i$ 是 1 或 0 範例: $(63)_{10} = (011111)_2 = (b_2\times 2^2 + b_{1}\times 2^{1}+b_0\times2^0)^2$ $(4)^2 < 63$,故 $b_2 = 1$ $(4 + 2)^2 < 63$,故 $b_1 = 1$ $(4 + 2 + 1)^2 < 63$,故 $b_0 = 1$ --- #### 程式實作 - clz `clz2()`:計算一個 32 位元整數的 leading zeros,即數字前面有多少個 0。 ```c static const int mask[] = {0, 8, 12, 14}; static const int magic[] = {2, 1, 0, 0}; unsigned clz2(uint32_t x, int c) { if (!x && !c) return 32; uint32_t upper = (x >> (16 >> c)); uint32_t lower = (x & (0xFFFF >> mask[c])); if (c == 3) return upper ? magic[upper] : 2 + magic[lower]; return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + 1); } ``` 範例分析: ```c clz2(0x000003F0, 0) ``` **第一次遞迴(c == 0)** ```c upper = (x >> (16 >> 0)); // x >> 16 lower = (x & (0xFFFF >> mask[0])); // x & 0xFFFF ``` * `upper = 0x0000`,取高 16-bit * `lower = 0x03F0`,取低 16-bit * `upper == 0`,所以走: ```c (16 >> 0) + clz2(lower, 1) // 16 + clz2(0x03F0, 1) ``` **第二次遞迴(c == 1)** ```c upper = (x >> (16 >> 1)); // x >> 8 lower = (x & (0xFFFF >> mask[1])); // x & (0xFFFF >> 8) ``` * `upper = 0x0003`,取高 24-bit * `lower = 0x00F0`,取低 8-bit * `upper != 0`,所以走: ```c clz2(upper, 2) // clz2(0x0003, 2) ``` **第三次遞迴(c == 2)** ```c upper = (x >> (16 >> 2)); // x >> 4 lower = (x & (0xFFFF >> mask[2])); // x & (0xFFFF >> 12) ``` * `upper = 0x0000`,取高 28-bit * `lower = 0x0003`,取低 4-bit * `upper == 0`,所以: ```c (16 >> 2) + clz2(lower, 3) // 4 + clz2(0x0003, 3) ``` **第四次遞迴(c == 3)** ```c upper = (x >> (16 >> 3)); // x >> 2 lower = (x & (0xFFFF >> mask[3])); // x & (0xFFFF >> 14) ``` * `upper = 0x0000`,取高 30-bit * `lower = 0x0003`,取低 2-bit * `upper == 0`,所以: ```c return 2 + magic[0x0003] // magic[3] = 0 ``` 回推回去: ```c clz2(0x000003F0, 0) = 16 + 4 + 2 = 22 ``` 結果是 22 個前導零。 --- `clz64()` 計算 64 位元 leading zeros * 如果 x 的高 32 位元不為 0,就計算高 32 位的 `clz32()`。 * 若 x 的高 32 位元為 0,則計算低 32 位元的 `clz32()`,並加上 32。 ```c #define clz32(x) clz2(x, 0) static inline int clz64(uint64_t x) { /* If the high 32 bits are nonzero, count within them. * Otherwise, count in the low 32 bits and add 32. */ return (x >> 32) ? clz32((uint32_t) (x >> 32)) : clz32((uint32_t) x) + 32; } ``` --- #### 程式實作 - sqrti `sqrti()`:整數開平方根 參考 [`<rota1001>`](https://hackmd.io/@rota1001/linux2025-homework2#%E6%95%B4%E6%95%B8%E9%96%8B%E5%B9%B3%E6%96%B9%E6%A0%B9) 的作法,以改進程式的觀點來理解程式碼。 ##### 初版程式 1. 平方根暫存 定義 $$p_i=\sum^{\frac{\text{shift}}{2}}_{j=i}a_j$$ 其中 $a_j$ 是 $2^j$ 或 $0$,取決於第 $j$ 個位元是不是 $1$。 假設 $\text{shift} = 4$,則 \begin{align} &p_2 = a_2 \\ &p_1 = a_1 + a_2 = p_2 + a_1 \\ &p_0 = a_0 + a_1 + a_2 = p_1 + a_0 \end{align} 可推得一般式 $$p_i = p_{i+1} + a_i$$ 因此 $p$ 的更新式可寫為 $$p_i = p_{i+1} + 2^i$$ 對應程式碼為 `p += (1 << i)` 2. 檢查條件 每次更新都檢查 $N\geq p_i^2$ 有沒有成立,令 $$x_{i+1} = N - p_{i+1}^2$$ 又 $$p_i^2 = (p_{i+1} + 2^i)^2 = p_{i+1}^2 + 2^{i+1}\cdot p_{i+1} + 2^{2i}$$ 則可以改成檢查: $$x_{i+1} + p_{i+1}^2\geq p_{i+1}^2 + 2^{i+1}\cdot p_{i+1} + 2^{2i}$$ 左右消去 $p_{i+1}^2$ 得 $$x_{i+1}\geq 2^{i+1}\cdot p_{i+1} + 2^{2i}$$ 對應程式碼為 `x >= b`,其中 `b = (p << (i + 1)) + (1 << (2 * i))` 3. 待處理數的更新 \begin{align} x_{i} &= N - p_{i}^2 \\ &= N - p_{i+1}^2 - 2^{i+1}\cdot p_{i+1} - 2^{2i} \\ &= x_{i+1} - (2^{i+1}\cdot p_{i+1} + 2^{2i}) \end{align} 對應程式碼為 `x -= b` 此部分的完整程式碼為: ```c uint64_t sqrti_simple(uint64_t x) { uint64_t p = 0; if (x <= 1) return x; int total_bits = 64; int shift = (total_bits - 1 - clz64(x)) & ~1; for(int i = (shift / 2); i >= 0; i--) { uint64_t b = (p << (i + 1)) + (1 << (2 * i)); if (x >= b) { x -= b; p += (1 << i); } } return p; } ``` ##### 改進位移運算 假設新的變數 $$y_i = p_i \times 2^i$$ 1. 平方根暫存 $$p_i = p_{i+1} + 2^i$$ 同乘 $2^i$ $$2^ip_i=2^ip_{i+1}+2^{2i}$$ 則可以改寫成 $$y_i=2^{-1}y_{i+1}+2^{2i}$$ 對應程式碼為 `y = (y >> 1) + (1 << (2 * i))` 2. 檢查條件 $$x_{i+1}\geq 2^{i+1}\cdot p_{i+1} + 2^{2i} = y_{i+1} + 2^{2i}$$ 對應程式碼為 `x >= b`,其中 `b = y + (1 << (2 * i))` 此部分的完整程式碼為: ```c uint64_t sqrti_simple(uint64_t x) { uint64_t y = 0; if (x <= 1) return x; int total_bits = 64; int shift = (total_bits - 1 - clz64(x)) & ~1; for(int i = (shift / 2); i >= 0; i--) { uint64_t b = y + (1 << (2 * i)); y >>= 1; if (x >= b) { x -= b; y += (1 << (2 * i)); } } return y; } ``` ##### 改寫成原題目的程式碼 將 `(1 << (2 * i))` 替換成 `m` ```c uint64_t sqrti(uint64_t x) { uint64_t m, y = 0; if (x <= 1) return x; int total_bits = 64; int shift = (total_bits - 1 - clz64(x)) & ~1; m = 1ULL << shift; while(m) { uint64_t b = y + m; y >>= 1; if (x >= b) { x -= b; y += m; } m >>= 2; } return y; } ``` * `clz64(x)` :計算 `x` 前導零的數量 * `total_bits - 1 - clz64(x)` :取得 `x` 最高有效位的索引 * `& ~1` :將數值的最右邊一位 (最低位) 清零,確保結果為偶數 * `m = 1ULL << shift` :將 `m` 的值令為 $2^{\text{shift}}$ * `1ULL`:代表 1,且是 `uint64_t` 類型 (unsigned long long) * `shift`:是一個偶數,確保 `m` 是 4 的次方數(即 1, 4, 16, ...) #### 延伸問題 1 >解釋上述程式碼,並探討擴充為 $\lceil \sqrt{x}) \rceil$ (向上取整數) 實作,該如何調整並保持只用整數加減及位元操作 第一次實作: ```c uint64_t ceil_sqrti(uint64_t x) { uint64_t m, y = 0; if (x <= 1) return x; int total_bits = 64; int shift = (total_bits - 1 - clz64(x)) & ~1; m = 1ULL << shift; while (m) { uint64_t b = y + m; y >>= 1; if (x >= b) { x -= b; y += m; } m >>= 2; if (y*y != x) y += 1; } return y; } ``` 上面的程式碼無法正確處理 `ceil_sqrti(64)` 的狀況 ``` ceil_sqrti(64) = 9 ``` 原因是 `x` 已經不是最初的值,因此使用 `y*y != x` 來判斷會發生錯誤。作以下改變即可得到正確答案: ```diff uint64_t ceil_sqrti(uint64_t x) { + uint64_t origin = x; ... - if (y*y != x) + if (y*y != origin) y += 1; } return y; } ``` ``` ceil_sqrti(64) = 8 ``` 然而,原本的 sqrti 「沒有」用到乘法,因此改寫過的程式碼也「不該用乘法」。 因為 $x$ 的更新式為 $x_{i} = N - p_{i}^2$,故若 $x$ 為完全平方數,最終值會是 $0$ 。判斷式則改為: ```diff uint64_t ceil_sqrti(uint64_t x) { ... - if (y*y != origin) + if (x != 0) y += 1; } return y; } ``` 如此一來即可避免使用成本較高的乘法計算。 #### 延伸問題 2 >參照[計算機結構測驗題 C](https://hackmd.io/@sysprog/arch2024-quiz3-sol#Problem-C) 及其[注釋](https://hackmd.io/@sysprog/HJKRbSAVJl#Problem-C30),以 C 語言 (搭配 GNU extension) 撰寫不依賴除法指令的 `sqrtf`,確保符合 IEEE 754 規格。對照 [sqrtf, rsqrt, reciprocal implementations in Arm assembly](https://github.com/picolibc/picolibc/issues/956) #### 延伸問題 3 >參照[從 √2 的存在談開平方根](從 √2 的存在談開平方根的快速運算)的快速運算提到的「藉由查表的平方根運算」,以 C 語言實作,並比較不同手法的整數開平方根效能。一旦發現效能改進的機會,可準備提交改進 `int_sqrt` 程式碼到 Linux 核心 ### 測驗 [`3`](https://hackmd.io/@sysprog/linux2025-quiz2?stext=7569%3A5%3A0%3A1742185627%3AeBp3ey) > [Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable) 實作 [Two Sum](https://leetcode.com/problems/two-sum/) 使用 [hash table](https://en.wikipedia.org/wiki/Hash_table) (以下簡稱 HT) 記錄缺少的那一個值 (即 `target - nums[i]`) 和對應的索引。考慮以下案例: ```c nums = [2, 11, 7, 15] target = 9 ``` | Index `i` | `nums[i]` | `target - nums[i]` | HT 是否有 `nums[i]` ? | 操作 | HT 狀態 | | --------- | --------- | ------------------ | --------------------- | -------------------------- | ------------------ | | 0 | 2 | 9-2=7 | No | 加入 `HT[7] = 0` | `{ 7:0 }` | | 1 | 11 | 9-11=-2 | No | 加入 `HT[-2] = 1` | ` { 7: 0, -2: 1 }` | | 2 | 7 | 9-7=2 | Yes | 回傳 `[2, HT[7]] = [2, 0]` | `{ 7: 0, -2: 1 }` | 參考 Linux 原始碼中 [type.h](https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/types.h#L194-L204) : ```c struct list_head { struct list_head *next, *prev; }; struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; }; ``` * `hlist_head` 只有 `first`,節省空間 * `hlist_node` 使用 `pprev`(指向指標的指標) #### 檢索程式碼 1. `find_key()`:在 hash table 中查找 key ```c static struct hash_key *find_key(map_t *map, int key) { struct hlist_head *head = &(map->ht)[hash(key, map->bits)]; // 遍歷 bucket 中的 hlist 鏈結串列 for (struct hlist_node *p = head->first; p; p = p->next) { struct hash_key *kn = container_of(p, struct hash_key, node); // 比對 key,找到則返回 kn if (kn->key == key) return kn; } return NULL; } ``` * 使用 `hash()` 計算 key 應該存放的 bucket(即 `hlist_head`)。 * `map->ht` 是 哈希表的 bucket 陣列,透過 `hash()` 計算索引,取得對應的 `hlist_head *head`。 * `p` 只是 `hlist_node`,但 `hash_key` 是: ```c struct hash_key { int key; void *data; struct hlist_node node; }; ``` 2. `map_get()`:查找 key 並返回 data ```c void *map_get(map_t *map, int key) { struct hash_key *kn = find_key(map, key); //利用 find_key() 找到 key return kn ? kn->data : NULL; // 如果 kn 存在,返回 kn->data,否則返回 NULL } ``` #### 新增操作 `map_add()`:將 `(key, data)` 新增至哈希表 (`map_t`) 中 ```c void map_add(map_t *map, int key, void *data) { // 檢查 key 是否已存在(避免重複) struct hash_key *kn = find_key(map, key); if (kn) return; // 分配記憶體 儲存新的 (key, data) kn = malloc(sizeof(struct hash_key)); kn->key = key, kn->data = data; struct hlist_head *h = &map->ht[hash(key, map->bits)]; // 取得對應的 bucket struct hlist_node *n = &kn->node, *first = h->first; // 新節點 // 插入新節點 n->next = first; if (first) first->pprev = &n->next; h->first = n; n->pprev = &h->first; } ``` * `h`: key 應該存放的 bucket * `n`: 新節點 (`hlist_node`) * `first`: 當前 bucket (`hlist_head`) 的第一個節點 Graphviz 練習: > 新增 test.dot > `$ dot -Tpng test.dot -o output.png` (To be done...) #### 釋放記憶體的操作 ```c void map_deinit(map_t *map) { if (!map) return; for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) { struct hlist_head *head = &map->ht[i]; for (struct hlist_node *p = head->first; p;) { struct hash_key *kn = container_of(p, struct hash_key, node); struct hlist_node *n = p; p = p->next; if (!n->pprev) /* unhashed */ goto bail; struct hlist_node *next = n->next, **pprev = n->pprev; *pprev = next; if (next) next->pprev = pprev; n->next = NULL, n->pprev = NULL; bail: free(kn->data); free(kn); } } free(map); } ``` #### 主體程式碼 ```c int *twoSum(int *nums, int numsSize, int target, int *returnSize) { map_t *map = map_init(10); *returnSize = 0; int *ret = malloc(sizeof(int) * 2); // 如果 malloc 失敗,則跳轉到 bail 進行清理 if (!ret) goto bail; // 走訪 nums ,查找匹配的數 for (int i = 0; i < numsSize; i++) { int *p = map_get(map, target - nums[i]); if (p) { /* found, 其索引存於 p */ ret[0] = i, ret[1] = *p; *returnSize = 2; break; } // 若 target - nums[i] 不在哈希表中,則存入 nums[i] p = malloc(sizeof(int)); *p = i; map_add(map, nums[i], p); } bail: map_deinit(map); // 清理哈希表,釋放記憶體 return ret; } ``` * `10`:哈希表的 bit 數,意即 bucket 大小為 $2^{10} = 1024$ * `ret`:用來存放找到的索引值,最多只有兩個數,所以分配 2 個整數大小的記憶體 * `ret[0] = i`:目前數字的索引 * `ret[1] = *p`:匹配數的索引,來自哈希表 #### 延伸問題 1 > 解釋上述程式碼運作原理,應提供測試程式碼 針對 [Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable),提供更多圖例並揣摩 Linux 核心開發者 #### 延伸問題 2 > 進行《[The Art of Computer Programming](https://www-cs-faculty.stanford.edu/~knuth/taocp.html)》(vol 3) 一書section 6.4 的 exercise 8,也就是證明 Theorem S 注意: Linux 核心內部大量使用 hash table,但並非每處的雜湊函數都滿足減少碰撞及避免 clustering 現象的原則,也就是存在若干改進空間 #### 延伸問題 3 > 探討 Linux 核心中使用 hash table 的應用案例,至少二項,應當列出原始程式碼並分析,斟酌搭配 git log 以得知 Linux 核心開發者變更程式碼的考量因素