# 2025q1 Homework2 (quiz1+2) contributed by <```Charlie-Tsai1123```> ## 第一周測驗題 - [測驗1](https://hackmd.io/@sysprog/linux2025-quiz1#%E6%B8%AC%E9%A9%97-1) ### ```list_insert_before``` ```cpp 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; } ``` 下圖的黑色部份是題目所用的 ```linked list``` ,紅色是 ```list_insert_before``` 傳入的參數,藍色則是指標的指標,這裡用箭頭來表示指標,所以由圖可以知道 ```p``` 初始話的位子在指向 ```head``` 指標的位子 ```cpp p = &l->head ``` ![quiz1_1 (1)](https://hackmd.io/_uploads/Bk2ySMQhJl.png) 由於要從 ```linked list``` 中找到 ```before``` 的位子 ```p``` 要移動到指向下一個指標的位子,由 ```*p``` 先找到現在指向的指標,再取下一個指標,最後藉由 ```&``` 取指向下一個指標的位子 ```cpp p = &(*p)->next ``` 由下圖可知當找到 ```before``` 時( ```*p = before```),需要變動指標,可以直接改動 ```p``` 所指向的指標,就可以直接變動 ```linked list``` 了,最後新元素的 ```next``` 也要指向 ```before``` 所在位子 ```cpp *p = item; item->next = before; ``` ![quiz1_1 (2)](https://hackmd.io/_uploads/HJ8xvz73yg.png) ### 解釋測試程式碼 ### 合併排序 ## 第一周測驗題 - [測驗2](https://hackmd.io/@sysprog/linux2025-quiz1#%E6%B8%AC%E9%A9%97-2) ### 解釋程式碼運作原理 程式在執行 binary search tree (BST) 的 remove ,以下為 BST 的架構,每個節點會有指向左子樹的指標跟指向右子樹的指標,而 ```size``` 是記憶體區塊的大小 ```cpp typedef struct block { size_t size; struct block *l, *r; } block_t; ``` ```find_free_tree``` 是從 BST 中找到要被釋放的記憶體區塊位子 (```target```) ```cpp block_t **find_free_tree(block_t **root, block_t *target); ``` ```find_predecessor_free_tree``` 則是找到 ```node``` 左子樹中的最大值 ```cpp block_t *find_predecessor_free_tree(block_t **root, block_t *node); ``` 在 ```remove_free_tree``` 中,把操作分成了三個 case : * case 1 : 被 remove 的節點有左子樹跟右子樹 * 左子樹沒有右子樹 (左子樹的 root 是最大的點) * 左子樹有右子樹 (左子樹有比左子樹 root 更大的點) ```cpp if ((*node_ptr)->l && (*node_ptr)->r) ``` * case 2 : 被 remove 的節點只有左子樹或右子樹 ```cpp else if ((*node_ptr)->l || (*node_ptr)->r) ``` * case 3 : 被 remove 的節點沒有左子樹或右子樹 #### case 1 : If the target node has two children, we need to find a replacement. 在 ```remove``` 節點之前要找到代替它位子的節點,那就是左子樹中最大的點 ==step 1 : 求左子樹最大的點== 因為最大的節點會在右子樹中,所以從左子樹不斷往右找,找到底就是左子樹中最大的節點了( step 2 兩張圖中藍色的節點就是要找的,而紅色是要移除的節點 ) ```cpp block_t **pred_ptr = &(*node_ptr)->l; while ((*pred_ptr)->r) pred_ptr = &(*pred_ptr)->r; /* 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); ``` ==step 2 : 移除節點並加上代替位子的節點== 左子樹的 ```root``` 是最大的節點 : 1. 紀錄欲刪除節點右子樹的位子 ```old_right``` 2. 將左子樹最大的節點 ```pred_ptr``` 移過去 3. 將右子樹 ```old_right``` 加到節點 ```*node_ptr``` 中 ```cpp /* 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); } ``` ![q1_2_1 (1)](https://hackmd.io/_uploads/rJ5DJ8Ehyx.png) 左子樹更深的地方才是最大的節點: 1. 紀錄左節點 ```old_left``` , 右節點 ```old_right``` , 左子樹最大節點 ```pred_node``` 2. 用 ```remove_free_tree``` 移除左子樹最大節點 ```pred_ptr``` (做完後可能變成 NULL) 3. 將左子樹最大節點 ```pred_node``` 代替欲移除的節點 4. 將左子樹 ```old_left``` , 右子樹 ```old_right``` 接到節點 ```*node_ptr``` 中 >[!Note]為什麼還要再存一次左子樹最大節點明明已經有 ```pred_ptr``` >因為經過 ```remove_free_tree(&old_left, *pred_ptr)``` 後,可能會變成 ```NULL``` ```cpp /* 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); ``` ![q1_2_2.drawio](https://hackmd.io/_uploads/SyalVUEnye.png) #### case 2 : If the target node has one child (or none), simply splice it out. 刪除的點是紅色的點,如果只有一個子樹,代替的點就是子樹的 root (藍色點) <div style="display: flex;"> <img src="https://hackmd.io/_uploads/Hy19i0SnJx.png" style="width: 45%; margin-right: 10px;" /> <img src="https://hackmd.io/_uploads/BJO6iCH3kg.png" style="width: 45%;" /> </div> #### case 3 : No children: remove the node. <div style="display: flex;"> <img src="https://hackmd.io/_uploads/SJSO60B3Jx.png" style="width: 45%; margin-right: 10px;" /> <img src="https://hackmd.io/_uploads/ryr56ArnJx.png" style="width: 45%;" /> </div> ### 補完 ```find_free_tree``` 及 ```find_predecessor_free_tree``` 並測試 ### 效能評比 ## 第一周測驗題 - [測驗3](https://hackmd.io/@sysprog/linux2025-quiz1#%E6%B8%AC%E9%A9%97-3) ### 解釋程式碼運作原理 #### ```rebuild_list_link``` 黑色是原本的 ```linked list``` 可以發現一開始是 ```singly linked list``` ,那 ```rebuild_list_link``` 是將這種結構變成 ```circular doubly linked list``` * 8 ~ 12 行是完成 ```circular doubly linked list``` 中的藍色部份, ```prev``` 指標指向前一個 node * 13 ~ 14 行完成 ```circular doubly linked list``` 中 cirsular 的部份,也就是紅色部份 ```cpp= static void rebuild_list_link(struct list_head *head) { if (!head) return; struct list_head *node, *prev; prev = head; node = head->next; while (node) { node->prev = prev; prev = node; node = node->next; } prev->next = head; head->prev = prev; } ``` ```graphviz digraph G { nodesep=0.8 node [shape=rect]; { rankdir=LR; rank=same; "head"; "10"; "20"; "30"; "40"; "50"; "NULL";} "NULL" [style=dotted]; "head" -> "10" [constraint=false]; "10" -> "head" [constraint=false, color = blue] "10" -> "20" [constraint=false]; "20" -> "10" [constraint=false, color = blue] "20" -> "30" [constraint=false]; "30" -> "20" [constraint=false, color = blue] "30" -> "40" [constraint=false]; "40" -> "30" [constraint=false, color = blue] "40" -> "50" [constraint=false]; "50" -> "40" [constraint=false, color = blue] "50" -> "head" [dir=both, color=red, constraint=false]; "50" -> "NULL"[style=dotted]; } ``` #### ```quick_sort``` 實作非遞迴 (non-recursive; iterative) 的快速排序法程式碼,用 ```begin``` 儲存切割的每個片段起始點,而對片段的處理分成以下幾種 : 1. 片段長度為 1 : 加入到 ```result``` 內 (不需要排序了) 2. 片段長度不為 1 : 分成三個片段, ```pivot``` (最左邊的節點)、比 ```pivot``` 還要小的片段、比 ```pivot``` 還要大的片段 ### 研讀 〈[A comparative study of linked list sorting algorithms](https://dl.acm.org/doi/pdf/10.1145/225890.225893)〉 ### 用 ```List API``` 實作排序 ## 第二周測驗題 - [測驗1](https://hackmd.io/@sysprog/linux2025-quiz2#%E6%B8%AC%E9%A9%97-1) ### 解釋程式碼運作原理 與上週測驗不同的是這周採用遞迴的方式實作,而相同的點是一樣會用三個 ```linked list``` 來存(```pivot``` 、較大、較小),首先找到 ```pivot``` (最左邊的點)並移除 ```cpp pivot = list_first_entry(head, struct listitem, list); list_del(&pivot->list); ``` 接著找到較大跟較小的節點分別存到 ```list_greater``` 跟 ```list_less``` 中 ```cpp 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_less``` 跟 ```list_greater``` ```cpp list_quicksort(&list_less); list_quicksort(&list_greater); ``` 到這步時理論上 ```head``` 會是 singular 的節點,節點都分布在 ```pivot```、```list_less```、```list_greater``` 當中,接下來將節點依序放入 ```head``` 中 ```cpp list_add(&pivot->list, head); list_splice(&list_less, head); list_splice_tail(&list_greater, head); ``` ### 改進 ```random_shuffle_array``` ### 若將 list_move_tail 換為 list_move,為何會無法滿足 stable sorting >Stable sorting & unstable sorting Stable sorting 代表相同數值的元素在經過排序後,相對順序不會改變,這對於多索引排序很重要;unstable sorting 則不能保證相同數值的元素排序後的相對順序能不改變。 因為 ```list_for_each_entry_safe``` 是由前到後去遍歷的,如果用 ```list_move``` 會移到 linked list 前面,相對的順序就改變了 ```graphviz digraph g { node[shape = plaintext]; pivot [fontcolor="red"]; "list_less" [fontcolor="purple"]; "list_greater" [fontcolor="purple"]; "head" "item" null1 [label=""]; null2 [label=""]; null3 [label=""]; null4 [label=""]; node[shape=box]; n0 [label="0"]; n2_1 [label="2", color=orange]; n2_2 [label="2", color=blue]; n3 [label="3"]; n4 [label="4"]; n5 [label="5"]; { rank="same"; } pivot -> n4 -> null1 [dir=both]; list_less -> n0 -> null2 [dir=both]; list_greater -> n5 -> null3 [dir=both]; head -> n2_1 -> n2_2 -> n3 -> null4 [dir=both]; item -> n2_1 } ``` 使用 ```list_move``` 加在 linked list 前面 (兩個 2 順序對調了) : ```graphviz digraph g { node[shape = plaintext]; pivot [fontcolor="red"]; "list_less" [fontcolor="purple"]; "list_greater" [fontcolor="purple"]; "head" "item" null1 [label=""]; null2 [label=""]; null3 [label=""]; null4 [label=""]; node[shape=box]; n0 [label="0"]; n2_1 [label="2", color=orange]; n2_2 [label="2", color=blue]; n3 [label="3"]; n4 [label="4"]; n5 [label="5"]; { rank="same"; } pivot -> n4 -> null1 [dir=both]; list_less -> n2_2 -> n2_1 -> n0 -> null2 [dir=both]; list_greater -> n5 -> null3 [dir=both]; head -> n3 -> null4 [dir=both]; item -> n3 } ``` 使用 ```list_move_tail``` 加在 linked list 後面 (兩個 2 順序不會對調) : ```graphviz digraph g { node[shape = plaintext]; pivot [fontcolor="red"]; "list_less" [fontcolor="purple"]; "list_greater" [fontcolor="purple"]; "head" "item" null1 [label=""]; null2 [label=""]; null3 [label=""]; null4 [label=""]; node[shape=box]; n0 [label="0"]; n2_1 [label="2", color=orange]; n2_2 [label="2", color=blue]; n3 [label="3"]; n4 [label="4"]; n5 [label="5"]; { rank="same"; } pivot -> n4 -> null1 [dir=both]; list_less -> n0 -> n2_1 -> n2_2 -> null2 [dir=both]; list_greater -> n5 -> null3 [dir=both]; head -> n3 -> null4 [dir=both]; item -> n3 } ``` ## 第二周測驗題 - [測驗2](https://hackmd.io/@sysprog/linux2025-quiz2#%E6%B8%AC%E9%A9%97-2) ### 解釋 counting leading zero ```clz``` ### 解釋 ```sqrti``` #### 十分逼近法 實作 ```sqrti``` 其實是用到二分逼近法,那什麼是二分逼近法呢?可以聯想到國中所教的十分逼近法。 想想國中是如何求 $\sqrt{2}$ 的小數,首先找到它介於哪兩個整數間 $$1 < \sqrt{2} < 2$$ 接著往更小的精度下去找 $$1.4 < \sqrt{2} < 1.5$$ $$1.41 < \sqrt{2} < 1.42$$ $$...重複此步驟$$ $$1.41421356 < \sqrt{2} < 1.41421357$$ 所以 $\sqrt{2}$ 可以表示成 $$ 1 \times \color{red}{10^1} + 4 \times \color{red}{10^{-1}} + 1 \times \color{red}{10^{-2}} + 4 \times \color{red}{10^{-3}} + 2 \times \color{red}{10^{-4}} + 1 \times \color{red}{10^{-5}} + 3 \times \color{red}{10^{-6}} + \dots $$ #### 二分逼近法 有了十分逼近法的概念就能對二分逼近法有更深的理解了,假設我們想要求 $y$ 使的 $y^2 = x$ 那我們可以先求 $x$ 跟 $y$ 的精度,若 $x$ 換成二進位後,第 $k$ 位元是最高為 1 的位元,那可以獲得以下資訊 : $$ 2^k <= x < 2^{k+1} $$ $$ 2^{k/2} <= y < 2^{(k+1)/2} $$ $$ y = a_1 \times \color{red}{2^{k/2}} + a_2 \times \color{red}{2^{k/2 - 1}} + a_3 \times \color{red}{2^{k/2 - 2}} + \dots + a_{k/2} \times \color{red}{2^{1}} + a_{k/2 + 1} \times \color{red}{2^{0}} $$ 因此我們只需要求得 $a_1$ ~ $a_{k/2 + 1}$ 的係數值 ( 0 或 1 )就能算出 $\sqrt{x}$ 的估計值,以下說明如何只用二分逼近的概念實作效率超極低的開更號程式 首先找出 $y$ 的範圍, total_bits - 1 - clz64(x) 可以算出 $x$ 最大為 1 的位元位子也就是上述的 $k$ ,接著除以 2 (>>1) 就能找出 $y$ 可能最大為 1 的位元位子也就是 $k/2$ (shift) 而 **m 就是 2^k/2^** ,m 就是檢查的第一個值 eg: x = 11001 | k | k/2 (shift) | m | | --- |:-----------:|:--------:| | 5 | 2 | 2^2^ = 4 | ```cpp int shift = (total_bits - 1 - clz64(x)) >> 1; m = 1ULL << shift; ``` 接著開始找係數也就是估計, b 是估計值看看係數補上 1 後也就是加上 m 的值看看是否會大於 x 如果不會將 y 加上 m ,最後檢查下一位 (k/2 - 1) 然後重複直到 m = 0 eg: x = 11001 = 25 k/2 = 2 \ \begin{array}{c c c} \textbf{1st} & \textbf{2nd} & \textbf{3rd} \\ \underset{\text{這裡!}}0 \quad {0} \quad 0 & 1 \quad \underset{\text{這裡!}}{0} \quad 0 & 1 \quad {0} \quad \underset{\text{這裡!}}0 \\ \text{(檢查第二位係數)} & \text{(檢查第一位係數)} & \text{(檢查第零位係數)} \\ m = 0b100 = 4 & m = 0b10 = 2 & m = 0b1 = 1 \\ y = 0 & y = 0b100 = 4 & y = 0b100 = 4 \\ b = y + m = 0 + 4 & b = y + m = 4 + 2 & b = y + m = 4 + 1 \\ x \geq b \cdot b \quad (\text{符合}) & \cancel{x \geq b \cdot b} \quad (\text{不符合}) & x \geq b \cdot b \quad (\text{符合}) \\ y = 4 & y = 4 & y = 5 \end{array} 1 0 1 y = 4 + 1 = 5 ```cpp while (m) { uint64_t b = y + m; if (x >= b*b) { y += m; } m >>= 1; } ``` 以下是第一版沒效率算法 ```cpp 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; if (x >= b*b) { y += m; } m >>= 1; } return y; } ``` >[!Tip]為甚麼說沒效率呢? 因為乘除法相較於 bitwise 的操作使用了更多的 CPU cycles #### 使用乘法公式優化 原本的 b 是估計值,用來估計 y 可能得值,那在優化的版本,我們將 ==b 的定義改為相較於上次的 y^2^ 新的 (y + m)^2^ 增加的值== : y + m 是預估的值 (y 是上一次的值) $$ (y + m)^2 = y^2 + 2 \times y \times m + m^2 $$ $$ b = (y + m)^2 - y^2 = \color{red}{2 \times y \times m} + \color{red}{m^2} $$ 讓 2^shift^ 保持等於 m ,所以乘上 m 只用往左移動 shift 就好 ```diff - uint64_t b = y + m; + uint64_t b = y << (shift + 1) + m << shift; // 2my + m^2 ``` 然後將 x 減掉,相較於上次的 y^2^ , (y+m)^2^ 所增加的值, if 的條件也變為只用判斷增加的值會不會超過 x 的範圍 ```diff - if (x >= b*b) { + if (x >= b) { + x -= b; y += m; } ``` 由於要保持 2^shift^ 等於 m ,所以 shift 要 -1 ```diff + shift -= 1; ``` 以下是第二版完整的 code ,可以發現已經沒有使用乘法了,效率理論上會好上許多,但我們可以再簡化 >測試效率 [To do] ```cpp 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 << (shift + 1) + m << shift; if (x >= b) { x -= b; y += m; } m >>= 1; shift -= 1; } return y; } ``` #### 善用 2 的冪簡化 在第二版每次計算新增的值時,都會使用 ```shift``` 對 y 跟 m 處理,我們是否有辦法不要使用 shift ? 以下針對 b = m^2^ + 2my 中的 ==m^2^ 、 y 以及 b== 分開討論 ##### m^2^ 仔細觀察第二版中 m^2^ 的變化,因為 m 每次都向右位移一個 bit 也就是檢查完 2^k^ 下一個會檢查 2^k-1^ ,所以 m^2^ 其實每次是變化兩個 bit,所以我們把 m 的定義直接改成 m^2^ ,每次變化 2 bit ```diff - uint64_t b = y << (shift + 1) + m << shift; + uint64_t b = y + m ``` ```diff - m >>= 1; -shift -= 1; + m >>= 2; ``` ##### y 那改成 m^2^ 後 y 會有什麼變化呢 ? 首先我們先想想第二版中的 while 迴圈會跑幾次,若從 2^k^ 開始估計,迴圈會跑 k + 1 次 接著可以思考若 y += m 在第三版中要怎麼修改 : 以第一次 while 迴圈來說,若 m 是 2^2k^ ,實際上估計值要加上 2^k^ 但因為直接加上 m ,所以會多乘以 2^k^ ,那我們就讓倒數第 k 次迴圈時 y 的值是實際估計值乘上 2^k^。以下面為例還剩 k 次迴圈 $$ y = y + 2^{2k} = 0 + 2^{2k} = 2^k \times 2^k = 2^k \times 實際估計值 $$ 第二次 while 迴圈時, m 變成 2^2k-2^ ,實際估計值要加上的是 2^k-1^ ,那我們先把 y (上一個實際估計值乘以 2^k^) 除以 2 (y >>= 1) ,y 就會是上一個實際估計值的 2^k-1^ 倍,那在加上 m 時 : $$ y + m = 2^{k-1} \times (上一個實際估計值) + 2^{k-1} \times 2^{k-1} $$ $$ y + m = 2^{k-1} \times ((上一個實際估計值) + 2^{k-1})) $$ $$ y + m = 2^{k-1} \times (這次的實際估計值) $$ 所以 y 的定義變成了 ==2^倒數第k次迴圈^ x (實際估計值)== 那到最後一次是 $$ y = 2^{倒數第0次迴圈} \times (實際估計值) = 2^0 \times (實際估計值) = 實際估計值 $$ ```diff while (m) { - uint64_t b = y << (shift + 1) + m << shift; + uint64_t b = y + m; + y >>= 1 if (x >= b) { x -= b; y += m; } - m >>= 1; + m >>= 2; - shift -= 1; } ``` ##### b 那確認了 y 跟 m 的定義後,接著我們要來驗證 b 的定義是否跟第二版相同 : 假設 m' 是理論上估計值要加上的值 y' 是上一個理論上的估計值 b 是現在理論上的估計值與上一個理論上估計值的差值 所以 $$b = (y' + m')^2 - y'^2 = m'^2 + 2m'y'$$ 根據上面所提 m = m'^2^ y = 2^倒數第k次迴圈^ x y' 所以 $$b = y + m = m'^2 + 2^{倒數第k次迴圈} \times y'$$ 所以接下來只要證明 $$ \begin{cases} b = m'^2 + 2m'y'\\ b = m'^2 + 2^{倒數第k次迴圈} \times y' \end{cases} $$ 也就是 $$ 2m'y' = 2^{倒數第k次迴圈} \times y'$$ $$ 2m' = 2^{倒數第k次迴圈}$$ $$2m' = 2 \times 2^{(倒數第k次迴圈) - 1}$$ $$m' = 2^{(倒數第k次迴圈) - 1} = 2^{(倒數第k-1次迴圈)}$$ 確實因為倒數第 k-1 次回圈就是檢查第 k-1 個 bit ,也就是 2^k-1^ ,所以 m' 的值就是 2^k-1^ 由此得證 以下是最終版的程式碼 ```cpp 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; } ```