--- tags: linux kernel --- # 2023q1 Homework1 (lab0) contributed by < `hongpei100` > ## 實驗環境 ```shell $ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 Copyright (C) 2021 Free Software Foundation, Inc. $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz CPU family: 6 Model: 158 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 10 CPU max MHz: 4000.0000 CPU min MHz: 800.0000 BogoMIPS: 4599.93 ``` :::warning 在終端機執行 `export LC_ALL=C`,再執行上述命令。`lscpu` 套件目前的中文翻譯品質不佳。 :notes: jserv > :bulb: 已使用上述<s>指令</s> 命令,將 `lscpu` 命令的語言從中文敘述轉成英文敘述 > 注意[資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary),command 的翻譯是「命令」。 ::: ## Lab0 作業要求 [作業要求](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-a) ## 開發流程紀錄 ### 了解 Linux 內部資料結構 透過閱讀 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list) : ![](https://i.imgur.com/gHQncR5.png) ![](https://i.imgur.com/Mg8GEIN.png) 透過上面兩張圖可以發現,資料結構為 Circular doubly linked list,並且有以下觀察 : 1. List 的 `head` 不存資料,而是透過 `next` 指向第一個有存資料的節點 2. 當一個 list 為 empty 狀態,它的 `head->next` 會等於 `head` 並且可透過 Linux 提供的巨集,來簡化程式碼的維護。 ### 針對佇列的操作修改程式碼 #### q_new 1. 透過 malloc 函式,分配一塊大小為 `struct list_head` 的記憶體給變數 `q` 2. 確認佇列 `q` 有被成功分配到記憶體,並透過 `INIT_LIST_HEAD` 對佇列做初始化 :::success :bulb: `INIT_LIST_HEAD` 會將 `head` 的 `prev` 與 `next` 指標指向 `head` ::: ```c /* Create an empty queue */ struct list_head *q_new() { struct list_head *q = malloc(sizeof(struct list_head)); if (!q) return NULL; INIT_LIST_HEAD(q); return q; } ``` :::warning 1. 注意程式碼風格規範 2. `malloc` 不需要額外的轉型,亦即 `(struct list_head *) malloc(...)` 非必要 :notes: jserv ::: :::success 1. 已透過命令 `clang-format -i queue.c` 來排版,符合程式碼風格規範 2. 已將 malloc 轉型移除 ::: #### q_free 在實作清除佇列記憶體時,回想起教材內部有針對 linked list 的 `NULL` 與 `empty` 狀態做討論,並且為了盡量減少分支: 1. linked list 為 `NULL` 或者是 `empty`,會直接回傳 2. 若 linked list 有帶資料的節點存在,則以巨集 `list_for_each_entry_safe` 對 list 走訪,並參考 [kdnvt 學員 的 code review](https://hackmd.io/@kdnvt/linux2022_lab0),得知可以善用 `queue.h` 內部提供的 `q_release_element` 來移除 linked list element ```c /* Free all storage used by queue */ void q_free(struct list_head *l) { if (l && !list_empty(l)) { element_t *entry = NULL, *safe = NULL; list_for_each_entry_safe (entry, safe, l, list) q_release_element(entry); free(l); } } ``` #### q_insert_head 在實作這部份時,發現沒有提供函式來新增 linked list element,故先在 `queue.c` 當中,定義一個 `q_new_ele` 的函式來創造新的節點 : ```c /* Create a new element to put into list */ element_t *q_new_ele(char *s) { if (!s) return NULL; element_t *new_ele = malloc(sizeof(element_t)); if (!new_ele) return NULL; INIT_LIST_HEAD(&new_ele->list); int len = strlen(s) + 1; new_ele->value = malloc(sizeof(char) * len); if (!new_ele->value) { free(new_ele); return NULL; } strncpy(new_ele->value, s, len); new_ele->value[len - 1] = '\0'; return new_ele; } ``` :::success :bulb: 記得複製長度為 `n` 的字串的時候,記憶體要分配 `n + 1` 個空間,留一個空間給 `\0` ::: 有了上述函式,則可實作 `LIFO` 的節點插入 : 1. 若 linked list 為 NULL 就先回傳 2. 透過 `q_new_ele` 創立一個節點,並觀察是否為 NULL 以確認創建成功 3. 最後透過巨集 `list_add` 來將節點放至 linked list 的開頭 :::success :bulb: 觀察 `list_add` 的函式,可知函式參數兩者皆為 `struct list_head*`,因此第一個參數放入的是節點 `new_node` 底下的 `list` ::: ```c /* Insert an element at head of queue */ bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *new_node = q_new_ele(s); if (!new_node) return false; list_add(new_node->list, head); return true; } ``` #### q_insert_tail 跟 `q_insert_head` 的概念相似,可重複利用新增節點的函式 `q_new_ele`,並透過巨集 `list_add_tail` 來將節點插入尾端. ```c /* Insert an element at tail of queue */ bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; element_t *new_node = q_new_ele(s); if (!new_node) return false; list_add_tail(new_node->list, head); return true; } ``` #### q_remove_head 這邊提供三個函式參數,在閱讀 `queue.h` 此函式的解釋後,知道可利用 `buf_size - 1` 來複製第一個 entry 的字串到 `sp` 內。 而透過 `list.h` 可找到巨集 `list_del_init` 來輔助 `remove` element 步驟如下 : 1. 若 linked list 為 `NULL` 或是 `empty`,就回傳 `NULL`,表示沒有任何被移除的節點 2. 若 linked list 有帶資料的節點存在,則分別利用 `head->next` 和 `list_first_entry` 來取得 linked list 開頭的 element 和該元素封裝的 entry 3. 利用 `strndup` 做字串複製到 `sp`,避免 `sp` 沒有被分配到記憶體 4. 最後透過 `list_del_init` 將第一個節點從 linked list 移除 5. 回傳移除節點的指標 :::success :bulb: 不論 `sp` 是否為 NULL,仍然要把該資料節點 remove ::: ```c /* Remove an element from head of queue */ element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; struct list_head *r_node = head->next; element_t *r_ele = list_first_entry(head, element_t, list); int min_len = 0; if (sp && bufsize) { min_len = strlen(r_ele->value) + 1 > bufsize ? bufsize : strlen(r_ele->value) + 1; strncpy(sp, r_ele->value, min_len); sp[min_len - 1] = '\0'; } list_del_init(r_node); return r_ele; } ``` #### q_remove_tail 跟 `q_remove_head` 的概念相似,差別在於要取的節點為 linked list 尾端節點,因此可透過 `head->prev` 與 `list_last_entry` 取得 linked list 開頭的 element 和該元素封裝的 entry ```c /* Remove an element from tail of queue */ element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; struct list_head *r_node = head->prev; element_t *r_ele = list_last_entry(head, element_t, list); int min_len = 0; if (sp && bufsize) { min_len = strlen(r_ele->value) + 1 > bufsize ? bufsize : strlen(r_ele->value) + 1; strncpy(sp, r_ele->value, min_len); sp[min_len - 1] = '\0'; } list_del_init(r_node); return r_ele; } ``` #### q_size 1. 若 linked list 為 NULL 或者是 empty,就直接回傳 0,表示 linked list 沒有任何帶資料的節點 2. 若 linked list 有帶資料的節點,利用巨集 `list_for_each` 來對 linked list 走訪,計算節點數量 :::warning :bulb: 若 linked list 能夠維護一個整數變數 `cnt`,在插入或刪除節點時更新 `cnt`,這個函式應該能從 `O(n)`變到 `O(1)` > 這樣對每個節點佔用的空間會有顯著衝擊。工程就是一系列的取捨 (trade-off) > :notes: jserv ::: ```c /* Return number of elements in queue */ int q_size(struct list_head *head) { if (!head || list_empty(head)) return 0; int cnt = 0; struct list_head *tmp = NULL; list_for_each (tmp, head) cnt++; return cnt; } ``` #### q_delete_mid 若 linked list 的節點數量為`n`,則一個 linked list 的中間節點,為在 `0-base indexing` 之下的第 `floor(n/2)` 的 element : 1. 藉由 `q_size` 來算出中間節點的 element 為第 `idx` 個 2. 透過巨集 `list_for_each` 走訪 linked list,找到第 `idx` 個 element 3. 再透過巨集 `list_del_init` 來移除中間節點 4. 最後使用巨集 `q_release_element`,來將節點記憶體釋放 :::warning :bulb: 注意到 : 這個函式為 `delete`,但是巨集提供的 `list_del_init` 只會把節點移除,須呼叫<s>巨集</s> `q_release_element` 函式來把節點記憶體空間歸還給作業系統 ::: ```c /* Delete the middle node in queue */ bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; int idx = q_size(head) / 2; int cnt = 0; struct list_head *tmp = NULL; element_t *d_node = NULL; list_for_each (tmp, head) { if (cnt == idx) break; cnt++; } d_node = list_entry(tmp, element_t, list); list_del_init(tmp); q_release_element(d_node); return true; } ``` #### q_delete_dup 這個函式想的比較久,有幾個想法: 令整個鏈結串列有 $n$ 個節點。 1. 直覺使用二層 for 迴圈來尋找重複的節點,時間複雜度是 `O(n^2)` 我試著找尋更有效率的解法來尋找重複的節點,於是想到第二種方法: 2. hash table 的 Chaining :同樣以鏈結串列實作,且對於搜尋單一字串,至多是 `O(n)`。 [ASCII table](https://zh.wikipedia.org/zh-tw/ASCII) 觀察 ASCII table 的字母對應的十進位,可發現最小值為 `32`,最大值為 `126`,因此建立 hash table 並使其具備 $126 - 32 + 1 = 95$ 個 bucket,並使用每個節點字串的第一個字母,來計算 hash value,就可以進行建表與搜尋。 但是在實作到一半的過程發現,我們只有維護一個鏈結串列,若為了要刪除重複資料的節點,需要在走訪鏈結串列過程中,一併建出 hash table。**這樣不僅時間複雜度沒有改善,反而還浪費了空間。** **分析如下 :** 針對 worst case 分析,若當前鏈結串列有 10 個節點,字串各別為 `a` 開頭,且每個字串皆相異。依據上述方法,會建出 1 個 bucket,但有 10 個節點鏈結串列。 在此過程中,若走訪至第一個節點,則需要檢查 bucket 第一個節點 ; 走訪第二個節點,則需要檢查 bucket 前兩個節點 ; 依此類推,故時間複雜度仍然為 `O(n^2)`,且還浪費與原本鏈結串列相同大小的空間 `O(n)` **結論 : 此方法比第一種方法來的更差** 3. 先將 linked list 做排序,在迭代比較相鄰節點的字串,若有重複,就刪除當前走訪到的節點 :::warning :bulb: 此方法是讓 unsorted list 轉變成 sorted list,並走訪整個 linked list,因此只需要花 `O(n^logn)` 即可完成。**若題目嚴謹要求只能在 unsorted list 上操作,則此方法無效,須採用前兩種方法。** ::: :::success :bulb: 最後發現在作業要求與 leetcode 題目說明之下,給定的 linked list 為已經排序的,故選擇第三種方法。 ::: :::success :bulb: 這邊採用 marked 變數來紀錄重複資料節點刪除的狀況: 若有遇到兩兩相鄰節點資料相同,則設立 marked 為 1 ; 反之若資料不相同,則設為 0。這個變數的重點在於 : 當重複資料節點刪除時,遇到 `safe` 繞回 `head`,或者是重複資料節點刪除到只剩下一個的時候,需要把剩下那一個節點也刪除掉,也就是代表 marked 從 1 變回 0。 ::: ```c /* Delete all nodes that have duplicate string */ bool q_delete_dup(struct list_head *head) { if (!head) return false; int marked = 0; element_t *entry = NULL, *safe = NULL; list_for_each_entry_safe (entry, safe, head, list) { if (&safe->list == head) { if (marked) { list_del_init(&entry->list); q_release_element(entry); } break; } if (strcmp(entry->value, safe->value) == 0) { list_del_init(&entry->list); q_release_element(entry); marked = 1; } else { if (marked) { list_del_init(&entry->list); q_release_element(entry); } marked = 0; } } return true; } ``` #### q_swap 根據 leetcode 題目的描述,linked list 的節點交換,不能夠直接交換兩個節點的資料,須更改節點本身的 link。 一開始想到的作法比較直觀 : 利用 `list_for_each(tmp, safe, head, list)` 走訪整個 linked list,並額外使用 `struct list_head *prev, *next` 分別當作 `tmp` 節點的前一個節點,與 `safe` 節點的後一個節點,來做 swap : ```c /* Swap every two adjacent nodes */ void q_swap(struct list_head *head) { if (head && !list_empty(head)) { struct list_head *tmp = NULL, *safe = NULL; list_for_each_safe (tmp, safe, head) { struct list_head *prev = tmp->prev, *next = safe->next; tmp->prev = safe; safe->next = tmp; tmp->next = next; safe->prev = prev; } } } ``` 後來經由觀摩 [kdvnt 的 q_swap 想法](https://hackmd.io/@kdnvt/linux2022_lab0),得知可以善用 `list.h` 裡面的巨集 `list_move_tail` 來調動節點。 但是在這邊思考了一下,有兩個疑惑的點 : 1. 為什麼要用 `list_move_tail` ? 用 `list_move` 一定也能達到相同的操作 的確可以做到,兩者差別在於 : - list_move_tail(current->next, current),current node 放在第二個參數,移動的節點是 current node 的下一個節點; - list_move(current, current->next),current node 放在第一個參數,移動的節點就是 current node :::warning list_move_tail 強調把 current node 當作 head,把其下一個節點,放至 head->prev ; list_move 則強調把 current node 下一個節點當 head,把 current node 放至 head->next; ::: :::warning :bulb: 注意 : 佇列的 head,跟 `list_move` 與 `list_move_tail` 的參數的 head 不能混淆 ::: 2. 為什麼不用 `list_for_each_safe` ? - 因為迴圈中止條件需要多設立一個 `node->next != head`,否則會繼續 swap 綜合以上描述,新的 `q_swap` 函式如下 : ```c /* Swap every two adjacent nodes */ void q_swap(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *cur = NULL; for (cur = head->next; cur != head && cur->next != head; cur = cur->next) list_move_tail(cur->next, cur); } ``` #### q_reverse 想法與 `q_swap` 相似,利用 `list_move`,讓每個 current node 的下一個節點,都被調到 linked list `head` 的下一個節點 ```c /* Reverse elements in queue */ void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *cur = NULL; for (cur = head->next; cur != head && cur->next != head;) list_move(cur->next, head); } ``` #### q_reverse_k 想法與 `q_reverse` 相似,每一次以 k 個節點作為單位,進行反向順序排列。 唯一不同的點在於,當每 k 個節點反向排序完成時,需要更新 `tmp_head` 至當前 k 個節點的最後一個,作為下一群 k個節點的 `head`,才能正確操作 `list_move`。 若走訪到後面,已經剩餘不足 k 個節點,就不需要 reverse。 ```c /* Reverse the nodes of the list k at a time */ void q_reverseK(struct list_head *head, int k) { if (!head || list_empty(head)) return; struct list_head *cur = NULL, *tmp_head = head; int i, j; for (i = 0; i < q_size(head) && q_size(head) - i >= k; i += k) { for (j = 0, cur = tmp_head->next; j < k - 1; j++) list_move(cur->next, tmp_head); tmp_head = cur; } } ``` #### q_sort 根據 [linked list sort](https://npes87184.github.io/2015-09-12-linkedListSort/),這邊採用 Merge sort 來排序 linked list,而 Merge sort 可分成 partition 和 merge 的部份來思考 : 1. Partition : 每一次的 Partition,都會把當前的 linked list 分成兩半,分別為 `left` 與 `right`。這邊透過兩個 `struct list_head*` 的指標,分別為 `slow` 和 `fast`,來決定這一個 linked list 的中間節點,再根據中間節點來分割成兩個 linked list。 在 while 迴圈中,`fast` 每次都會比 `slow` 走的多一個節點,這個代表**每次第一個與第二個 linked list都會各新增一個節點**,直到 `fast` 遇到 `head` :::success :bulb: 不過 `queue.c` 已經有實作 `q_size`,可透過迭代尋找在 0-based indexing 之下的第 `floor(n/2)` 個節點,當作 `slow`,而 `fast = slow->next`。 ::: :::warning :bulb: 當找到第一個與第二個 linked list 的分界點,會發現 Merge sort 在遞迴呼叫時,放入的參數是 `linked list` 的 `head`,其不存放任何資料。此時發現,可使用 `LIST_HEAD` 來產生 `left` 跟 `right` 的 `head` ::: 接著,再透過 `list_cut_position`,把包含 `slow` 以前的 linked list 放入 `left`,剩下的放入 `right` 內,並遞迴的進行 partition。 2. Merge: 不斷分別比較第一個與第二個 `list` 內的節點資料大小,並呼叫 `list_move_tail` 來把節點的 `value` 較小者,放入 `head` linked list 的尾端。 等到 `left` 與 `right` 其中一個為 `empty`,就呼叫 `list_splice_tail_init` 來把另一個不為 `empty` 的 linked list 內部所有節點,都放到 `head` 的尾端,並呼叫 `free` 來把 `q1` 和 `q2` 的記憶體歸還給作業系統,最後回傳 `head`。注意這個函式不需要在額外 malloc 一塊空間來放資料,而是直接放入 `head` 即可,因為 `head` 傳入時必定為 `empty` 。 ```c void merge(struct list_head *head, struct list_head *q1, struct list_head *q2) { element_t *cur_q1 = NULL, *cur_q2 = NULL; while (!list_empty(q1) && !list_empty(q2)) { cur_q1 = list_entry(q1->next, element_t, list); cur_q2 = list_entry(q2->next, element_t, list); if (strcmp(cur_q1->value, cur_q2->value) <= 0) list_move_tail(q1->next, head); else list_move_tail(q2->next, head); } if (q1 && !list_empty(q1)) list_splice_tail_init(q1, head); if (q2 && !list_empty(q2)) list_splice_tail_init(q2, head); } void q_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *fast = head->next->next, *slow = head->next; // split list while (fast != head && fast->next != head) { slow = slow->next; fast = fast->next->next; } // generate left & right head LIST_HEAD(left); LIST_HEAD(right); list_cut_position(&left, head, slow); list_splice_init(head, &right); q_sort(&left); q_sort(&right); merge(head, &left, &right); } ``` #### q_descend 根據 [leetcode - Remove Nodes From Linked List](https://leetcode.com/problems/remove-nodes-from-linked-list/) 題目可知,對於 linked list 內的每個節點,若當前節點右邊有任意一個節點的值,嚴格大於其本身的值,則當前節點會從 linked list 內被刪除。 由上敘述,可知從 linked list 的最後一個節點開始,依序向左檢驗每一個節點的值,並持續以 `max_ele` 紀錄當前遇到擁有最大 ASCII 值的節點,並刪除字串小於該最大 ASCII 字串的節點,最後再透過 `q_size` 回傳當前的節點數量。 ```c /* Remove every node which has a node with a strictly greater value anywhere to * the right side of it */ int q_descend(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *cur = NULL; element_t *cur_ele = NULL, *max_ele = list_entry(head->prev, element_t, list); for (cur = max_ele->list->prev; cur != head; cur = max_ele->list->prev) { cur_ele = list_entry(cur, element_t, list); if (strcmp(max_ele->value, cur_ele->value) > 0) { list_del_init(cur); q_release_element(cur_ele); } else max_ele = cur_ele; } return q_size(head); } ``` #### q_merge 一開始看到這個函式,非常疑惑為什麼不是給雙重指標,如 `struct lish_ead **head` 的形式,這樣才能存取到每一個 queue 的開頭。 後來經由觀摩 [paul90317](https://hackmd.io/@paul90317/linux2023-spring-lab0) 的 `q_merge`,得知這個 `struct list_head *head` 確實是每一個 queue 的開頭,經由 `queue.h` 內部的下列結構體可知,`head` 就是 `queue_context_t` 結構下的 `struct list_Head *q`。 ```c /** * queue_contex_t - The context managing a chain of queues * @q: pointer to the head of the queue * @chain: used by chaining the heads of queues * @size: the length of this queue * @id: the unique identification number */ typedef struct { struct list_head *q; struct list_head chain; int size; int id; } queue_contex_t; ``` 於是開始思考可以利用 Merge sort 內的 `merge` 函式,每走訪一次所有的 queue,就讓兩兩相鄰的 queue 合併。 想法如下: 1. 若 queue 為 NULL 或 empty,代表沒有任何 queue 有資料,回傳 0 2. 若有多個 queue,令總共要 merge k linked lists : - 總共需要走訪整個 chain `celling(k/2)` 次,由於 C 的整數除法為無條件捨去,因此可改寫成 `(k + 1) / 2` - 每一次走訪需要合併的次數為當前 `queue` 的數量的一半 - 每次利用 `cur` 和 `empty` 當指標走訪所有的 queues。 - `cur` 每一次都會合併當前的 queue 和下一個 queue,放到 `temp` - `empty` 則指向第一個為 empty 的 `queue` ,讓 `cur` 合併好的 `temp` queue 放到 `empty` 3. 在每一次走訪完之後,須確認這次合併的 queue 的數量 : - 若是為偶數,代表這一次走訪,每一個 queue 都有被合併到 - 若是為奇數,代表這一次走訪,還有一個 queue 沒有被合併,需要將其搬到最後一個合併的 queue 的下一個 queue ```c /* Merge all the queues into one sorted queue, which is in ascending order */ int q_merge(struct list_head *head) { if (!head || list_empty(head)) return 0; int k; // Total k queue needs ceiling(n/2) times to merge, which means (k + 1) / 2 for (k = q_size(head); k > 1 ; k = (k + 1) / 2) { struct list_head *cur = head->next, *empty = head->next; // k queue needs k/ 2 times merge for (int i = 0; i < k / 2; i++) { LIST_HEAD(temp); merge(&temp, list_entry(cur, queue_contex_t, chain)->q, list_entry(cur->next, queue_contex_t, chain)->q); list_splice_tail(&temp, list_entry(empty, queue_contex_t, chain)->q); cur = cur->next->next; empty = empty->next; } // if there has odd number queues, put the last queue to the next of the last combined queue if (k % 2) list_splice_tail_init(list_entry(cur, queue_contex_t, chain)->q, list_entry(empty, queue_contex_t, chain)->q); } return q_size(list_entry(head->next, queue_contex_t, chain)->q); } ``` ### 評分結果 目前得到 95 分,剩下 5 分目前無法得知不是 constant time 的原因,在多次重跑的情況之下,`q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head` 皆有出現 `Probably constant time` 過。 :::success +++ 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 ERROR: Probably not constant time or wrong implementation ERROR: Probably not constant time or wrong implementation Probably constant time ERROR: Probably not constant time or wrong implementation --- trace-17-complexity 0/5 --- TOTAL 95/100 ::: ### 開發過程中的疑惑 1. linked list 的結構體宣告如下 : ```c struct list_head { struct list_head *prev; struct list_head *next; }; ``` 而封裝 linked list 的結構 `element_T` 結構體宣告如下 : ```c typedef struct { char *value; struct list_head list; } element_t; ``` 為什麼 `element_t` 內的成員 `list`,不是以指標的形式宣告? 每一個 linked list 的 `prev` 與 `next` 都是指標,那照理來講,`element_t` 的成員 `list` 應該是指標,才能讓其它節點的 `prev` 或 `next` 指向 `list` 這個節點。 --- ### 在 qtest 提供新的命令 shuffle 根據 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法,若資料結構內有 n 個 element,則以**陣列實作**的方法如下 : 1. 總共有 `n` 個 element,代表索引從 `0 ~ n-1` 2. 令 `i` 從陣列索引 `n-1` 至 `0`,不斷從範圍 `[0,i]` 內取出一個隨機數值 `j`,把第 i 個索引元素,跟第 j 個索引元素交換。 > 注意,[0,i] 是包含 0 與 i 索引。 Pseudo code 引用自維基百科: ``` -- 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] ``` 在 linked list 內的 Fisher–Yates shuffle,則可以透過巨集 `list_move tail` 與 `list_move` 的輔助來完成 shuffle,想法如下 : 1. 透過 `q_size` 取得共 `cnt` 個節點,並逐次遞減 `cnt` 2. 令 `safe` 節點為不會被修改的節點,每次的 `safe->prev` 就是會被交換的節點之一,`safe` 會不斷的移動至 `safe->prev`,直到 `head->next` 等同於 `safe` - 這邊 `safe->prev` 對應到陣列實作版本的 `a[i]` 元素 3. 從 `[0,cnt]` 隨機抽取一個數值 `j`,並以 `cur` 指標,從 linked list head,移動 `j + 1` 次至要被交換的令一個元素。 - 這邊 `cur` 對應到陣列實作版本的 `a[j]` 元素 4. 令 `temp = cur->prev` 當作傳入 `list_move` 的 `head` 參數 ; 並令 `safe` 當作傳 `list_move_tail` 的 `head` 參數 : - 分別以 `list_move_tail` 與 `list_move` 來移動 `cur` 節點與 `safe->prev->prev` 節點 :::success :bulb: 在 linked list 交換節點有兩個要注意的地方 : 1. 如果第 `i` 個節點與第 `j` 個節點相同,就不需要交換 2. 避免第 `j+1` 個節點與第 `i` 個節點相同,使用 `list_move_tail` 與 `list_move`,才不會發生相同節點呼叫 `list_move` 或 `list_move_tail` 的巨集時,存取到 `list_del` 過後的節點 ::: ```c /* Shuffle elements in queue */ void q_shuffle(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; srand(199); struct list_head *cur = NULL, *safe = NULL, *temp = NULL; int j = 0, cnt = q_size(head); for (safe = head; cnt >= 1; cnt--, safe = safe->prev) { j = rand() % cnt; for (cur = head; j >= 0; j--) cur = cur->next; if (cur != safe->prev) { temp = cur->prev; list_move_tail(cur, safe); list_move(safe->prev->prev, temp); } } } ``` 並在 `qtest.c` 內部加入下列程式碼 : ```c static bool do_shuffle(int argc, char *argv[]) { if (argc != 1) { report(1, "%s takes too much arguments", argv[0]); return false; } if (!current || !current->q) report(3, "Warning: Calling ascend on null queue"); error_check(); set_noallocate_mode(true); if (exception_setup(true)) q_shuffle(current->q); exception_cancel(); set_noallocate_mode(false); q_show(3); return !error_check(); } static void console_init() { ADD_COMMAND(shuffle, "Shuffle queue", ""); ... } ``` #### 簡短探討 random shuffle linked list 根據 [What-is-the-most-efficient-way-to-randomize-shuffle-a-linked-list](https://www.quora.com/What-is-the-most-efficient-way-to-randomize-shuffle-a-linked-list) 的討論,可以得知 : - 在 `O(1)` space complexity 之下,可以做到最快的 random shuffle 為 `O(nlogn)` time complexity - 在 `O(n)` space complexity 之下,可以做到最快的 random shuffle 為 `O(n)` time complexity 並且 Fisher–Yates shuffle 可以在 linked list 內節點數量多的時候,得到較好的效率,其方法是透過一個額外的指標陣列,以索引的方式存取到節點,來做到節點交換,時間複雜度可以達到 `O(n)`。若像上述實作的方式,利用第二層 for 迴圈找到相應的節點,則時間複雜度 `O(n^2)`。 --- ### 解釋本程式的 “simulation” 模式 #### 論文重點整理 根據 [Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf) : 1. 不同的 measurements 是經由不同的 p percentile pre-processing 處理得到的 2. 作者選擇捨棄大於 p percentile 的 timing distribution,是因為作者 low tails of time distribution 較容易觀察的到 constant time implementation 和 non-constant time implementation 之間的差異 3. 作者採用兩種資料的來源是基於 fix-vs-random testing 的想法 : 所以在做實驗比較的時候,一個是來自於 fixed-value,也就是固定的 clocks cycle. 作者使用的 Cycle counter 常見於現代 CPU,如 x86 或 ARM 家族都有 ; 另一種是 random value,也就是隨機的 clocks cycle, 4. 實驗主要使用 Welch's t test 來衡量兩個 timing distribution 5. Figure 1 : x 軸代表一個函式執行了多少個 cycles,y 軸為機率密度函數,代表函式擁有 y % 的機率,執行最多有 x 個 cycles. - 可看到 Constant time 是一個 step function - 也可看到 Constant time 跟 Non-constant time 執行的 CDF 之間的差異,是很明顯的 timing attack 6. Figure 2 : x 軸代表這兩個 timing distribution 有多少個 measurements; y 軸代表 t 值,**同一條線代表同一種的 pre-processing 參數組**。 #### 解釋 Student’s t-distribution 及程式實作的原理。 1. Student's t-distribution : - t's test 又稱為 Student's t-test ,其類似於常態分佈,可以獨立雙樣本檢定兩個分佈的群組,並驗證主張 "兩個分佈相近,都是 constant time" 的 Null hypothesis. - 但是通常會**假設兩個分佈的變異數具有同性質** - 而 Welch's t-test 是 Student's t-test 的改版,**可用於兩個分佈擁有不同變異數時,他們的均值多相近** - Welch's t t-test 透過 **degree of freedom** 的概念來解決 Student's t-test 之下的變異數不同問題。 - **當 t 值越大,代表兩個分佈差越遠** - 如果 Welch's t-test 接受 Null hypothesis,代表我們的函式**有可能是 Constant time,但無法完全保證一定就是 Constant time**。 - 如果 Welch's t-test 拒絕 Null hypothesis,代表我們的函式**可能不是 Constant time 或者 絕對不是 Constant time**,取決於 Critical values。 2. Welford’s method : - 參考 [Welford’s method](https://jonisalonen.com/2013/deriving-welfords-method-for-computing-variance/),可知這種方法,可避免一般計算變異數,會採用的 Second-pass algorithm,其須大量暫存中間計算值,例如 : One-pass 會先計算一筆資料的平均值,Second-pass 再計算 Square difference from the mean。 - Welford's method 想法是,觀察**第 `N` 與第 `N - 1` 差平方和**,可以透過簡化,以 **bottom-up 的方式來推出第 `N` 的變異數與標準差** ![](https://i.imgur.com/oxKJutZ.png) - 在程式碼裡,對應到 `ttest.c` 內的 `t_push` 函式 3. Welch's t-test 原理 : - Welch's t-test - Welch's t-test 的計算式如下 : ![](https://i.imgur.com/kJhND65.png) - 其對應的 Degree of freedom 計算式如下 : ![](https://i.imgur.com/HDYxBnN.png) :::success :bulb: 兩個 t 分佈的變異數若不相同的話,自由度需要做加權調整,才可用於 t 檢定 ::: - 類似於卡方分佈的概念,我們可透過查詢 t-distribution table 內,找到在**先前設定的 significance level** + 計算完的 **degree of freedom**,來找到對應的 **critical values** : ![](https://i.imgur.com/z6RDkbC.png) :::success :bulb: 若兩者分佈的變異數相同的話,我們可以雙尾檢定的 significance level 來查表 ; 若兩個分佈中,一方的變異數大於另一方,則採用單尾檢定的 significance level。**由於論文要做 fix-vs-random test,須採單尾檢定。** ::: - 最後驗證 Null hypothesis : - 如果計算出來的 t 統計量 $t^{'}$,大於我們查表得到的 critical values $t_{significance \ level, degree \ of \ freedom}$, 則我們**拒絕 Null hypothesis,接受 alternative hypothesis,意義代表兩者的分佈,平均值有顯著差異。** - 反之,若小於 critical values,我們則**接受 Null hypothesis,拒絕 alternative hypothesis,意義代表兩者分佈的均值相近。** - 在程式碼裡面,對應到 `fixture.c` 內的 `report` 函式 :::success :bulb: Welch's t-test 的計算式與範例可參考此[網站](https://highscope.ch.ntu.edu.tw/wordpress/?p=73780) ::: 4. 程式實作原理 : 先做 Code trace,再做分析。 #### Code trace 首先看到 `fixture.c` 的前幾行註解可知,該檔案以多次不同的輸入來衡量一個函式的執行時間,並利用 `Welch's t-test` 來決定其是否為 Constant time implementation。 根據 `fixture.c`: 1. 先看到 `test_const` 其接收參數 `char *text` 是被衡量的 function 名稱,其執行多次的 `doit` 來衡量此函式。 ```c // fixture.c #define ENOUGH_MEASURE 10000 #define TEST_TRIES 10 ... static bool test_const(char *text, int mode) { bool result = false; t = malloc(sizeof(t_context_t)); for (int cnt = 0; cnt < TEST_TRIES; ++cnt) { printf("Testing %s...(%d/%d)\n\n", text, cnt, TEST_TRIES); init_once(); for (int i = 0; i < ENOUGH_MEASURE / (N_MEASURES - DROP_SIZE * 2) + 1; ++i) result = doit(mode); printf("\033[A\033[2K\033[A\033[2K"); if (result) break; } free(t); return result; } ``` 2. 接著可發現 `doit` 就是用來執行一整個衡量流程的函式 : ```c static bool doit(int mode) { int64_t *before_ticks = calloc(N_MEASURES + 1, sizeof(int64_t)); int64_t *after_ticks = calloc(N_MEASURES + 1, sizeof(int64_t)); int64_t *exec_times = calloc(N_MEASURES, sizeof(int64_t)); uint8_t *classes = calloc(N_MEASURES, sizeof(uint8_t)); uint8_t *input_data = calloc(N_MEASURES * CHUNK_SIZE, sizeof(uint8_t)); if (!before_ticks || !after_ticks || !exec_times || !classes || !input_data) { die(); } prepare_inputs(input_data, classes); bool ret = measure(before_ticks, after_ticks, input_data, mode); differentiate(exec_times, before_ticks, after_ticks); update_statistics(exec_times, classes); ret &= report(); free(before_ticks); free(after_ticks); free(exec_times); free(classes); free(input_data); return ret; } ``` 3. 其會先根據 `constan.c` 內的 `prepare_inputs` 來準備 `N_MEASURES = 150` 筆隨機字串當測試資料,也就是一次 test 會有 150 筆 measurements: ```c // constant.c void prepare_inputs(uint8_t *input_data, uint8_t *classes) { randombytes(input_data, N_MEASURES * CHUNK_SIZE); for (size_t i = 0; i < N_MEASURES; i++) { classes[i] = randombit(); if (classes[i] == 0) memset(input_data + (size_t) i * CHUNK_SIZE, 0, CHUNK_SIZE); } for (size_t i = 0; i < N_MEASURES; ++i) { /* Generate random string */ randombytes((uint8_t *) random_string[i], 7); random_string[i][7] = 0; } } // constan.h /* Number of measurements per test */ #define N_MEASURES 150 ``` 4. 準備好測試資料的輸入之後,可看到 `measure` 函式,會針對不同的函式,會從剛剛準備好的測試資料內隨機存取一筆資料來衡量其 CPU cycles,方法為 : - 在執行的函式開始之前,插入 `beforeticks[i] = cpucycles()`,紀錄當前在第幾個 cpu cycle - 在執行的函式結束之後,插入 `afterticks[i] = cpucycles()`,紀錄執行後在第幾個 cpu cycle :::warning :question: 不知道為何每一個函式 case,內部都有一個 `dut_insert_head( get_random_string(), *(uint16_t *) (input_data + i * CHUNK_SIZE) % 10000);` ::: ```c bool measure(int64_t *before_ticks, int64_t *after_ticks, uint8_t *input_data, int mode) { assert(mode == DUT(insert_head) || mode == DUT(insert_tail) || mode == DUT(remove_head) || mode == DUT(remove_tail)); switch (mode) { case DUT(insert_head): for (size_t i = DROP_SIZE; i < N_MEASURES - DROP_SIZE; i++) { char *s = get_random_string(); dut_new(); dut_insert_head( get_random_string(), *(uint16_t *) (input_data + i * CHUNK_SIZE) % 10000); int before_size = q_size(l); before_ticks[i] = cpucycles(); dut_insert_head(s, 1); after_ticks[i] = cpucycles(); int after_size = q_size(l); dut_free(); if (before_size != after_size - 1) return false; } break; case DUT(insert_tail): for (size_t i = DROP_SIZE; i < N_MEASURES - DROP_SIZE; i++) { char *s = get_random_string(); dut_new(); dut_insert_head( get_random_string(), *(uint16_t *) (input_data + i * CHUNK_SIZE) % 10000); int before_size = q_size(l); before_ticks[i] = cpucycles(); dut_insert_tail(s, 1); after_ticks[i] = cpucycles(); int after_size = q_size(l); dut_free(); if (before_size != after_size - 1) return false; } break; case DUT(remove_head): for (size_t i = DROP_SIZE; i < N_MEASURES - DROP_SIZE; i++) { dut_new(); dut_insert_head( get_random_string(), *(uint16_t *) (input_data + i * CHUNK_SIZE) % 10000 + 1); int before_size = q_size(l); before_ticks[i] = cpucycles(); element_t *e = q_remove_head(l, NULL, 0); after_ticks[i] = cpucycles(); int after_size = q_size(l); if (e) q_release_element(e); dut_free(); if (before_size != after_size + 1) return false; } break; case DUT(remove_tail): for (size_t i = DROP_SIZE; i < N_MEASURES - DROP_SIZE; i++) { dut_new(); dut_insert_head( get_random_string(), *(uint16_t *) (input_data + i * CHUNK_SIZE) % 10000 + 1); int before_size = q_size(l); before_ticks[i] = cpucycles(); element_t *e = q_remove_tail(l, NULL, 0); after_ticks[i] = cpucycles(); int after_size = q_size(l); if (e) q_release_element(e); dut_free(); if (before_size != after_size + 1) return false; } break; default: for (size_t i = DROP_SIZE; i < N_MEASURES - DROP_SIZE; i++) { dut_new(); dut_insert_head( get_random_string(), *(uint16_t *) (input_data + i * CHUNK_SIZE) % 10000); before_ticks[i] = cpucycles(); dut_size(1); after_ticks[i] = cpucycles(); dut_free(); } } return true; } ``` 5. 接著透過 `differentiate` 來將函式測試時,執行完畢時的 clocks cycle 減去開始執行時的 clocks cycle,得到針對不同 input 時,執行一次函式所需要的 clocks cycle : ```c static void differentiate(int64_t *exec_times, const int64_t *before_ticks, const int64_t *after_ticks) { for (size_t i = 0; i < N_MEASURES; i++) exec_times[i] = after_ticks[i] - before_ticks[i]; } ``` 6. 再利用`update_statistics` 來執行 Welford’s method,以 single-pass 的方式計算變異數 ```c // fixture.c static void update_statistics(const int64_t *exec_times, uint8_t *classes) { for (size_t i = 0; i < N_MEASURES; i++) { int64_t difference = exec_times[i]; /* CPU cycle counter overflowed or dropped measurement */ if (difference <= 0) continue; /* do a t-test on the execution time */ t_push(t, difference, classes[i]); } } // ttest.c void t_push(t_context_t *ctx, double x, uint8_t class) { assert(class == 0 || class == 1); ctx->n[class]++; /* Welford method for computing online variance * in a numerically stable way. */ double delta = x - ctx->mean[class]; ctx->mean[class] = ctx->mean[class] + delta / ctx->n[class]; ctx->m2[class] = ctx->m2[class] + delta * (x - ctx->mean[class]); } ``` 7. 最後再利用 `report`,執行 Welch's t-test 來看是否要拒絕 Null hypothesis,其 Null hypothesis 為 : 我們寫的函式等同於 Constant time : ```c // fixture.c static bool report(void) { double max_t = fabs(t_compute(t)); double number_traces_max_t = t->n[0] + t->n[1]; double max_tau = max_t / sqrt(number_traces_max_t); printf("\033[A\033[2K"); printf("meas: %7.2lf M, ", (number_traces_max_t / 1e6)); if (number_traces_max_t < ENOUGH_MEASURE) { printf("not enough measurements (%.0f still to go).\n", ENOUGH_MEASURE - number_traces_max_t); return false; } /* max_t: the t statistic value * max_tau: a t value normalized by sqrt(number of measurements). * this way we can compare max_tau taken with different * number of measurements. This is sort of "distance * between distributions", independent of number of * measurements. * (5/tau)^2: how many measurements we would need to barely * detect the leak, if present. "barely detect the * leak" = have a t value greater than 5. */ printf("max t: %+7.2f, max tau: %.2e, (5/tau)^2: %.2e.\n", max_t, max_tau, (double) (5 * 5) / (double) (max_tau * max_tau)); /* Definitely not constant time */ if (max_t > t_threshold_bananas) return false; /* Probably not constant time. */ if (max_t > t_threshold_moderate) return false; /* For the moment, maybe constant time. */ return true; } // ttest.c double t_compute(t_context_t *ctx) { double var[2] = {0.0, 0.0}; var[0] = ctx->m2[0] / (ctx->n[0] - 1); var[1] = ctx->m2[1] / (ctx->n[1] - 1); double num = (ctx->mean[0] - ctx->mean[1]); double den = sqrt(var[0] / ctx->n[0] + var[1] / ctx->n[1]); double t_value = num / den; return t_value; } ``` #### 實作分析與缺陷 1. 利用 **fix-vs-random test**: - 程式內部會不斷以 constant sample 與 random sample,來驗證函式以 random sample 下是否貼近 constant sample 下的均值,並以多次測試來決定其是否為 constant time 2. 沒有做論文內提及的 **Cropping**: - Cropping 是一種在 statistical analysis 之前的 pre-processing 方法 - Cropping 可根據不同的 `p percentile` 來捨棄掉大於 `p` 的 measurements - 作者希望專注在做 **low cycle count tail** 的單尾檢定,並認為 **upper tail 容易被 date-independent noise 給影響** :::success :bulb: Data-independent noise 像是執行函式時,被 OS 發出中斷訊號,或者其它影響函式測試的行為 ::: 根據上述, 如果沒有做 Cropping 的話,因為 `fixture.c` 對於每一個函式,都會做 `ENOUGH_MEASURE / (N_MEASURES - DROP_SIZE * 2) + 1` 次的測試,並且測試回合數為 `TEST_TRICES = 10`,因此在執行過程中,執行次數多且時間長,**upper tail 容易被影響**,因此 t 檢定值可能會隨著 data-independent noise 而使得每次測試結果不同。 在我每一次以 simulation mode 跑測試時,四個函式的結果每次測出來的結果都不太一樣,經過追蹤程式碼,並畫出當自動評分認為不是 constant time 時, `insert_head` 一個測試的Cycle count 如下 : ![](https://i.imgur.com/FpFQ9GL.png) 可發現在接近 x = 80 的 measurements,有峰值接近 1000 的 cycle,可能是函式被 OS 的 interrupt 的中斷影響,而中斷可能來自於 Linux 的排程,也有可能來自於 I/O。 ---- ### 引入 lib/list_sort.c 根據 [linux/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c),可分析 linux 核心的 linked list 排序由三個函式組成 : `merge`, `merge_final` 和 `list_sort` 為了引入 linux 的排序演算法,並且跟我的 merge sort 能夠互相切換,我先在 `queue.h` 內引入了下列程式碼: ```c #define LINUX_SORT typedef unsigned char u8; #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) ``` 其中: - `u8` 代表 unsigned 型態且為 8 bit 的資料,故這邊直接以 `typedef unsigned char` 來定義 - 根據 [likely and unlikely macro](https://meetonfriday.com/posts/cecba4ef/) 的解釋可知,**`__bultin_expect` 是 gcc 的內建 function ,用來將 branch 的相關資訊提供給 compiler,告訴 compiler 設計者期望的比較結果,以便對程式碼改變分支順序來進行優化。** - 因此 `likely(x)` 代表我們認為它發生 `x == 1`的機率比較高,也告訴 Compiler 要條件式內 `x == 1` 對應到的程式碼,接在後面執行 - `unlikely(x)` 則代表發生`x == 0` 的機率比較高,告訴 Compiler 要條件式內 `x == 0` 對應到的程式碼,接在後面執行 :::success :bulb: 因此 `likely` 和 `unlikely` 有助於程式減少分支跳躍的成本,但編譯時 gcc 要開啟最佳化, `__builtin_expect` 才有效果 ::: :::success :bulb: `__builtin_expect` 使用 `!!(x)` 的原因在於,連續兩次的 NOT 運算,能夠保證其值為 0 或 1,例如 `!!(5) = !(0) = 1` ::: 並重新改寫了 `queue.c` 內的 `q_sort` 與新增 `linux_cmp`: - 利用 predecessor 來切換使用的是我的 merge sort 或是 linux 的 sort - 並根據 [linux/list_sort.h](https://github.com/torvalds/linux/blob/master/include/linux/list_sort.h),找到 `list_cmp_func_t` 的定義,再參考 [Function pointer](https://chenhh.gitbooks.io/parallel_processing/content/cython/function_pointer.html),實作出 `linux_cmp` ```c /* Sort elements of queue in ascending order */ void q_sort(struct list_head *head) { #ifndef LINUX_SORT if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *fast = head->next->next, *slow = head->next; // split list while (fast != head && fast->next != head) { slow = slow->next; fast = fast->next->next; } // generate left & right head LIST_HEAD(left); LIST_HEAD(right); list_cut_position(&left, head, slow); list_splice_init(head, &right); q_sort(&left); q_sort(&right); my_merge(head, &left, &right); #else list_sort(NULL, head, linux_cmp); #endif } static int linux_cmp(void *priv , const struct list_head *a, const struct list_head *b) { char *a_str = list_entry(a, element_t, list)->value; char *b_str = list_entry(b, element_t, list)->value; return strcmp(a_str, b_str); } ``` 接著我使用 `linux` 系統的 `perf`,為一套系統效能分析的工具,可提供多個面向的效能比較,使用方式可參考 [成大資工wiki - perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) 1. 先寫好執行各別在 $10^6,10^7, 10^8$ 排序資料的命令 :::warning :bulb: perf 缺點在於若要量測一段程式碼,須將其獨立出來。這邊使用的命令,測量上也包括了 `new`,無法最公平的比較 ::: ```cmd # Test of my merge sort v.s. Linux sort # 執行插入一百萬次的腳本 option fail 0 option malloc 0 new ih RAND 1000000 sort ``` 2. 並利用 Python 作為腳本,執行 `perf` 來觀察每一個排序的效能比較 : ```python3 import subprocess from math import pow nums = [int(pow(10, k)) for k in range(6, 9)] for k in nums: p = subprocess.run(f"""sudo perf stat -C 1,2,3,4,5,6,7 -e branches -e instructions -e context-switches -e cpu-cycles -e cache-misses -e cache-references -e L1-dcache-loads -e system_time -e user_time --repeat 10 -o perf_report/sortcmp_perf_{k}_report.txt ./qtest -f traces/my-trace-sorting-cmp-{k}.cmd""", shell=True) ``` 得到的結果比較如下 : #### my merge sort | 資料數量 | branches | instructions | context-switches | cpu-cycles | cache-misses | | -------- | -------- | -------- | -------- | -------- | -------- | | $10^6$ | 679,012,523 | 4,875,172,271 | 978 | 7,829,057,985 | 101,640,677 | | $10^7$ | 788,000,774 | 5,622,147,933 | 1,623 | 9,277,850,351 | 140,175,351 | | $10^8$ | 630,853,888 | 5,085,136,193 | 1,423 | 9,329,504,976 | 138,576,715 | | 資料數量 | cache-references | L1-dcache-loads | system_time | user_time | | -------- | -------- | -------- | -------- | -------- | | $10^6$ | 209,742,446 | 1,001,857,248 | 3,010,816,900 | 10,992,776,200 | | $10^7$ | 255,647,029 | 1,152,871,723 | 3,329,508,000 | 12,598,190,500 | | $10^8$ | 254,038,731 | 1,173,757,396 | 3,321,200,400 | 12,635,215,600 | #### list_sort | 資料數量 | branches | instructions | context-switches | cpu-cycles | cache-misses | | -------- | -------- | -------- | -------- | -------- | -------- | | $10^6$ | 526,535,184 | 4,222,727,130 | 956 | 7,149,526,207 | 91,477,708 | | $10^7$ | 606,694,876 | 5,152,602,028 | 1,298 | 8,656,871,637 | 98,835,155 | | $10^8$ | 778,221,524 | 5,612,350,276 | 1,043 | 8,469,879,742 | 93,550,735 | | 資料數量 | cache-references | L1-dcache-loads | system_time | user_time | | -------- | -------- | -------- | -------- | -------- | | $10^6$ | 159,253,765 | 840,097,790 | 2,892,750,700 | 9,682,878,100 | | $10^7$ | 198,423,050 | 1,179,638,533 | 3,351,852,000 | 11,284,926,800 | | $10^8$ | 196,866,365 | 1,118,001,716 | 3,265,051,300 | 11,206,414,100 | 由上述統計結果,我們進行下列的討論與結論 : 1. 排序不需要大量 kernel-space 才能執行的函式,因此排序消耗的時間大多都是在 user-space : - 可看到我的 `merge sort` 消耗在 user-space 的時間就多於 linux 的 `list_sort` - Linux 比較快的地方在於它以 bottom-up 的方式排序元素,不需要 Top-down 遞迴呼叫的時間,也省下使用遞迴堆疊的空間 2. 再來看到 `branches`,我實作的 `merge sort` 也比 linux 的 `list_sort` 還要多 : - 因為 `list_sort` 有利用 `__builtin_expect` 來減少分支跳躍指令的情形,可以讓編譯器最佳化指令的排序 - 而且我的排序是以 Top-down 的方式,先做 partition,再 Bottom up 的組合起來 ; 而 Linux 的 `list_sort` 是以 bottom up 的方式直接把元素組合起來,因此所需的條件分支,比我的少更多 3. 最後看到 `cache-misses` 的部份,我實作的 `merge sort`,明顯在每個層級比 `list_sort` 的快取失誤還要來的多 : - 主因是 `list_sort` 在合併的時候有考量到 2:1 的原則,也就是維持已合併與合併的 linked list 為 2:1,**防止 cache threashing 的現象**