# 2024q1 Homework2 (quiz1+2) ## quiz1 測驗一 :::success 1. 解釋程式碼的運作原理,提出改進方案並予以實作。 1. 使用 Linux 核心風格的 List API 改寫上述程式碼,並針對鏈結串列,提出可避免最差狀況的快速排序實作,應設計效能評比的測試程式。 ::: ### 運作原理 #### quick_sort 鏈結串列結構: ```clike typedef struct __node { struct __node *left, *right; struct __node *next; long value; } node_t; ``` ```graphviz digraph "struct __node" { node[shape=plaintext]; NULL[fontcolor="black"]; rankdir = "LR" subgraph "cluster __node" { next [shape = box]; value[shape = box]; left_right [shape = record, label = "{<left> left |<right> right}"]; } next -> NULL; } ``` `L`:指向被排序的鍵結串列第一個節點 `R`:指向被排序的鍵結串列最後一個節點 `pivot` :指向被排序的鍵結串列第一個節點 用 `begin[]` 與 `end[]` 作為堆疊,用來紀錄比較的範圍 `begin[i]`:left 串列的第一個節點 `end[i]`:left 串列的最後一個節點 `begin[i+1]`: pivot 節點 `end[i+1]`: pivot 節點 `begin[i+2]`:right 串列的第一個節點 `end[i+2]`:right 串列的最後一個節點 **1. 選擇 `pivot`** 每次都選擇鏈結串列第一個節點作為 `pivot` **2. 分割鏈結串列** 所有比 `pivot` 小的元素放在 L 串列,比`pivot`大的元素放在 R 串列 **3. 由 `begin[]` 與 `end[]` 的範圍進行排序** 每次都優先處理 R 串列去進行排序 直到 `beg[i] == end[i]` ,表示此串列中只有一個節點,就將該節點加入到 `result` 串列中。 以下方的鏈結串列為例: ```graphviz digraph quick_sort_example{ node[shape=plaintext]; pivot [fontcolor="red"]; L [fontcolor="blue"]; R [fontcolor="blue"]; NULL[fontcolor="black"]; p[fontcolor="black"]; node[shape=box]; { rank="same"; rankdir=LR; 4->1->3->2->5->NULL; } pivot->4; L->4; p->1; R->5; } ``` `p` 依序走完鏈結串列,並分割鏈結串列,將所有比 `pivot` 小的元素放在 L 串列,比`pivot`大的元素放在 R 串列,如下圖: ```graphviz digraph quick_sort_divide{ node[shape=plaintext]; R [fontcolor="blue"]; pivot [fontcolor="red"]; L [fontcolor="blue"]; node[shape=box]; rankdir=LR; L->1->3->2 pivot->4; R->5; } ``` 更新 `begin[]` 與 `end[]` 的值,接著每次都優先處理 R 串列去進行排序,此時 `beg[i] == end[i]` ,表示 R 串列中只有一個節點,加入到 `result` 串列中第一個節點的位置 ```graphviz digraph result{ node[shape=plaintext]; result_head [fontcolor="red"]; node[shape=box]; result_head->5 } ``` 接著 `i--`,此時`beg[i] == end[i]` ,`begin[i]` 與 `end[i]` 的值放的是 `pivot`,將 `pivot` 節點加入到 `result` 串列中第一個節點的位置 ```graphviz digraph result{ node[shape=plaintext]; result_head [fontcolor="red"]; node[shape=box]; { rank="same"; rankdir=LR; 4->5; } result_head->4 } ``` 接著 `i--`,開始處理 L 串列,進行排序直到整個串列排序完成。 ### 改進方法 #### 縮減 begin[]的大小 #### 避免最差狀況的快速排序實作 **平均情況:** 快速排序的時間複雜度為 $O(nlogn)$ **最差情況:** 快速排序的時間複雜度為 $O(n^2)$,當排列的元素已經按照升序或降序排列,導致每次分割都只能減少一個元素。 所以選擇 `pivot` 很重要,希望每次都能將串列平均分割,避免最差情況的發生,我的作法是隨機選擇 `pivot`。 #### Linux 核心風格的 List API 在 `node_t` 結構中新增 `struct list_head list`,因 Linux List API 中 list_head 的結構中已經有 `prev` 和 `next` ,因此可以把 `node_t` 結構中的 `left`、`right`、 `next` 指標移除,另外因避免快速排序最差情況的發生,需再新增一個隨機選取 `pivot` 的函式 `rand_pivot()`。 ```clike typedef struct __node { long value; struct list_head list; } node_t; ``` 在 `quick_sort()`中不需要 `end[]`,因 linux list 是雙向鏈結串列,查找最後一個只需要使用 `head->prev` 即可找到串列最後一個節點。 在 改寫 Linux 核心風格的 List API 時,發現在 `list_add` 時會發生 segamentation fault,進一步去檢查,發現在 `quick_sort` 中把 `pivot` 加入到 `begin[i+1]` 時發生錯誤,再重新審查一遍後發現是 `begin[]` 沒有去做 `list_new()`。 #### Linux 效能分析工具: Perf 效能測試上使用 `perf` 來分析選取 `pivot` 策略的差異,`perf` 是一個在 Linux 系统上用於性能分析的工具。`perf stat` 是一個用於監控和分析系統性能的命令。它的主要目的是執行指定的命令,並在命令執行期間保持對硬體和軟體事件發生的運行計數,並生成這些計數的統計信息。 硬體事件統計:CPU 迴圈數、快取命中率、指令執行次數等。 軟體事件統計:上下文切換次數、記憶體存取、I/O 處理時間等。 實驗分成最差情況和一般情況下隨機選取 `pivot` 和 直接選取鏈結串列的第一個節點作為 `pivot` 的差異,因一般情況下指令數以及耗費時間預計會高於最差情況下,因在排序前會先打亂陣列資料,主要比較隨機 `pivot` 和直接選取第一個節點的平均執行時間,和觀察兩者之間的差異,特別是在不同大小的輸入資料上。 每次執行效能測試前須清除快取 ```shell $ echo 3 | sudo tee /proc/sys/vm/drop_caches ``` 執行效能測試 ```shell $ perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles ./linux_qsort_no_rand_pivot $ perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles ./linux_qsort ``` 重複執行 5 次該程序,並顯示每個 event 的變化區間。 觀察的 event 分別為 快取的命中率、快取的存取、指令數和執行的 cycle 數。 **worst case 的情況下(排序的順序為升序或降序):** :::spoiler **測試 100000 筆資料** ```perf Performance counter stats for './linux_qsort_no_rand_pivot' (5 runs): 17,1340 cache-misses # 7.08% of all cache refs ( +- 19.31% ) 241,9193 cache-references ( +- 4.59% ) 3,2687,3138 instructions # 2.56 insn per cycle ( +- 0.02% ) 1,2752,6925 cycles ( +- 0.53% ) 0.03275 +- 0.00447 seconds time elapsed ( +- 13.64% ) ``` ```perf Performance counter stats for './linux_qsort' (5 runs): 17,5439 cache-misses # 15.18% of all cache refs ( +- 19.82% ) 115,5482 cache-references ( +- 0.18% ) 3,6791,2078 instructions # 3.19 insn per cycle ( +- 0.02% ) 1,1530,0504 cycles ( +- 1.76% ) 0.02647 +- 0.00122 seconds time elapsed ( +- 4.59% ) ``` ::: :::spoiler **測試 1000 筆資料** ```perf Performance counter stats for './linux_qsort_no_rand_pivot' (5 runs): 5820 cache-misses # 22.02% of all cache refs ( +- 28.89% ) 2,6429 cache-references ( +- 4.96% ) 3900,0168 instructions # 4.22 insn per cycle ( +- 0.02% ) 924,3143 cycles ( +- 3.14% ) 0.00822 +- 0.00137 seconds time elapsed ( +- 16.72% ) ``` ```perf Performance counter stats for './linux_qsort' (5 runs): 4940 cache-misses # 23.56% of all cache refs ( +- 40.92% ) 2,0971 cache-references ( +- 7.79% ) 380,2888 instructions # 2.31 insn per cycle ( +- 0.09% ) 164,9545 cycles ( +- 2.61% ) 0.00603 +- 0.00311 seconds time elapsed ( +- 51.58% ) ``` ::: :::spoiler **測試 10筆資料** ```perf Performance counter stats for './linux_qsort_no_rand_pivot' (5 runs): 3549 cache-misses # 17.13% of all cache refs ( +- 41.67% ) 2,0712 cache-references ( +- 2.02% ) 103,6432 instructions # 1.23 insn per cycle ( +- 0.37% ) 84,3458 cycles ( +- 0.97% ) 0.0015844 +- 0.0000369 seconds time elapsed ( +- 2.33% ) ``` ```perf Performance counter stats for './linux_qsort' (5 runs): 4262 cache-misses # 19.76% of all cache refs ( +- 40.00% ) 2,1571 cache-references ( +- 7.35% ) 104,3593 instructions # 1.25 insn per cycle ( +- 0.41% ) 83,4795 cycles ( +- 3.44% ) 0.00492 +- 0.00288 seconds time elapsed ( +- 58.52% ) ``` ::: --- **一般情況:** :::spoiler **測試 100000 筆資料** ```perf Performance counter stats for './linux_qsort_no_rand_pivot' (5 runs): 18,6967 cache-misses # 7.09% of all cache refs ( +- 14.72% ) 263,6204 cache-references ( +- 1.68% ) 3,2716,9090 instructions # 2.56 insn per cycle ( +- 0.04% ) 1,2779,8557 cycles ( +- 0.37% ) 0.03144 +- 0.00288 seconds time elapsed ( +- 9.16% ) ``` ```perf Performance counter stats for './linux_qsort' (5 runs): 13,1878 cache-misses # 2.92% of all cache refs ( +- 31.84% ) 452,2847 cache-references ( +- 2.92% ) 3,7541,7692 instructions # 2.07 insn per cycle ( +- 0.01% ) 1,8132,7720 cycles ( +- 0.65% ) 0.04373 +- 0.00391 seconds time elapsed ( +- 8.93% ) ``` ::: :::spoiler **測試 1000 筆資料** ```perf Performance counter stats for './linux_qsort_no_rand_pivot' (5 runs): 5773 cache-misses # 26.58% of all cache refs ( +- 30.05% ) 2,1721 cache-references ( +- 4.27% ) 360,6218 instructions # 2.15 insn per cycle ( +- 0.06% ) 167,3905 cycles ( +- 1.24% ) 0.0027962 +- 0.0000931 seconds time elapsed ( +- 3.33% ) ``` ```perf Performance counter stats for './linux_qsort' (5 runs): 4256 cache-misses # 18.23% of all cache refs ( +- 42.39% ) 2,3345 cache-references ( +- 3.09% ) 392,6116 instructions # 2.10 insn per cycle ( +- 0.04% ) 187,4003 cycles ( +- 0.80% ) 0.003057 +- 0.000155 seconds time elapsed ( +- 5.06% ) ``` ::: :::spoiler **測試 10 筆資料** ```perf Performance counter stats for './linux_qsort_no_rand_pivot' (5 runs): 4312 cache-misses # 23.12% of all cache refs ( +- 40.36% ) 1,8653 cache-references ( +- 5.29% ) 103,6244 instructions # 1.23 insn per cycle ( +- 0.16% ) 84,0002 cycles ( +- 3.29% ) 0.0012722 +- 0.0000279 seconds time elapsed ( +- 2.19% ) ``` ```perf Performance counter stats for './linux_qsort' (5 runs): 4039 cache-misses # 19.44% of all cache refs ( +- 36.90% ) 2,0777 cache-references ( +- 2.39% ) 103,2732 instructions # 1.16 insn per cycle ( +- 0.17% ) 88,8412 cycles ( +- 3.26% ) 0.001064 +- 0.000126 seconds time elapsed ( +- 11.84% ) ``` ::: --- **最差情況** | 輸入資料大小 | 100000 | 1000 | 10 | | --------------------------- | --------- | ---- | --- | | **選取第一個節點作`pivot`** | 0.03275 s | 0.00822 s |0.0015844 s | | **隨機選取節點作`pivot`** | 0.02647 s | 0.00603 s| 0.00492 s| **一般情況** | 輸入資料大小 | 100000 | 1000 | 10 | | --------------------------- | --------- | ---- | --- | | **選取第一個節點作`pivot`** | 0.03144 s | 0.0027962 s | 0.0012722 s | | **隨機選取節點作`pivot`** | 0.04373 s | 0.003057 s| 0.001064 s| **總結:** * 隨機 `pivot` 可以減少最壞情況下的執行時間,因為它避免了選擇最差的 `pivot`。 * 直接選取第一個節點的方法簡單,但在已排序情況下導致效能下降。 * 選擇 `pivot` 的方法和數據的特性(例如數據是否已經部分排序)可能會影響 `quick_sort` 的效率。因此,在使用 `quick_sort` 時,選擇合適的 `pivot` 選擇策略是很重要的。 ## quiz1 測驗二 :::success 1. 解釋上述程式碼的運作原理,提出改進方案並予以實作。 2. 將改進過的 Timsort 實作整合進 sysprog21/lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。 ::: ### 運作原理 **主要想法:** 想要利用資料中已存在排序好的子序列來最小化比較和交換的次數。 Tim Peter發現在現實世界中,許多資料都是由部分排序好的序列組合而成。他提出的策略是先找出這些已排序的子序列(也就是所謂的`run`),然後透過不斷合併這些子序列來完成整個資料範圍的排序。儘管在處理隨機資料時,Timsort的效能稍遜於理論上的極限值$O(nlogn)$,但對於部分已排序的資料,其表現卻相當出色。 Timsort的改進主要集中在三個方面: 1. 加快合併過程。 2. 減少合併的次數。 3. 在某些情況下尋找比合併排序更有效的排序方法。 Timsort 使用一組堆疊 (stack) 來存儲run,這樣做是為了減少掃描整個資料範圍以產生run所需的記憶體開銷。在這個過程中,Timsort使用`merge_collapse`函式來確保堆疊上的run長度保持平衡。該函式主要檢查堆疊頂端的3個run是否滿足以下原則: * A的長度要大於B和C的長度總和。 * B的長度要大於C的長度。 如果不符合這些條件,函式將決定是否進行合併。這樣做確保了堆疊中run的長度平衡,並且由於合併只能在相鄰的兩個run之間進行,以確保排序的穩定性,因此合併操作僅可能採取A和BC 以及 AB和C 兩種形式。這使得Timsort在執行時能夠兼顧高效和穩定性。 例如,考慮 3 段 run: A 的長度為 30,B 的長度為 20,C 的長度為 10,由於 A 的長度不大於 B 加 C 的總和(亦即 $A<=B+C$),且 C 的長度小於 A,因此會選擇將 C 與 B 進行合併,形成兩個新的 run * A: 30 * BC: 30 藉此,不僅確保堆疊中 run 的長度平衡,且因合併只能在相鄰的兩個 run 間進行,以確保排序的穩定性,因此合併操作僅可能採取 $A+(B+C)$ 或 $(A+B)+C$ 的形式進行。此舉讓 Timsort 在執行時兼顧高效又穩定。 處理相鄰子序列的合併過程中,直接在記憶體中進行操作會面臨挑戰,因為大量的記憶體操作不僅難以實作,效率也未必理想。因此,Timsort採用了使用臨時記憶區的策略,其大小設定為兩個子序列(A、B)中較短者的長度。 當A的長度小於B時,會先將A序列暫存。一種直觀的合併方法是從A和B的開頭開始進行逐對比較,將較小的元素放置於A的原位置,這一過程持續進行直到A和B完全排序,類似於經典的合併排序,也稱為逐對合併(one-pair-at-a-time)。 然而,考慮到A和B都是已排序的序列,可以進一步改進:首先尋找B的首個元素(即B[0])在A中的排序位置,這樣就能確定A中有一段是小於B[0]的,可以將這部分元素放回A。接著,尋找剩餘A的首個元素在B中的位置,如此反覆進行,直到完成排序。這種方法被稱為Galloping mode。 例如,考慮以下情況: A: [3, 5, 7, 9, 11] B: [6, 8, 10, 12, 13, 17] 顯然A較短,因此先將A放入暫存記憶區`temp`。然後找到B[0]在A中的位置,即A[1]和A[2]之間,因為B[0]=6,所以A[0] 到 A[1] 都可以直接放回A序列。然後,將`temp`的首個元素放到B中適當的位置,如此反覆進行。 這種改進方案在大多數情況下效果顯著,但對於隨機資料序列而言,可能比逐對合併更慢。因此,在Timsort中有一個機制決定是否要啟動Galloping mode。 相對於合併排序,Timsort的改進包括: * 降低快取失效(cache miss) * 減少記憶體開銷 * 降低比較次數 傳統的合併排序會從合併樹的最底層開始合併,這導致每進行一層的合併就要掃過整個序列,這對快取不友好。Timsort優化了這一點,利用Galloping mode和臨時記憶區的策略,在適當時機進行合併,從而降低快取失效和記憶體開銷,同時減少比較次數,提高了效率。 #### **Galloping mode** 在Timsort中,當資料呈現非均勻分布且存在叢集(Clusters)特性時,會啟用Galloping演算法。初始時無法得知資料分布情況,但在使用傳統的「逐對比較」方式時,若連續進行多次比較都來自同一個Run,就會推測資料可能具有叢集特性,進而切換至Galloping模式。判斷是否切換的條件是有一個名為`MIN_GALLOP`的常數,默认值為7,即當進行了7次連續比較,且來自同一個Run時,就會啟動Galloping模式。一旦進入Galloping模式,如果從同一個Run找到的連續資料次數小於`MIN_GALLOP`,則會回到傳統的「逐對比較」模式。 Galloping Mode: * 進入 Galloping 模式時,Timsort 需要不斷尋找某個數字在已排序陣列中的位置。 * 假設 A 是較短的序列,我們要找 A[0] 在 B 序列中應排序的位置。 * 傳統的二分查找是一種選擇,但 Timsort 使用了另一種演算法,稱為 Galloping Search(也稱為指數搜尋)。 * Galloping Search 透過跳躍式地尋找,更快地確定 B[0] 在 A[0] 中的位置,從而將整個 A 的一部分移動到正確的位置。 **1. 初始化堆疊大小** `stk_size` 被設置為 0,用來紀錄堆疊中的 `run` 數量 **2. 建單向鏈表** 如果 `head == head->prev`,那麼函數將返回。 否則,它將把雙向鏈表轉換為單向鏈表。 **3.找到下一個 `run` 並合併** 在一個循環中尋找下一個 `run` (`find_run` 函數來完成),並在每次找到一個 `run` 時合併它 (`merge_collaps` 來合併)。 **4. 強制合併所有 `run`** 當所有的輸入都被處理後,將強制合併所有剩餘的 `run` (`merge_force_collapse` 來強制合併)。 **5. 最終合併並重建鏈接** 最終的合併(merge_final ),並重建雙向鏈表的鏈接(build_prev_link)。 ## quiz2 測驗一 :::success 1. 解釋上述程式碼的運作,撰寫完整的測試程式,指出其中可改進之處並實作 2. 在 Linux 核心的 cgroups 相關原始程式碼,找出 pre-order walk 程式碼並探討 ::: ### 運作原理 * 建立一棵二元樹的,使用了前序(preorder)和中序(inorder)來建立這棵樹。 * 透過前序遍歷可以確定節點的上下關係(越前面的越在上面),而中序遍歷可以確定節點的左右關係(越前面的越在左邊) * 利用前序遍歷的第一個元素來確定根節點的值,並在中序遍歷中找到該元素,將其左邊的元素視為左子樹,右邊的元素視為右子樹。 結構: ```clike struct hlist_node { struct hlist_node *next, **pprev; }; struct hlist_head { struct hlist_node *first; }; ``` 這邊的 `pprev` 宣告為指標的指標 可以發現: * 只有鏈結串列的尾端指標內容是 NULL。 * next 指向相鄰的節點本身,而 pprev 指向指著自身節點的指標。 這樣設計的好處是,當需要刪除的節點是第一個節點時,可以直接透過 `*pprev = next` 來修改指標的指向。 另外,hlist 是一種特別針對雜湊表設計的鏈結串列。雖然雙向鏈結串列也可以用來實作雜湊表,但因為需要在開頭和結尾各使用 2 個指標,所以在 bucket 很大的情況下,會增加記憶體的使用量。 ```graphviz digraph "struct hlist_node" { rankdir = "LR" node[shape = record]; hlist_head[label = "<m>hlist_head | <n>first", group = list] hlist_node1[label = "<m>hlist_node1| {<p> pprev |<n> next}", group = list]; hlist_node2[label = "<m>hlist_node2| {<p> pprev |<n> next}", group = list]; NULL[label = "NULL", shape = plaintext ,fontcolor = "black", group = list]; {rank = "min" hlist_head} hlist_head -> hlist_node1 -> hlist_node2[ weight = 10, style = invis ] hlist_node1 : p -> hlist_head : n hlist_head : n -> hlist_node1 : m hlist_node1 : n -> hlist_node2 : m hlist_node2 : p -> hlist_node1 : n hlist_node2 : n -> NULL } ``` **`struct TreeNode`**: 二元樹節點的結構,包含一個整數值和兩個指向左右子節點的指標。 **`struct order_node`**: 用於存儲數字和其在陣列中的索引。它還包含一個 `hlist_node` 結構,用於將 `hlist_node` 添加到雜湊表中。 **`hlist_add_head`**: 將一個節點添加到雜湊表的頭部。 **`find`**: 在雜湊表中查找一個數字並返回其在中序的索引。如果找不到該數字,則返回-1。 **`dfs`**: 一個遞迴函數,用於建立二元樹。它先建立一個新的節點,然後在中序遍歷的結果中找到該節點的值,利用前序遍歷的第一個元素來確定根節點的值,並在中序遍歷中找到該元素,將其左邊的元素視為左子樹,右邊的元素視為右子樹,並以此劃分左右子樹。然後對左右子樹進行遞迴調用。 ```clike //左子樹 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); ``` 以下方 preorder[], inorder[] 為例: | preorder[0] | preorder[1] | preorder[2] | preorder[3] | preorder[4] | preorder[5] | |:-----------:|:-----------:|:-----------:|:-----------:|:-----------:|:-----------:| | 3 | 9 | 12 | 20 | 15 | 7 | | inorder[0] | inorder[1] | inorder[2] | inorder[3] | inorder[4] | inorder[5] | |:-----------:|:-----------:|:-----------:|:-----------:|:-----------:|:-----------:| | 12 | 9 | 3 | 15 | 20 | 7 | root = 3,其對應中序的索引為 2, 其左右子樹的範圍便是 | 左子樹 | 右子樹 | | ------------------------------ | ------------------------------- | | inorder[in_low]~inorder[idx-1] | inorder[idx+1]~inorder[in_high] | | inorder[0] ~inorder[1] | inorder[3]~inorder[5] | 對應回前序,其左右子樹的範圍便是 | 左子樹 | 右子樹 | | ------------------------------ | ------------------------------- | | preorder[pre_low+1]~preorder[pre_high- (idx-in_low)] | preorder[pre_high -(idx-in_low)+1]~preorder[pre_high] | | preorder[1]~preorder[2] | preorder[3]~preorder[5] | ```clike //左子樹 tn->left = dfs(preorder, 1, 2, inorder, 0, 1, in_heads, size); //右子樹 tn->right = dfs(preorder,3 , 5, inorder,3, 5, in_heads, size); ``` **`buildTree`**: 用於建立二元樹。藉由呼叫 `node_add` 函式將 `inorder` 的節點建立一個雜湊表(雜湊函式為 :` |val| % inorderSize`。最後,它調用 `dfs` 函數來建立二元樹。 為了能快速找到值所對應中序的索引,我們建立一個雜湊表加快查詢,以值為3為例,當我們要找值為3其在中序的索引,我們會遍歷 `hash = 3 % 5` ->heads[3],第一個便是我們所要找的節點。 ```graphviz digraph hash { nodesep=.05; rankdir=LR; node [shape=record,width=.8,height=.1]; hash [label = "<th>heads[hash] | <f0>heads[0] |<f1>heads[1] |<f2>heads[2] |<f3>heads[3] |<f4>heads[4] ",height=2.5]; node [width=0.8]; node0 [label = "{<n> 20 }"]; node1 [label = "{<n> 15 }"]; node2 [label = "{<n> 7 }"]; node3 [label = "{<n> 12 }"]; node4 [label = "{<n> 3}"]; node5 [label = "{<n> 9}"]; NULL1, NULL2, NULL3 ,NULL4[shape=plaintext,label="NULL"]; hash:f0 -> node0:n -> node1:n -> NULL1; node1:n -> node0:n -> hash:f0; hash:f2 -> node2:n -> node3:n -> NULL2; node3:n -> node2:n -> hash:f2; hash:f3 -> node4:n -> NULL3; node4:n -> hash:f3; hash:f4 -> node5:n -> NULL4; node5:n -> hash:f4; } ``` ### Linux 核心的 cgroups 相關原始程式碼 在 [linux/kernel/cgroup/cgroup.c](https://github.com/torvalds/linux/blob/master/kernel/cgroup/cgroup.c) 中 `css_next_descendant_pre` 利用 pre-order walk 來尋找下一個後代。 `css_for_each_descendant_pre` 會使用 `css_next_descendant_pre` 尋找下一個後代,來進行 pre-order traversal 。 **`css_next_descendant_pre`**: ```clike struct cgroup_subsys_state * css_next_descendant_pre(struct cgroup_subsys_state *pos, struct cgroup_subsys_state *root) { struct cgroup_subsys_state *next; cgroup_assert_mutex_or_rcu_locked(); /* if first iteration, visit @root */ if (!pos) return root; /* visit the first child if exists */ next = css_next_child(NULL, pos); if (next) return next; /* no child, visit my or the closest ancestor's next sibling */ while (pos != root) { next = css_next_child(pos, pos->parent); if (next) return next; pos = pos->parent; } return NULL; } ``` 這個函式主要用於在樹中進行深度優先搜索,直到遍歷完整棵樹為止。 ## quiz2 測驗二 :::success 1. 解釋上述程式碼的運作,撰寫完整的測試程式,指出其中可改進之處並實作 2. 在 Linux 核心找出 LRU 相關程式碼並探討 ::: ### 運作原理 * LRU 緩存(Least Recently Used)是一種常見的緩存演算法,當緩存達到其容量時,會先淘汰最長時間未被訪問的數據。 **結構:** **`list_head`**: 雙向鏈表用於實現 LRU 策略 **`hlist_head`,`hlist_node`**: 用於雜湊表,雜湊表則用於快速查找鍵值。 **`LRUNode`**: 每個節點包含一個鍵(key)、一個值(value)、一個雜湊表節點(hlist_node)和一個雙向鏈表節點(list_head)。 **`LRUCache`**: 主要的緩存結構,包含緩存的容量(capacity)、當前的數量(count)、一個雙向鏈表頭(dhead)和一個雜湊表頭陣列(hhead)。 ```clike struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; }; struct list_head { struct list_head *next, *prev; }; typedef struct { int capacity; int count; struct list_head dhead; struct hlist_head hhead[]; } LRUCache; typedef struct { int key; int value; struct hlist_node node; struct list_head link; } LRUNode; ``` **hlist_head 和 hlist_node 結構:** ```graphviz digraph "struct hlist" { rankdir = "LR" node[shape = record]; hlist_head[label = "<m>hlist_head | <n>first", group = list] hlist_node1[label = "<m>hlist_node1| {<p> pprev |<n> next}", group = list]; hlist_node2[label = "<m>hlist_node2| {<p> pprev |<n> next}", group = list]; NULL[label = "NULL", shape = plaintext ,fontcolor = "black", group = list]; {rank = "min" hlist_head} hlist_head -> hlist_node1 -> hlist_node2[ weight = 10, style = invis ] hlist_node1 : p -> hlist_head : n hlist_head : n -> hlist_node1 : m hlist_node1 : n -> hlist_node2 : m hlist_node2 : p -> hlist_node1 : n hlist_node2 : n -> NULL } ``` **list_head 結構:** ```graphviz digraph "struct list_head" { rankdir = "LR" node[shape = record]; list_head[label = "<m>list_head | {<p> prev |<n> next}", group = list]; rank="same" list_head : n :e-> list_head:e ; list_head : p :w-> list_head:w; } ``` **LRUCache 結構:** ```graphviz digraph "struct LRUCache" { node[shape=plaintext]; rankdir = "LR" subgraph "cluster __node" { capacity [shape = box]; count[shape = box]; node[shape = record]; list_head[label = "<m>dhead | {<p> prev |<n> next}", group = list]; hlist_head[label = "<m>hhead[] |first ", group = list] } } ``` **LRUNode 結構:** ```graphviz digraph "struct LRUNode" { node[shape=plaintext]; rankdir = "LR" subgraph "cluster __node" { key [shape = box]; value [shape = box]; node[shape = record]; hlist_head[label = "<m>link |first ", group = list] list_head[label = "<m>node | {<p> prev |<n> next}", group = list]; } } ``` **`lRUCacheCreate`**: 用於創建一個新的 LRU 緩存,並初始化其容量和數量。 **`lRUCacheFree`**: 用於釋放 LRU 緩存的所有節點和緩存本身。 **`lRUCacheGet`**: 用於獲取指定 key 的 value。如果找到 指定 key,則將其對應的節點移到雙向鏈表的頭部,並返回其值。 **`lRUCachePut`**: 用於增加或更新一個鍵值對。如果鍵已經存在,則更新其值並將其節點移到雙向鏈表的頭部。如果鍵不存在,則建立一個新的節點,並將其添加到雙向鏈表的頭部和雜湊表中。如果此時緩存已滿,則還需要從雙向鏈表的尾部淘汰一個最近最少使用的節點。 `lRUCacheGet` 和 `lRUCachePut` 的時間複雜度都是 O(1)。這是因為雜湊表可以在常數時間內完成查找操作,而雙向鏈表可以在常數時間內加入、刪除和移動節點的操作。這對於需要快速響應的緩存系統來說非常重要。 ## quiz2 測驗三 :::success 1. 解釋上述程式碼的運作原理 2. 在 Linux 核心原始程式碼找出 find_nth_bit 的應用案例,並予以解說和分析,需要涵蓋 CPU affinity 相關的原始程式碼。 ::: ### 運作原理 * 用來操作位元組(bit) **`BITMAP_LAST_WORD_MASK(nbits)`**: 產生一個遮罩,該遮罩的最後 nbits 位元為 0,其餘位元為 1。 **`BIT_MASK(nr)`**: 產生一個遮罩,該遮罩的第 nr 位元為 1,其餘位元為 0。 **`BIT_WORD(nr)`**: 計算第 nr 位元所在的 word 的索引。 **`__const_hweight8(w), __const_hweight16(w), __const_hweight32(w), __const_hweight64(w)`**: 計算一個 8 位元、16 位元、32 位元或 64 位元的數字中,有多少位元為 1。 **`hweight_long(w)`**: 計算一個長整數中,有多少位元為 1。 **`__ffs(word)`**: 找出一個字中,最低位的 1 在哪一位。 **`__clear_bit(nr, addr)`**: 清除位址 addr 中的第 nr 位元。 **`fns(word, n)`**: 找出一個字中,第 n 個 1 在哪一位。 **`find_nth_bit(addr, size, n)`**: 在一個記憶體區域中,找出第 n 個 1 的位元位置 ## Reference [2024q1 第 1 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz1) [2024q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz2) [Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable#Linux-%E6%A0%B8%E5%BF%83%E7%9A%84-hash-table-%E5%AF%A6%E4%BD%9C) [Linux 效能分析工具: Perf](https://wiki.csie.ncku.edu.tw/embedded/perf-tutorial)