--- tags: linux2022 --- # 2022q1 Homework1 (lab0) contributed by < `blueskyson` > ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 12 On-line CPU(s) list: 0-11 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 165 Model name: Intel(R) Core(TM) i7-10750H CPU @ 2.60GHz Stepping: 2 CPU MHz: 2600.000 CPU max MHz: 5000.0000 CPU min MHz: 800.0000 BogoMIPS: 5199.98 Virtualization: VT-x L1d cache: 192 KiB L1i cache: 192 KiB L2 cache: 1.5 MiB L3 cache: 12 MiB NUMA node0 CPU(s): 0-11 ``` ## lab0-c 開發過程 參考[你所不知道的 C 語言:linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#Linked-list-%E5%9C%A8-Linux-%E6%A0%B8%E5%BF%83%E5%8E%9F%E5%A7%8B%E7%A8%8B%E5%BC%8F%E7%A2%BC)實作 ![](https://hackmd.io/_uploads/ByHFSCRit.png) 以下開發過程以 Head 表示佇列的頭,以 Node 表示 `element_t` 結構體。 ### q_new 首先 `malloc` 一個 `struct list_head *head` 作為佇列的 Head,並檢查 `head` 是否為 `NULL`。接下來我把 `head->prev` 和 `head->next` 初始化為 `head`,`INIT_LIST_HEAD` 函式已經實作了前述初始化。 ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (!head) return NULL; INIT_LIST_HEAD(head); return head; } ``` 參考 [kdnvt](https://hackmd.io/@5ux520T8Sd-OoYfdxZR55Q/SyzDKJ9y9),若 malloc `head` 失敗,`head` 本身就會指向 `NULL`。所以程式碼還能改得更短: ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (head) INIT_LIST_HEAD(head); return head; } ``` ### q_free 首先確認傳入的 `struct list_head *l` 是否為 `NULL`。接下來使用 `list_for_each_entry_safe` 走訪佇列中所有 Node,使用 `list_del` 將 `node` 從佇列移除並且 `free` 掉 `node->value` 和 `node` 本身。最後 `free` 掉 `l`,即佇列的 Head。 ```c void q_free(struct list_head *l) { if (!l) return; element_t *node, *tmp; list_for_each_entry_safe (node, tmp, l, list) { list_del(&node->list); free(node->value); free(node); } free(l); } ``` ### q_insert_head 檢查 Head 是否為 `NULL`。`malloc` 一個 `element_t *node` 作為即將插入佇列的 Node,並檢查 `node` 是否為 `NULL`。接下來透過 `strdup` 將字元陣列 `s` 複製到 `node->value`。最後呼叫 `list_add` 將 `node` 插入佇列的開頭。 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *node = malloc(sizeof(element_t)); if (!node) return false; node->value = strdup(s); if (!node->value) { free(node); return false; } list_add(&node->list, head); return true; } ``` ### q_insert_tail 檢查 Head 是否為 `NULL`。`malloc` 一個 `element_t *node` 作為即將插入佇列的 Node,並檢查 `node` 是否為 `NULL`。接下來透過 `strdup` 將字元陣列 `s` 複製到 `node->value`。最後呼叫 `list_add_tail` 將 `node` 插入佇列的尾端。 ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; element_t *node = malloc(sizeof(element_t)); if (!node) return false; node->value = strdup(s); if (!node->value) { free(node); return false; } list_add_tail(&node->list, head); return true; } ``` ### q_remove_head 檢查 Head 是否為 `NULL`、檢查佇列中是否有 Node。透過 `list_entry` 取得佇列的第一個 Node,然後用 `list_del` 移除此 Node。接下來檢查 `sp` 是否為 `NULL` 以及 `bufsize` 是否為 0,使用 `strncpy` 把 Node 的字元陣列複製到 `sp` 中。 改進: - 以 `list_empty(head)` 替換 `head == head->next` - 以 `list_first_entry(head, element_t, list)` 替換 `list_entry(head->next, element_t, list)` ```c if (!head || list_empty(head)) return NULL; element_t *node = list_first_entry(head, element_t, list); list_del(head->next); if (sp && bufsize) { strncpy(sp, node->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return node; } ``` ### q_remove_tail 檢查 Head 是否為 `NULL`、檢查佇列中是否有 Node。透過 `list_entry` 取得佇列的最後一個 Node,然後用 `list_del` 移除此 Node。接下來檢查 `sp` 是否為 `NULL` 以及 `bufsize` 是否為 0,使用 `strncpy` 把 Node 的字元陣列複製到 `sp` 中。 改進: - 以 `list_empty(head)` 替換 `head == head->next` - 以 `list_last_entry(head, element_t, list)` 替換 `list_entry(head->prev, element_t, list)` ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *node = list_last_entry(head, element_t, list); list_del(head->prev); if (sp && bufsize) { strncpy(sp, node->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return node; } ``` ### q_size 檢查 Head 是否為 `NULL`,接下來透過 `list_for_each` 走訪整個佇列以計算 Node 的數量。 ```c int q_size(struct list_head *head) { if (!head) return 0; int len = 0; struct list_head *li; list_for_each (li, head) len++; return len; } ``` ### q_delete_mid 宣告 `struct list_head *slow = head->next, *fast =slow->next`,`slow` 每次會指向 `slow` 的下一個 `list_head`,而 `fast` 會指向 `fast` 的下下個 `list_head`,所以當 `fast` 指向 `head` 時,`slow` 正好在佇列正中間的 Node。然後,透過 `list_entry` 得到 `slow` 所在的 Node 的位址、透過 `list_del` 把 slow 從佇列中移除,最後 `free` 掉這個 Node 的所有資料。 改進: - 以 `list_empty(head)` 替換 `head == head->next` ```c bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; // find the middle node struct list_head *slow = head->next, *fast = slow->next; while (fast != head) { fast = fast->next; if (fast != head) { fast = fast->next; slow = slow->next; } } element_t *node = list_entry(slow, element_t, list); list_del(slow); free(node->value); free(node); return true; } ``` ### q_delete_dup 確認佇列的 Head 是否為 `NULL`。在 `list_for_each_entry_safe` 的每次迭代,`prev_value` 會指向前一個未重複的字串,若 `prev_value` 所儲存的字串與 `node->value` 一模一樣,代表字串重複,將當前的 Node 從佇列移除;反之將 `prev_value` 指向 `node->value`。 ```c bool q_delete_dup(struct list_head *head) { if (!head) return false; element_t *node, *tmp; char *prev_value = ""; list_for_each_entry_safe (node, tmp, head, list) { if (strcmp(prev_value, node->value) == 0) { list_del(&node->list); free(node->value); free(node); } else { prev_value = node->value; } } return true; } ``` [#73](https://github.com/sysprog21/lab0-c/pull/73) 修正了評分程式的 bug,我上面的 `q_delete_dup` 不能通過測試,所以我修正了程式碼: ```c bool q_delete_dup(struct list_head *head) { if (!head) return false; element_t *node, *tmp; char *prev_value = ""; bool only_one = true; list_for_each_entry_safe (node, tmp, head, list) { if (strcmp(prev_value, node->value) == 0) { only_one = false; list_del(&node->list); free(node->value); free(node); } else { prev_value = node->value; if (!only_one) { element_t *del = list_last_entry(&node->list, element_t, list); list_del(&del->list); free(del->value); free(del); only_one = true; } } } // If tail nodes are duplicated if (!only_one) { element_t *del = list_last_entry(head, element_t, list); list_del(&del->list); free(del->value); free(del); only_one = true; } return true; } ``` ### q_swap 確認佇列的 Head 是否為 `NULL`。用 `first` 和 `second` 指向一對連續的 `list_head`,操作 `first->prev`、`first`、`second`、`second->next` 的指標來達成 swap,最後將 `first`、`second` 指向下一對連續的 `list_head`。 ```c void q_swap(struct list_head *head) { if (!head) return; struct list_head *first = head->next, *second = first->next; while (first != head && second != head) { first->prev->next = second; second->prev = first->prev; first->prev = second; first->next = second->next; second->next->prev = first; second->next = first; // point to next pair first = first->next; second = first->next; } } ``` 改進: - 參考 [SmallHanley](https://hackmd.io/@SmallHanley/linux2022-lab0) 的作法,用 list.h 的 API 取代所有指標操作。 ```c void q_swap(struct list_head *head) { if (!head) return; struct list_head *first; list_for_each (first, head) { if (first->next == head) { break; } list_del(first); list_add(first, first->next); } } ``` ### q_reverse 首先確認佇列的 Head 是否為 `NULL`。接下來宣告 `struct list_head *prev, *node` 代表 circular linked list 中的任兩個連續的 `list_head`,接下來透過 `do while` 迴圈,把 `prev->prev` 與 `prev->next` 指向的位址對調,然後把 `prev` 和 `node` 指向下兩個連續的 `list_head`,直到所有 `list_head` 的 `prev` 和 `next` 指向的位址都被對調。 ```c void q_reverse(struct list_head *head) { if (!head) return; struct list_head *prev = head, *node = head->next; do { prev->next = prev->prev; prev->prev = node; prev = node; node = node->next; } while (prev != head); } ``` 圖示: **尚未反轉的佇列** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold", pos="0,0!"]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; e1:left:s -> e5:s; e2:left -> e1:top; e3:left -> e2:top; e4:left -> e3:top; e5:left -> e4:top; e5:right -> e1:s; e1:right -> e2:top; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; } ``` **Step 0: 反轉 Head** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold"]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; e1:left:n -> e2:left:nw [color=red]; e2:left -> e1:top; e3:left -> e2:top; e4:left -> e3:top; e5:left -> e4:top; e5:right -> e1:s; e1:right -> e5 [color=red]; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; p [label="prev", style=dashed, color=grey]; p -> e1:n [style=dashed, color=grey]; n [label="node", style=dashed, color=grey]; n -> e2:n [style=dashed, color=grey]; } ``` **Step 1: 反轉 Node 1** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold"]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; e1:left:n -> e2 [color=blue]; e2:left:s -> e3:left:s [color = red]; e3:left -> e2:top; e4:left -> e3:top; e5:left -> e4:top; e5:right -> e1:s; e1:right:s -> e5:left:s [color=blue]; e2:right:w -> e1:right:e [color=red]; e3:right -> e4:top; e4:right -> e5:top; p [label="prev", style=dashed, color=grey]; p -> e2:n [style=dashed, color=grey]; n [label="node", style=dashed, color=grey]; n -> e3:n [style=dashed, color=grey]; } ``` **Step N: 反轉 Node N** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold"]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; e1:left -> e2:left:s [color=blue]; e2:left -> e3:left:s [color=blue]; e3:left -> e4:left:s [color=blue]; e4:left -> e5:left:s [color=blue]; e5:left -> e1:top:e [color=red]; e5:right:s -> e4:right:e [color=red]; e1:right -> e5:top:w [color=blue]; e2:right:w -> e1:right:e [color=blue]; e3:right:w -> e2:right:e [color=blue]; e4:right:w -> e3:right:e [color=blue]; p [label="prev", style=dashed, color=grey]; p -> e5:n [style=dashed, color=grey]; n [label="node", style=dashed, color=grey]; n -> e1:n [style=dashed, color=grey]; } ``` ### q_sort 實作 merge sort。想法:在排序過程中把佇列的 Head 斷開、把所有 Node 當作 singly-linked list,當排序完成,再把佇列恢復成 circular doubly-linked list。 :::warning 注意連字號擺放位置: singly-linked, doubly-linked :notes: jserv ::: 首先讓最後一個 Node 的 `next` 指向 `NULL`,將 `&(head->next)` 傳入遞迴函式 `sort_recur`。在 `sort_recur` 中,以傳入的 list 少於兩個 Node 作為中止條件,若此段 list 的長度大於等於兩個 Node,則把它平分成 `left`、`right` 左右兩段,呼叫 `sort_recur(&left)`, `sort_recur(&right)` 進入下一層遞迴。當 `sort_recur` 回傳後,用 `strcmp()` 比較字串長度及大小,把 `left` 及 `right` 的所有 Node 由小到大排序。 當 `sort_recur` 的遞迴結束,佇列已經排序完成,但是所有 `list_head` 的 `prev` 都亂指一通,所以我再用一個 `while` 迴圈來把 `prev` 指向前一個 Node 的,然後再讓最後一個 Node 的 `next` 指向 Head,至此佇列便恢復成 circular doubly linked list。 ```c void sort_recur(struct list_head **phead) { // terminal condition if (*phead == NULL || (*phead)->next == NULL) return; // find the middle node struct list_head *slow = *phead, *fast = slow->next; while (fast != NULL) { fast = fast->next; if (fast != NULL) { fast = fast->next; slow = slow->next; } } // split list struct list_head *left = *phead, *right = slow->next; slow->next = NULL; sort_recur(&left); sort_recur(&right); // merge splited lists struct list_head dummy_head; struct list_head *cursor = &dummy_head; while (true) { if (left == NULL) { cursor->next = right; break; } else if (right == NULL) { cursor->next = left; break; } char *leftval = list_entry(left, element_t, list)->value; char *rightval = list_entry(right, element_t, list)->value; if (strcmp(leftval, rightval) <= 0) { cursor->next = left; left = left->next; } else { cursor->next = right; right = right->next; } cursor = cursor->next; } *phead = dummy_head.next; } void q_sort(struct list_head *head) { if (!head || head == head->next) return; // treat the queue as a singly linked list // let the last node's next point to NULL head->prev->next = NULL; sort_recur(&(head->next)); // turn the list back to doubly circular linked list struct list_head *prev = head, *node = head->next; while (node) { node->prev = prev; prev = node; node = node->next; } // link head and the last node prev->next = head; head->prev = prev; } ``` `sort_recur` 應用 pointer-to-pointer 與 dummy_head 使程式碼精簡。 圖示如下: **尚未排序的佇列** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold", pos="0,0!"]; e2 [label="<top>dolphin|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>bear|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>gerbil|{<left>prev|<right>next}", style="bold"]; e1:left:s -> e5:s; e2:left -> e1:top; e3:left -> e2:top; e4:left -> e3:top; e5:left -> e4:top; e5:right -> e1:s; e1:right -> e2:top; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; } ``` **前處理:** 把佇列視為 singly linked list,讓最後一個 Node 的 `next` 指向 `NULL`。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold", pos="0,0!", color=grey]; e2 [label="<top>dolphin|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>bear|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>gerbil|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold", color=red, fontcolor=red]; e1:left:s -> e5:s [color=grey]; e2:left -> e1:top [color=grey]; e3:left -> e2:top [color=grey]; e4:left -> e3:top [color=grey]; e5:left -> e4:top [color=grey]; e5:right -> NULL [color=red]; e1:right -> e2:top [color=grey]; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; } ``` **Merge sort:** 示意圖摘自 [你所不知道的 C 語言:linked list 和非連續記憶體](https://hackmd.io/YA7RMqd6RE2UL8OmmjU9PQ?view#Merge-Sort-%E7%9A%84%E5%AF%A6%E4%BD%9C) ```graphviz digraph G { compound=true node[shape=box, height=.1];sorted_1 sorted_2 sorted_3 sorted_4 sorted_5 sorted_6 sorted_7 sorted_8; node[shape=record, height=.1];input result divide_41 divide_42 divide_21 divide_22 divide_23 divide_24 merge_21 merge_22 merge_23 merge_24 merge_41 merge_42; input[label="<f0>2|<f1>5|<f2>4|<f3>6|<f4>8|<f5>1|<f6>7|<f7>3"] result[label="<f0>1|<f1>2|<f2>3|<f3>4|<f4>5|<f5>6|<f6>7|<f7>8"] divide_41[label="<f1>2|<f2>5|<f3>4|<f4>6"] divide_42[label="<f1>8|<f2>1|<f3>7|<f4>3"] divide_21[label="<f0>2|<f1>5"] divide_22[label="<f0>4|<f1>6"] divide_23[label="<f0>8|<f1>1"] divide_24[label="<f0>7|<f1>3"] sorted_1[label="1"] sorted_2[label="2"] sorted_3[label="3"] sorted_4[label="4"] sorted_5[label="5"] sorted_6[label="6"] sorted_7[label="7"] sorted_8[label="8"] merge_21[label="<f0>2|<f1>5"] merge_22[label="<f0>4|<f1>6"] merge_23[label="<f0>1|<f1>8"] merge_24[label="<f0>3|<f1>7"] merge_41[label="<f1>2|<f2>4|<f3>5|<f4>6"] merge_42[label="<f1>1|<f2>3|<f3>7|<f4>8"] subgraph cluster_0 { style=filled; color="#ef6548"; label="divide"; divide_pad[label="----------------------", style=invis] divide_pad -> divide_23[style=invis] input -> divide_41 input -> divide_42 divide_41 -> divide_21 divide_41 -> divide_22 divide_42 -> divide_23 divide_42 -> divide_24 } divide_21:f0 -> sorted_2 divide_21:f1 -> sorted_5 divide_22 -> sorted_4 divide_22 -> sorted_6 divide_23:f0 -> sorted_8 divide_23:f1 -> sorted_1 divide_24:f0 -> sorted_7 divide_24:f1 -> sorted_3 subgraph cluster_1 { style=filled; color="#a6cee3"; label="sorted lists"; sorted_1; sorted_2; sorted_3; sorted_4; sorted_5; sorted_6; sorted_7; sorted_8; } sorted_2 -> merge_21:f0 sorted_5 -> merge_21:f1 sorted_4 -> merge_22:f0 sorted_6 -> merge_22:f1 sorted_8 -> merge_23:f1[constraint=false] sorted_1 -> merge_23:f0 sorted_7 -> merge_24:f1 sorted_3 -> merge_24:f0 subgraph cluster_2 { style=filled; color="#b2df8a"; label="merge"; merge_pad[label="--------------------", style=invis] rank=same; merge_41; merge_pad; merge_42 rank=same; merge_41; merge_42; merge_pad; merge_22 -> merge_pad[style=invis] merge_23 -> merge_pad[style=invis] merge_pad -> result[style=invis] merge_21 -> merge_41 merge_22 -> merge_41 merge_23 -> merge_42 merge_24 -> merge_42 merge_41 -> result merge_42 -> result } } ``` **完成排序的佇列:** 此時所有 Node 的 `next` 按照順序連接,但是 `prev` 亂指一通。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold", pos="0,0!"]; e2 [label="<top>bear|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>dolphin|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>vulture|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; e1:left:s -> e4:right:s [color=grey]; e2:left:s -> e4:s [color=grey]; e3:left:s -> e4:sw [color=grey]; e5:left:s -> e4:se [color=grey]; e5:right -> NULL; e1:right -> e2; e2:right -> e3:left; e3:right -> e4:left; e4:right -> e5:left; } ``` **讓佇列恢復成 circular doubly linked list** ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e1 [label="<top>Head |{<left>prev|<right>next}", style="bold", pos="0,0!"]; e2 [label="<top>bear|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>dolphin|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>vulture|{<left>prev|<right>next}", style="bold"]; e1:left:s -> e5:s [color=red]; e2:left -> e1:top [color=red]; e3:left -> e2:top [color=red]; e4:left -> e3:top [color=red]; e5:left -> e4:top [color=red]; e5:right -> e1:s [color=red]; e1:right -> e2:top; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; } ``` ## 研讀 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) **閱讀時間點:** Latest commit [9dbbc3b](https://github.com/torvalds/linux/commit/9dbbc3b9d09d6deba9f3b9e1d5b355032ed46a75) on Jul 8, 2021 `list_sort` 實做為 $2:1$ 的 balanced merge,即,假設現在有兩個長度為 $2^k$ 的 pending sublist (正等著被 merge),如果這兩個 sublist 後面還有 $2^k$ 以上個元素,就馬上 merge 這兩個 sublist 成為 $2^{k+1}$ 長度的 list,如此一來合併後的 list 與剩下的 $2^k$ 個元素數量正好為 $2:1$。 這樣做的好處是,當 cache 容納得下 $3*2^k$ 個元素時,可以避免 cache thrashing。這個實作沒有比 bottom-up merge sort 好,但是可以減少 $0.2*n$ 次比較,所以當 L1 cache 可以容納 $3*2^k$ 個元素時此作法會比較快。 :::info 既然 $2:1$ balanced merge sort 可以減少 $0.2*n$ 次比較,不知道為什麼註解說 bottom-up merge sort 要比 $2:1$ balance merge sort 好,$0.2*n$ 這個數字怎麼來的也不知道。需要計算數學與設計實驗測量 branch、cache miss 數量。 > 快去讀論文! > :notes: jserv ::: 為了找到更詳細的資料,我查看了 list_sort.c 的 commit 紀錄,發現 $2:1$ merge 的實作在 [b5c56e0](https://github.com/torvalds/linux/commit/b5c56e0cdd62979dd538e5363b06be5bdf735a09) 引入,commit 訊息解釋了為何要如此實作,並提供三個參考資料: Bottom-up Mergesort: A Detailed Analysis Wolfgang Panny, Helmut Prodinger Algorithmica 14(4):340--354, October 1995 https://doi.org/10.1007/BF01294131 https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.6.5260 The cost distribution of queue-mergesort, optimal mergesorts, and power-of-two rules Wei-Mei Chen, Hsien-Kuei Hwang, Gen-Huey Chen Journal of Algorithms 30(2); Pages 423--448, February 1999 https://doi.org/10.1006/jagm.1998.0986 https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.4.5380 Queue-Mergesort Mordecai J. Golin, Robert Sedgewick Information Processing Letters, 48(5):253--259, 10 December 1993 https://doi.org/10.1016/0020-0190(93)90088-q https://sci-hub.tw/10.1016/0020-0190(93)90088-Q 稍後詳閱。 ### 參數欄位 ```c= __attribute__((nonnull(2,3))) void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp) { ``` 第一行 `__attribute__((nonnull(2,3)))` 是告訴 gcc 這個函式應該具有什麼屬性,在編譯時期就可以檢查程式碼的正確性,詳情在下方[\_\_attribute\_\_ 的作用](https://hackmd.io/0oQNR91SSRKprDpLbObf6w?view#__attribute__-%E7%9A%84%E4%BD%9C%E7%94%A8)說明。 參數說明: - `@priv`: private data,`list_sort()` 並不會直接操作,只會把 `priv` 傳遞給 `@cmp`,如果 `@cmp` 不需要使用 `priv`,可以直接傳入 `NULL` 作為 `priv`,但是 `@cmp` 仍必須讓第一個參數去接收 `priv`。(可以參考 [linux/drivers/gpu/drm/i915/gvt/debugfs.c](https://github.com/torvalds/linux/blob/57fa2369ab17d67e6232f85b868652fbf4407206/drivers/gpu/drm/i915/gvt/debugfs.c) 如何使用 `list_sort` 以及 `mmio_offset_compare`) - `@head`:要排序的串列的 Head。 - `@cmp`:元素比較函數,為 function pointer,其型態 `list_cmp_func_t` 宣告如下: ```c typedef int __attribute__((nonnull(2,3))) (*list_cmp_func_t)(void *, const struct list_head *, const struct list_head *); ``` `@cmp` 的回傳值必須是 `int` 型態,`@cmp` 的第一個參數(型態為 `void*`)負責接收 `@priv`。第二、三個參數 `@a`、`@b` 為串列的 Node 的 `list_head`。如果 `@a` 應該排在 `@b` 之後,`@cmp` 必須回傳 > 0(例如 `@a > @b`,並且升冪排序);反之,如果 `@a` 應該排在 `@b` 之前(即順序不變),`@cmp` 必須回傳 <= 0。`list_sort` 是 stable sort,因此不需要區分 `@a < @b` 和 `@a == @b` 的情況。 :::info 這裡原文提到 "This is compatible with two styles of @cmp function ... e.g. `plug_ctx_cmp()` in block/blk-mq.c." 但是在 [block/blk-mq.c](https://github.com/torvalds/linux/blob/a12821d5e012a42673f6fe521971f193441d8aa4/block/blk-mq.c) 裡根本沒有 `plug_ctx_cmp()`,我也不知道是什麼機制允許 `@cmp` 為兩個參數的 function pointer,或許我理解錯 "two styles of @cmp function" 的意思? ::: ### 初始化 merge sort ```c=4 struct list_head *list = head->next, *pending = NULL; size_t count = 0; /* Count of pending */ if (list == head->prev) /* Zero or one elements */ return; /* Convert to a null-terminated singly-linked list. */ head->prev->next = NULL; ``` 首先檢查 list 是否至少有兩個 Node,接下來讓最後一個 Node 的 `next` 指向 `NULL`,使其變成 singly linked list。`prev` 指標不再指向前一個 Node,而是另有用途。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold", pos="0,1!"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>...|{<left>|<right>}", style="bold"]; e5 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold", color=red, fontcolor=red]; e3:left -> e2:top; e4:left -> e3:top; e5:left -> e4:top; e5:right -> NULL [color=red]; e2:right -> e3:top; e3:right -> e4:top; e4:right -> e5:top; p [label="pending", style=dashed, color=grey]; p -> NULL:n [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e2:n [style=dashed, color=grey]; } ``` ### 走訪整個 list 同時執行 merge sort ```c=12 do { size_t bits; struct list_head **tail = &pending; /* Find the least-significant clear bit in count */ for (bits = count; bits & 1; bits >>= 1) tail = &(*tail)->prev; /* Do the indicated merge */ if (likely(bits)) { struct list_head *a = *tail, *b = a->prev; a = merge(priv, cmp, b, a); /* Install the merged result in place of the inputs */ a->prev = b->prev; *tail = a; } /* Move one element from input list to pending */ list->prev = pending; pending = list; list = list->next; pending->next = NULL; count++; } while (list); ``` 參照上方 do while 迴圈與註解可以看出 merge sort 實做的一些想法: - `pending` 是已經排序好但尚未被 merge 的 sublists 的 list,亦稱為 "list of list"。 - 每個排序好的 sublist 的長度正好為 $2^k$。 `bits` 用於判斷何時必須 merge 相鄰的若干個 Node,其目的是檢查目前的 sublist 是否湊滿 $2^k$ 個 Node,若目前有 $2^k$ 個 Node,`if(likely(bits))` 就會成立(`likely()` 用於優化編譯後的組合語言,稍後在[ likely 與 unlikely 巨集](https://hackmd.io/0oQNR91SSRKprDpLbObf6w?view#likely-%E8%88%87-unlikely-%E5%B7%A8%E9%9B%86)解釋),並呼叫 `merge` 來合併最新的兩個 pending sublists,然後讓 `list` 指向下一個 Node;若不到 $2^k$ 能 `merge`,就只讓 `list` 指向下一個 Node。 `for (bits = count; bits & 1; bits >>= 1)` 會持續將 `bits` 右移,直到遇到由右向左數來的的第一個 clear bit,若 for 迴圈完畢後 `bits` 仍不為 0,就 merge 兩個長度一樣的 pending sublists。只閱讀程式碼太抽象,我列舉 `count` 等於 0 到 11 的狀態: | count(十進位) | count(二進位) | 所有 sublist 的狀態 | | --------------- | ------------------- |:------------------------ | | 0 | $0000$ | [1] | | 1 | $000\color{red}{1}$ | [1,1] | | 2 (merge) | $0010$ | [1,1,1] -> [2,1] | | 3 | $00\color{red}{11}$ | [2,1,1] | | 4 (merge) | $0100$ | [2,1,1,1] -> [2,2,1] | | 5 (merge) | $010\color{red}{1}$ | [2,2,1,1] -> [4,1,1] | | 6 (merge) | $0110$ | [4,1,1,1] -> [4,2,1] | | 7 | $0\color{red}{111}$ | [4,2,1,1] | | 8 (merge) | $1000$ | [4,2,1,1,1] -> [4,2,2,1] | | 9 (merge) | $100\color{red}{1}$ | [4,2,2,1,1] -> [4,4,1,1] | | 10 (merge) | $1010$ | [4,4,1,1,1] -> [4,4,2,1] | | 11 (merge) | $10\color{red}{11}$ | [4,4,2,1,1] -> [8,2,1,1] | 上表中第一個欄位是 `count` 在 do while 迴圈中每一次迭代的值(0 到 n-1),後面緊接著 (merge) 代表在該次迭代中有呼叫 `merge` 來合併 sublist。 第二個欄位是以二進位表示 `count`,可以注意到由右到左的連續 set bits 被標記為紅色,代表被 for 迴圈右移而被移除。 第三個欄位為每次 do while 迴圈迭代完成後,所有 pending sublist 的狀態,每個以逗號隔開的數字代表該 sublist 的 Node 數量,所有數字加起來會等於 `count+1`,箭號代表 merge 後 pending lists 變為箭號後的狀態。舉 `count == 5` 為例,`[2,2,1,1]` 代表第一、二個 Node 屬於第一個 sublist,第三、四個 Node 屬於第二個 sublist,第五個 Node 自成第三個 sublist,第六個 Node 自成第四個 sublist。此時因為第一、二個 sublist 長度皆為 $2^1$,且第一、二個 sublist 後面的 Node 數量也為 $2^1$ 個,符合 $2:1$ balanced merge,所以 merge 這兩個 sublist,因此狀態變為 `[4,1,1]`。 解釋完 `bits` 的奧妙機制後,接下來談實作上如何是如何儲存 pending sublist 狀態,最關鍵的手法是利用 `prev` 指向每個 sublist 的第一個 Node。在每次 do while 迭代,`list` 會指向第 `count` 個 Node,`pending` 會指向前一個 Node,indirect pointer `tail` 會指向 `&pending` 並在 `bits` 向右移時,一直指向 `tail = &(*tail)->prev` 藉以把自己移動到可能將被 merge 的 sublist,在 sublist 被 merge 後更新 `prev`。 圖示: **執行完 count = 0 的迭代:** Node 1 自成一個 sublist,所以所有 sublists 的狀態為 `[1]`。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>Node 3|{<left>prev|<right>next}", style="bold"]; e5 [label="<top>...|{<left>|<right>}", style="bold"]; e6 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; NULL [label=NULL, style="bold"]; subgraph cluster_0 { style=filled; color="#a6cee3"; label="sublist 1"; e2; } e2:left -> NULL [color=red]; e3:left:n -> e2:top; e4:left:n -> e3:top; e5:left:n -> e4:top; e6:left:n -> e5:top; e2:right -> NULL [color=red]; e3:right -> e4; e4:right -> e5; e5:right -> e6; e6:right:s -> NULL:sw; p [label="pending", style=dashed, color=grey]; p -> e2:nw [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e3:nw [style=dashed, color=grey]; } ``` **count = 1** 此時 Node 1 自成一個 sublist、Node 2 也自成一個 sublist,sublists 的狀態為 `[1,1]`。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>Node 3|{<left>prev|<right>next}", style="bold"]; e5 [label="<top>...|{<left>|<right>}", style="bold"]; e6 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; NULL [label=NULL, style="bold"]; subgraph cluster_0 { style=filled; color="#a6cee3"; label="sublist 1"; e2; } subgraph cluster_1 { style=filled; color="#a6cee3"; label="sublist 2"; e3; } e2:left -> NULL; e3:left:n -> e2 [color=red]; e4:left:n -> e3; e5:left:n -> e4; e6:left:n -> e5; e2:right -> NULL; e3:right -> NULL:sw [color=red]; e4:right -> e5; e5:right -> e6; e6:right -> NULL; p [label="pending", style=dashed, color=grey]; p -> e3:nw [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e4:n [style=dashed, color=grey]; } ``` **count = 2** 此時 Node 3 也自成一個 sublist,sublists 的狀態為 `[1,1,1]`。我們發現 Node 1 與 Node 2 為首的 sublist 長度都為 $2^0$,且後面有一個 Node 3,形成 $2:1$,我們 merge 以 Node 1 和 Node 2 為首的 sublists,讓狀態變成 `[2,1]`。長度為 2 的 sublist 即為下圖綠色區域,Node 1、Node 2 是排序好的 singly linked list。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; e2 [label="<top>Node 1|{<left>prev|<right>next}", style="bold"]; e3 [label="<top>Node 2|{<left>prev|<right>next}", style="bold"]; e4 [label="<top>Node 3|{<left>prev|<right>next}", style="bold"]; e5 [label="<top>...|{<left>|<right>}", style="bold"]; e6 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; NULL [label=NULL, style="bold"]; subgraph cluster_0 { style=filled; color="#b2df8a"; label="sublist 1"; e2;e3; } subgraph cluster_1 { style=filled; color="#a6cee3"; label="sublist 2"; e4; } e2:left -> NULL [color=red]; e4:left:n -> e2 [color=red]; e5:left:n -> e4; e6:left:n -> e5; e2:right -> e3 [color=red]; e3:right -> NULL; e4:right -> NULL [color=red]; e5:right -> e6; e6:right -> NULL; p [label="pending", style=dashed, color=grey]; p -> e4:nw [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e5:n [style=dashed, color=grey]; } ``` **count = 3** sublists 的狀態為 `[2,1,1]`。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; l1 [label="<top>Node 1, 2|{<left>|<right>}", style="bold"]; e4 [label="<top>Node 3|{<left>prev|<right>next}", style="bold"]; e5 [label="<top>Node 4|{<left>prev|<right>next}", style="bold"]; e6 [label="<top>...|{<left>|<right>}", style="bold"]; e7 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; NULL [label=NULL, style="bold"]; subgraph cluster_0 { style=filled; color="#b2df8a"; label="sublist 1"; l1; } subgraph cluster_1 { style=filled; color="#a6cee3"; label="sublist 2"; e4; } subgraph cluster_2 { style=filled; color="#a6cee3"; label="sublist 3"; e5; } l1:right -> NULL; l1:left -> NULL; e4:left:n -> l1; e5:left:n -> e4 [color=red]; e6:left:n -> e5; e7:left:n -> e6; e4:right -> NULL; e5:right -> NULL [color=red]; e6:right -> e7; e7:right -> NULL; p [label="pending", style=dashed, color=grey]; p -> e5:nw [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e6:nw [style=dashed, color=grey]; } ``` **count = 4** sublist 2 與 sublist 3 長度都為 $2^0$,且後面有 Node 5 形成 $2:1$,我們 merge sublist 2 和 sublist 3,讓狀態由 `[2,1,1,1]` 變成 `[2,2,1]`。 ```graphviz digraph ele_list { rankdir=LR; node[shape=record]; l1 [label="<top>Node 1, 2|{<left>|<right>}", style="bold"]; e4 [label="<top>Node 3|{<left>prev|<right>next}", style="bold"]; e5 [label="<top>Node 4|{<left>prev|<right>next}", style="bold"]; e6 [label="<top>Node 5|{<left>prev|<right>next}", style="bold"]; e7 [label="<top>...|{<left>|<right>}", style="bold"]; e8 [label="<top>Node N|{<left>prev|<right>next}", style="bold"]; NULL [label=NULL, style="bold"]; NULL [label=NULL, style="bold"]; subgraph cluster_0 { style=filled; color="#b2df8a"; label="sublist 1"; l1; } subgraph cluster_1 { style=filled; color="#b2df8a"; label="sublist 2"; e4;e5; } subgraph cluster_2 { style=filled; color="#a6cee3"; label="sublist 3"; e6; } l1:right -> NULL; l1:left -> NULL; e4:left:s -> l1 [color=red]; e6:left:n -> e4:w [color=red]; e7:left:n -> e6; e8:left:n -> e7; e4:right -> e5; e5:right -> NULL; e6:right -> NULL [color=red]; e7:right -> e8; e8:right -> NULL; p [label="pending", style=dashed, color=grey]; p -> e6:nw [style=dashed, color=grey]; l [label="list", style=dashed, color=grey]; l -> e7:nw [style=dashed, color=grey]; } ``` **count = 5...N** 省略 由上方的圖示可以看出 `prev` 串連了每個 sublist 的第一個 Node,透過 `tail = &pending` 以及執行若干次 `tail = &(*tail)->prev` 定位到指定的 sublist。 ### merge 剩餘的 sublist,並重建構 `prev`,恢復成 circular doubly-linked list 前面的 do while 迴圈執行結束後會留下許多長度為 $2^k$ 且由大到小排序的 pending sublist,舉 n = 15 為例,do while 結束後會留下 [8,4,2,1] 4 個 sublist,此時我們需要透過以下 for 迴圈達成:[8,4,2,1] -> [8,4,3] -> [8,7]。最後呼叫 `merge_all` 來合併剩餘的 [8,7],並且將整個 list 恢復成 circular doubly linked list。 ```c=35 /* End of input; merge together all the pending lists. */ list = pending; pending = pending->prev; for (;;) { struct list_head *next = pending->prev; if (!next) break; list = merge(priv, cmp, pending, list); pending = next; } /* The final merge, rebuilding prev links */ merge_final(priv, cmp, head, pending, list); } ``` ### `__attribute__` 的作用 前面閱讀 `list_sort` 原始碼時,在函數的開頭使用了 `__attribute__((nonnull(2,3)))`。為了釐清它的作用,尋找第一手材料,首先進入 gcc 9.x 的 [onlinedocs](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/),找到第 6 章 [Extensions to the C Language Family](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/C-Extensions.html#C-Extensions),找到 [6.33 Declaring Attributes of Functions](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Function-Attributes.html#Function-Attributes): 在 GNU C 和 C++ 中,您可以使用 Function Attributes(以下簡稱"屬性")為函式指定某些屬性,這些屬性可以幫助編譯器優化函式,或更仔細地檢查程式碼的正確性。您還可以使用屬性來控制函式的 memory placement、code generation options 和 call/return conventions。許多 attribute 是 target-specific 的,例如許多 target 支援定義 interrupt handler functions 的屬性,這些函式通常必須使用特定的 register 來執行和回傳,此類屬性與其對應的 target 將在 6.33 的每個小節描述。但是大部分 target 都支援相當多屬性了,這些通用的屬性在 [6.33.1 Common Function Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Common-Function-Attributes.html#Common-Function-Attributes) 描述。 屬性由函數宣告中的 `__attribute__` 關鍵字引入,接著用雙括號括起想要使用的屬性,若想要指定多個屬性,就在雙括號內用逗點分隔它們,或者在一個屬性之後緊跟另一個。有關屬性語法的確切規則,請參閱 [6.39 Attribute Syntax](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Attribute-Syntax.html#Attribute-Syntax)。如果兩個屬性互相不兼容,編譯器會直接忽略屬性並產生警告。 GCC 還支援 [6.34 Variable Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Variable-Attributes.html#Variable-Attributes)、[6.35 Type Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Type-Attributes.html#Type-Attributes)、[6.36 Label Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Label-Attributes.html#Label-Attributes")、[6.37 Enumerator Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Enumerator-Attributes.html#Enumerator-Attributes)、[6.38 Statement Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Statement-Attributes.html#Statement-Attributes)。 讀到這裡我們已經完全瞭解 Function Attributes 的目的與語法,接下來閱讀它如何去搭配 `nonnull` 屬性。在 [6.33.1 Common Function Attributes](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Common-Function-Attributes.html#Common-Function-Attributes) 找到 `nonnull` 的段落:`nonnull` 屬性可以應用於使用至少一個 pointer 作為參數的函式,它表示傳入該欄位的參數必須不為 `NULL`,例如以下函式的宣告: ```c void *my_memcpy (void *dest, const void *src, size_t len) __attribute__((nonnull (1, 2))); ``` 宣告表明第 1、2 個參數不能為 `NULL`,也就是 `dest` 和 `src` 為 non-null。如果編譯器確定在標記為 non-null 的參數槽中傳遞了 `NULL`,並且編譯時啟用了 `-Wnonnull` 選項,就會發出警告,見 [Warning Options](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Warning-Options.html#Warning-Options)。除非啟用了 `-fno-delete-null-pointer-checks` 選項,否則編譯器還可以根據某些 non-null 參數進行優化。此外啟用 `-fisolate-erroneous-paths-attribute` 選項以使 GCC 把任何傳遞 `NULL` 到 non-null 函數的呼叫轉換為 *traps*,請參閱 [Optimize Options](https://gcc.gnu.org/onlinedocs/gcc-9.4.0/gcc/Optimize-Options.html#Optimize-Options)。 :::info 我不知道 GCC 的 "trap" 是什麼東西,查不到解釋。 > 就是你在計算機組織課程學到的 trap, interrupt, exception > :notes: jserv ::: 如果沒有在 `nonnull` 填入 *arg-index*,則所有 pointer 參數都被標記為 non-null,例如: ```c void *my_memcpy (void *dest, const void *src, size_t len) __attribute__((nonnull)); ``` ### likely 與 unlikely 巨集 在 list_sort 實作中,某些 if 條件判斷會被包在 `likely` 與 `unlikely` 巨集中,其宣告在 [linux/compiler.h](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h),這個巨集有兩種實現(摘錄自 compiler.h、compiler_types.h 與 compiler_attributes.h,忽略其餘無關的程式碼)。 實作一: ```c // compiler_attributes.h #define __aligned(x) __attribute__((__aligned__(x))) #define __section(section) __attribute__((__section__(section))) ``` ```c // compiler_types.h struct ftrace_branch_data { const char *func; const char *file; unsigned line; union { struct { unsigned long correct; unsigned long incorrect; }; struct { unsigned long miss; unsigned long hit; }; unsigned long miss_hit[2]; }; }; struct ftrace_likely_data { struct ftrace_branch_data data; unsigned long constant; }; ``` ```c // compiler.h #if defined(CONFIG_TRACE_BRANCH_PROFILING) \ && !defined(DISABLE_BRANCH_PROFILING) && !defined(__CHECKER__) #define likely_notrace(x) __builtin_expect(!!(x), 1) #define unlikely_notrace(x) __builtin_expect(!!(x), 0) void ftrace_likely_update(struct ftrace_likely_data *f, int val, int expect, int is_constant); #define __branch_check__(x, expect, is_constant) ({ \ long ______r; \ static struct ftrace_likely_data \ __aligned(4) \ __section("_ftrace_annotated_branch") \ ______f = { \ .data.func = __func__, \ .data.file = __FILE__, \ .data.line = __LINE__, \ }; \ ______r = __builtin_expect(!!(x), expect); \ ftrace_likely_update(&______f, ______r, \ expect, is_constant); \ ______r; \ }) /* * Using __builtin_constant_p(x) to ignore cases where the return * value is always the same. This idea is taken from a similar patch * written by Daniel Walker. */ #ifndef likely #define likely(x) (__branch_check__(x, 1, __builtin_constant_p(x))) #endif #ifndef unlikely #define unlikely(x) (__branch_check__(x, 0, __builtin_constant_p(x))) #endif ``` 實作二: ```c #else #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) #define likely_notrace(x) likely(x) #define unlikely_notrace(x) unlikely(x) #endif ``` 觀察上述兩種實作,可以得知以下資訊:當 `CONFIG_TRACE_BRANCH_PROFILING` 被定義時才會採用實作一,否則採用實作二。兩種實作的關鍵都是 `__builtin_expect(!!(x), expect);`。 實作一雖然看起寫很多行看起來很複雜,但仔細看看可以發現它比實作二多做了三件事: - 初始化一個 `static struct ftrace_likely_data ______f`,將 `__func__`、`__FILE__`、`__LINE__` 放入 `______f` 中對應的成員。`__aligned` 與 `__section` 是用 macro 再包裝的 [Variable Attributes](https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html#Common-Variable-Attributes)。 - 呼叫 `__builtin_expect(!!(x), expect)` 並把回傳值儲存到 `______r`。 - 呼叫 `ftrace_likely_update(&______f, ______r, expect, is_constant)`,由函式的名子與傳入參數可以得知 `ftrace_likely_update` 用於追蹤每次執行 `likely` 當前所在的函式、原始碼檔名、行號、`__builtin_expect` 的回傳值、預期為 1 或 0、以及傳入的 `x` 是否為常數。 由此推論實作一是用來給開發者分析 likely 與 unlikely 的發生頻率是否如預期所想,畢竟把一個相對不常成立的條件放到 `likely` 會降低效能,有這麼一個 trace 工具很合理,此外 `likely_notrace` 不會追蹤上述資訊。 關於 `__func__`、`__FILE__`、`__LINE__` 參見 [Standard Predefined Macros](https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html#Standard-Predefined-Macros)。關於 `__builtin_constant_p`、`__builtin_expect` 參見 [Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)。 Built-in Function: `long builtin_expect (long exp, long c)` 您可以使用 __builtin_expect 為編譯器提供分支預測的資訊,一般來說您更應該要使用 profile feedback(-fprofile-arcs),因為寫程式的人在預測程式實際執行方式出了名的不準。`builtin_expect` 的回傳值是 `exp` 的值,必須是一個 integral expression,並且預期 `exp == c`。 ```c if (__builtin_expect (x, 0)) foo (); ``` 上方程式碼表示 `x` 較可能為 0,因此 `foo` 不應該常被執行。 ```c if (__builtin_expect (ptr != NULL, 1)) foo (*ptr); ``` 若想要判斷浮點數,可以改用上方的寫法,判斷浮點數的指標是否為 `NULL`。 期望 `__builtin_expect` 的 `exp` 為 1 的機率由 GCC 的 `builtin-expect-probability` 參數控制,預設為 90%。您還可以使用 `__builtin_expect_with_probability` 自行指定機率。如果在迴圈內使用此 built-in,則機率會影響優化過後的迭代次數。 ## 引入 list_sort.c 到 lab-0 我在 queue.c 中使用前置處理器來切換自己實做的排序與 list_sort。如果有 `#define USE_LINUX_LIST_SOR`,就會編譯 `#ifdef` 和 `#else` 之間的程式碼;否則編譯 `#else` 和 `#endif` 之間的程式碼。詳細在:[blueskyson/lab0-c@24782f7](https://github.com/blueskyson/lab0-c/commit/24782f71f33e3f7c4d3abd1d62f64464286f281d)。 我修改後的 queue.c 如下: ```c // in queue.c #define USE_LINUX_LIST_SORT // use linux list_sort #ifdef USE_LINUX_LIST_SORT #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) typedef int __attribute__((nonnull(2, 3))) (*list_cmp_func_t) (); __attribute__((nonnull(2, 3, 4))) static struct list_head *merge() __attribute__((nonnull(2, 3, 4, 5))) static void merge_final() __attribute__((nonnull(2, 3))) void list_sort() // custom compare function static int compare() // call list_sort void q_sort(struct list_head *head) { if (!head) return; list_sort(NULL, head, compare); } #else // do top down merge sort void sort_recur(struct list_head **phead) // my merge sort implementation void q_sort(struct list_head *head) #endif ``` ## 比較自己的 sort 與 list_sort 效能落差 打算用 `perf `測試排序 100、1k、10k、100k、1000k 個 Node 的 branches, instructions, context-switches。每筆測試使用 `perf` 重複 10 次,最後畫成表格或折線圖分析我的 sort 與 list_sort 的差異。 使用 `perf` 有個缺點,`sort` 前後的 `ih RAND number` 和 `free` 的過程也會被一併記錄下來,所以用 `perf` 只能看出誰好誰壞,無法單獨計算 `q_sort` 函式耗用的資源。 ### 測試腳本 參考 [Linux 效能分析工具: Perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) 來設定 `perf`。 在 lab0-c 新增 perf-traces 目錄,目錄結構如下: ``` perf-traces/ ├── 1000000.cmd ├── 100000.cmd ├── 10000.cmd ├── 1000.cmd └── 100.cmd ``` `num.cmd` 代表排序 `num` 個隨機字串,例如: ```cmd # 100.cmd, sort 100 RAND strings option fail 0 option malloc 0 new ih RAND 100 sort free ``` 接下來我寫了一個 bash script 來執行效能測試,這個腳本會讓 `perf` 對每個 `qtest -v 0 -f perf-traces/$i.cmd` 重複執行十次,並將輸出儲存在 `perf-traces/$i_report`。腳本內容如下: ```bash # perf-test.sh declare -a traces=("100" "1000" "10000" "100000" "1000000") for i in "${traces[@]}" do perf stat --repeat 10 -o perf-traces/"$i"_report \ -e branches,instructions,context-switches \ ./qtest -v 0 -f perf-traces/"$i".cmd done ``` 詳細在:[blueskyson/lab0-c@299c0b2](https://github.com/blueskyson/lab0-c/commit/299c0b2c0ca3394a3a00b97515b670e5ad8ec32a)。 接下來執行: ```bash $ make $ ./perf-test.sh ``` 就能得到類似以下的報告: ``` Performance counter stats for './qtest -v 0 -f perf-traces/100.cmd' (10 runs): 28,4176 branches ( +- 0.56% ) 141,1366 instructions # 1.14 insn per cycle ( +- 0.57% ) 0 context-switches 0.0005918 +- 0.0000304 seconds time elapsed ( +- 5.14% ) ``` 最後我將報告整理成表格。 ### 用 perf 測量效能 **我實作的 sort** | node num | time (ms) | branches | instructions | contex-switches | |:-------- | --------------- | --------- | ------------ | --------------- | | 100 | 0.5918 (±5.14%) | 284176 | 1411366 | 0 | | 1000 | 0.8602 (±0.72%) | 758525 | 3574613 | 0 | | 10000 | 5.5165 (±1.43%) | 5830672 | 26608412 | 0 | | 100000 | 77 (±2.19%) | 59741126 | 270658689 | 1 | | 1000000 | 1195 (±0.34%) | 630086164 | 2844184459 | 39 | **list_sort** | node num | time (ms) | branches | instructions | contex-switches | |:-------- | --------------- | --------- | ------------ | --------------- | | 100 | 0.5774 (±4.01%) | 285063 | 1419843 | 0 | | 1000 | 0.8351 (±1.16%) | 747332 | 3557516 | 0 | | 10000 | 4.9556 (±0.27%) | 571656 | 26554288 | 0 | | 100000 | 67 (±1.32%) | 58440294 | 270425912 | 5 | | 1000000 | 1012 (±0.52%) | 614368417 | 2840187614 | 34 | 由上兩表可以看出 `list_sort` 在任何情況下,平均執行時間 (上表的 time (ms)) 都比較少,在 node num 為 100 以外的情況下 branch 和 instructions 也都比較低。這說明了 list_sort 在 node num 大於 100 時使用比我的 merge sort 還要少的指令、更快的完成排序。 ### 測量 `q_sort` 的 Wall Clock Time 因為我想得知 `q_sort` 函式在這兩種實作確切的執行時間,我將測試用的 cmd 檔的 `sort` 改成 `time sort`。並且修改 console.c 的 `do_time` 使得顯示的執行時間顯示到小數點後第六位: ```c // in console.c do_time() report(1, "%.6f", delta); // %.3f to %.6f ``` 然後執行以下 bash 腳本: ```bash # perf-test.sh declare -a traces=("200000" "400000" "600000" "800000" "1000000") for i in "${traces[@]}" do ./qtest -v 1 -f perf-traces/"$i".cmd > perf-traces/dump.txt done ``` 得到一連串執行時間: ```bash $ ./perf-test.sh 0.121041 0.355911 0.483806 0.7128ㄕ 0.934433 ``` 畫成表格: | node num | my sort time | list_sort time | list_sort time 是 my sort 的多少 | |:-------- | ------------ | -------------- | -------------------------------- | | 200k | 121.04 | 108.78 | 89.87% | | 400k | 355.91 | 266.27 | 74.81% | | 600k | 483.81 | 423.91 | 87.61% | | 800k | 712.89 | 636.96 | 89.34% | | 1000k | 934.43 | 761.94 | 81.54% | 將兩者的執行時間畫成圖表: :::spoiler python script ```python import plotly.express as px import plotly.graph_objs as go def readfile(fname): arr = [] file = open(fname, "r") while True: line = file.readline() if not line: break arr.append(round(float(line) * 1000, 2)) return arr fig = go.Figure() fig.update_layout( title="Execution Time", xaxis_title="node number", yaxis_title="time (ms)", template="plotly_white" ) xtics = [200000, 400000, 600000, 800000, 1000000] arr1 = readfile("dump.txt") arr2 = readfile("dump1.txt") fig.add_trace(go.Scatter(x=xtics, y=arr1, mode='markers+text', \ text=arr1, textposition="top right", name="my sort")) fig.add_trace(go.Scatter(x=xtics, y=arr2, mode='markers+text', \ text=arr2, textposition="bottom right", name="list_sort")) fig.show() ``` ::: ![](https://i.imgur.com/d5ikDqk.png) 接下來我把 sort 的所有執行時間同除以 node 數量為 200k 的執行時間,讓第一次執行時間固定為 1.0,然後與 $O(Nlog_2N)$、$O(n)$ 的圖表比較: :::spoiler python script ```python base1 = arr1[0] base2 = arr2[0] for i in range(5): arr1[i] = round(arr1[i] / base1, 2) arr2[i] = round(arr2[i] / base2, 2) fig = go.Figure() fig.update_layout( title="Execution Time", xaxis_title="node number", yaxis_title="time (ms)", template="plotly_white" ) fig.add_trace(go.Scatter(x=xtics, y=arr1, mode='markers+text', \ text=arr1, textposition="top right", name="my sort")) fig.add_trace(go.Scatter(x=xtics, y=arr2, mode='markers+text', \ text=arr2, textposition="bottom right", name="list_sort")) nlog2n = [i * math.log(i, 2) for i in range(1, 11)] fig.add_trace(go.Scatter(x=xtics, y=nlog2n, mode='lines', \ line=dict(dash='dash'), name="O(n * log2 n)")) fig.add_trace(go.Scatter(x=xtics, y=list(range(1,6)), mode='lines', \ line=dict(dash='dash'), name="O(n)")) fig.show() ``` ::: ![](https://i.imgur.com/8JEWB8h.png) ## 在 qtest 提供新的命令 shuffle 首先我觀察 qtest 添加指令的機制,發現 `ADD_COMMAND` 巨集定義如下: ```c #define ADD_COMMAND(cmd, msg) add_cmd(#cmd, do_##cmd, msg) ``` 前置處理器會把 `cmd` 字串串接為 "do_"`cmd`,自動產生 do_ 開頭的函式名稱,透過 `add_cmd` 將這個 do_ 函式和函式說明串接到 console.c 的 `cmd_list`。我在 qtest.c 的 `console_init` 中添加以下程式碼,並且寫了一個 `do_shuffle` 函式: ```c bool do_shuffle() { return show_queue(3); } static void console_init() { ADD_COMMAND(shuffle, "| Do Fisher-Yates shuffle"); } ``` 這樣便新增了一個 `shuffle` 指令,功能與 `show` 一樣。接下來模仿 `do_sort`,在執行 shuffle 之前檢查佇列並發出警告訊息,再執行 `q_shuffle`。 ```c bool do_shuffle() { if (!l_meta.l) report(3, "Warning: Calling shuffle on null queue"); error_check(); int cnt = q_size(l_meta.l); if (cnt < 2) report(3, "Warning: Calling shuffle on single node"); error_check(); q_shuffle(l_meta.l); show_queue(3); return !error_check(); } ``` ### `q_shuffle` 實作 我的想法是按照 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm) 提供的演算法: ```bash -- To shuffle an array a of n elements (indices 0..n-1): for i from n−1 downto 1 do j ← random integer such that 0 ≤ j ≤ i exchange a[j] and a[i] ``` 我一開始想透過操作上述 pseudo code 的 `a[j]` 和 `a[i]` 的 `prev` 與 `next` 來達成 shuffle,但發現如果 `i == j + 1` 時會有例外狀況需要另行處理。後來想到可以直接對調 `a[i]` 和 `a[j]` 的 `value` 就好,這樣就不用判斷例外,而且可以可以用較少的指標操作(完全不用 `list_add`、`list_del`)完成交換。 此外我參考 [XOR Linked List](https://hackmd.io/@kenjin/xor_llist#C-Implementation) 定義 `XOR` 巨集來交換 `value` 指向的位址,只是覺得這樣比較帥,不知道與宣告 `char *tmp` 比起來有哪些優缺點。詳細變更見:[blueskyson/lab0-c@10acbd9](https://github.com/blueskyson/lab0-c/commit/10acbd9fde08ce5cad4a648a905ef5fbbd5f6728)。 ```c #include <stdint.h> /* For swapping two strings in q_shffle */ #define XOR(a, b) (char *)((intptr_t)(a)^(intptr_t)(b)) void q_shuffle(struct list_head *head) { int len = q_size(head); if (len < 2) return; for (struct list_head *p1 = head->prev; len > 1; len--, p1 = p1->prev) { int n = rand() % len; if (n == len - 1) continue; struct list_head *p2 = head->next; for (int i = 0; i < n; i++, p2 = p2->next); char **v1 = &list_entry(p1, element_t, list)->value; char **v2 = &list_entry(p2, element_t, list)->value; *v1 = XOR(*v1, *v2); *v2 = XOR(*v1, *v2); *v1 = XOR(*v1, *v2); } } ``` ## 用 Valgrind 排除 qtest 實作的記憶體錯誤 執行 `make valgrind` 後,並沒有顯示任何 memory leak 的報告,輸出如下: ```bash $ make valgrind # Explicitly disable sanitizer(s) make clean SANITIZER=0 qtest make[1]: Entering directory '/home/lin/Desktop/sysprog2022/lab0-c' rm -f qtest.o report.o console.o harness.o queue.o random.o dudect/constant.o dudect/fixture.o dudect/ttest.o linenoise.o .qtest.o.d .report.o.d .console.o.d .harness.o.d .queue.o.d .random.o.d .dudect/constant.o.d .dudect/fixture.o.d .dudect/ttest.o.d .linenoise.o.d *~ qtest /tmp/qtest.* rm -rf .dudect rm -rf *.dSYM (cd traces; rm -f *~) CC qtest.o CC report.o CC console.o CC harness.o CC queue.o CC random.o CC dudect/constant.o CC dudect/fixture.o CC dudect/ttest.o CC linenoise.o LD qtest make[1]: Leaving directory '/home/lin/Desktop/sysprog2022/lab0-c' cp qtest /tmp/qtest.scGuNm chmod u+x /tmp/qtest.scGuNm sed -i "s/alarm/isnan/g" /tmp/qtest.scGuNm scripts/driver.py -p /tmp/qtest.scGuNm --valgrind --- Trace Points +++ TESTING trace trace-01-ops: # Test of insert_head and remove_head --- trace-01-ops 5/5 +++ TESTING trace trace-02-ops: # Test of insert_head, insert_tail, remove_head, remove_tail, and delete_mid --- trace-02-ops 6/6 +++ TESTING trace trace-03-ops: # Test of insert_head, insert_tail, reverse, and remove_head --- trace-03-ops 6/6 +++ TESTING trace trace-04-ops: # Test of insert_head, insert_tail, size, swap, and sort --- trace-04-ops 6/6 +++ TESTING trace trace-05-ops: # Test of insert_head, insert_tail, remove_head, reverse, size, swap, and sort --- trace-05-ops 6/6 +++ TESTING trace trace-06-ops: # Test of insert_head, insert_tail, delete duplicate, and sort --- trace-06-ops 6/6 +++ TESTING trace trace-07-string: # Test of truncated strings --- trace-07-string 6/6 +++ TESTING trace trace-08-robust: # Test operations on empty queue --- trace-08-robust 6/6 +++ TESTING trace trace-09-robust: # Test remove_head with NULL argument --- trace-09-robust 6/6 +++ TESTING trace trace-10-robust: # Test operations on NULL queue --- trace-10-robust 6/6 +++ TESTING trace trace-11-malloc: # Test of malloc failure on insert_head --- trace-11-malloc 6/6 +++ TESTING trace trace-12-malloc: # Test of malloc failure on insert_tail --- trace-12-malloc 6/6 +++ TESTING trace trace-13-malloc: # Test of malloc failure on new --- trace-13-malloc 6/6 +++ TESTING trace trace-14-perf: # Test performance of insert_tail, reverse, and sort --- trace-14-perf 6/6 +++ TESTING trace trace-15-perf: # Test performance of sort with random and descending orders # 10000: all correct sorting algorithms are expected pass # 50000: sorting algorithms with O(n^2) time complexity are expected failed # 100000: sorting algorithms with O(nlogn) time complexity are expected pass --- trace-15-perf 6/6 +++ TESTING trace trace-16-perf: # Test performance of insert_tail --- trace-16-perf 6/6 +++ TESTING trace trace-17-complexity: # Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant Probably constant time ERROR: Probably not constant time Probably constant time ERROR: Probably not constant time --- trace-17-complexity 0/5 --- TOTAL 95/100 make: *** [Makefile:68: valgrind] Error 1 ``` 我再參考 [2020leon](https://hackmd.io/@6649/linux2022-lab0#%E4%BF%AE%E6%AD%A3%E9%8C%AF%E8%AA%A4%EF%BC%88-Valgrind-%EF%BC%89),執行 `valgrind ./qtest -f ./traces/trace-eg.cmd` 也完全沒有如預期輸出任何錯誤訊息。 接著我再按照 lab-0 的作業說明,測試了一個明顯會 memory leak 的程式: ```c // test valgrind #include <stdlib.h> void func(void) { char *buff = malloc(10); } int main(void) { func(); return 0; } ``` 輸出顯示 valgrind 是正常的,可以抓出 memory leak。 ``` $ gcc -g test.c $ valgrind -q --leak-check=full ./a.out ==14940== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==14940== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==14940== by 0x10915E: func (test.c:3) ==14940== by 0x109172: main (test.c:6) ==14940== ``` 為何沒有任何 memory leak 還需要再探討。 ### Massif 視覺化 ``` $ valgrind --tool=massif ./qtest -f traces/trace-01-ops.cmd $ massif-visualizer massif.out.* ``` ![](https://i.imgur.com/J2OYPfQ.png)