# 2024q1 Homework1 (lab0) contributed by < [Ackerman666](https://github.com/Ackerman666) > ### Reviewed by `allenliao666` - 建議使用 git diff 方便閱讀者了解內容,例如 `q_free` 中的「原先有在函式中將 head 設成 NULL 的動作 (在 commit 時跳以下錯誤)」。 - 內文中同時使用元素與節點表示 element,應統一翻譯所使用的詞語。 - 應把 assign 、 reverse 等翻譯成中文,避免中英交雜。 - `q_swap` 可以使用 list_move 簡化程式碼。 - ## 開發環境 $ gcc --version gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ 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): 16 On-line CPU(s) list: 0-15 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz CPU family: 6 Model: 165 Thread(s) per core: 2 Core(s) per socket: 8 Socket(s): 1 Stepping: 5 CPU max MHz: 4800.0000 CPU min MHz: 800.0000 BogoMIPS: 5799.77 <s> ## 修改 `queue.c` </s> ## 指定佇列的操作 ### `q_new` :::danger allocate 翻譯為「配置」,以區格 dispatch (分發/分配) 的翻譯 > 已更正 ::: * 用`malloc` <s>分配</s>**配置**空間給予 `list_head` * `list_head` 物件透過 `INIT_LIST_HEAD` 初始化 值得注意的是`malloc`可能會回傳==空指標==,因此進行初始化前先判斷其是否為空指標 ```c /* Create an empty queue */ struct list_head *q_new() { struct list_head *q_head = (struct list_head *) malloc(sizeof(struct list_head)); if(q_head) INIT_LIST_HEAD(q_head); return q_head; } ``` ### `q_free` :::danger HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」 ::: ```c /* Free all storage used by queue */ void q_free(struct list_head *head) { if (!head) return; element_t *element, *safe; list_for_each_entry_safe (element, safe, head, list) { q_release_element(element); } free(head); } ``` 原先有在函式中將 `head` 設成 `NULL` 的動作 (在 `commit` 時跳以下錯誤) Following files need to be cleaned up: queue.c queue.c:39:5: warning: Assignment of function parameter has no effect outside the function. Did you forget dereferencing it? [uselessAssignmentPtrArg] head = NULL; ^ Fail to pass static analysis. 後來意識到 `head` 這個變數本身就只是函式內的區域變數 因此就算將其設定成 `NULL`,本質上不會影響到當初傳遞進去的指標變數,因此要避免 ### `q_insert_head` :::danger 避免非必要的項目符號 (即 `* `),以清晰、明確,且流暢的漢語書寫。 ::: 建立一個新的 `element` 物件 檢查佇列和 `element` 是否存在 如通過上述檢查,`strdup` 會分配空間,並將 `s` 指向字串複製進去,再將空間地址 `assign` 到 `element->value` 最後判斷 `element->value` 是否為NULL,是則本次 insert 失敗,並透過 `free` 釋放出空間 insert 成功則透過 `list_add` 將 `element` 加進佇列開頭 ```c /* Insert an element at head of queue */ bool q_insert_head(struct list_head *head, char *s) { element_t *element = (element_t *) malloc(sizeof(element_t)); if (!element || !head) return false; element->value = strdup(s); if (!element->value) { free(element); return false; } list_add(&element->list, head); return true; } ``` ### `q_insert_tail` 程式邏輯和 `q_insert_head` 本質上相同 ```c /* Insert an element at tail of queue */ bool q_insert_tail(struct list_head *head, char *s) { element_t *element = (element_t *) malloc(sizeof(element_t)); if (!element || !head) return false; element->value = strdup(s); if (!element->value) { free(element); return false; } list_add_tail(&element->list, head); return true; } ``` ### `q_remove_head` * 佇列存在且非空就透過 `list_first_entry` 找到第一個 `element` * 執行 `list_del` 將 `element` 移除 * 如果 `sp` 非 `NULL`,則透過 `strncpy` 將 `element->value` 複製到 `sp` ```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; element_t *element = list_first_entry(head, element_t, list); list_del(&element->list); if (sp && bufsize > 0) { strncpy(sp, element->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return element; } ``` ### `q_remove_tail` 程式邏輯和 `q_remove_head` 本質上相同 ```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; element_t *element = list_last_entry(head, element_t, list); list_del(&element->list); if (sp && bufsize > 0) { strncpy(sp, element->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return element; } ``` ### `q_size` * 設定一個計數器為 `size` ,透過 `list_for_each` 走訪每個節點,每走訪一個節點 `size` 就加一 ```c /* Return number of elements in queue */ int q_size(struct list_head *head) { if (!head || list_empty(head)) return 0; int size = 0; struct list_head *iterator; list_for_each (iterator, head) { ++size; } return size; } ``` ### `find_mid_node` **該函式用於找尋佇列裡的 middle point** 原先想透過一次走訪得到佇列長度,再進行一次走訪刪除中間節點,複雜度為 $\mathcal{O}(n)$ 但看了[分析「快慢指標」](https://hackmd.io/@sysprog/ry8NwAMvT)後,得知上述方法因時間局部性 ,在 cache miss 上會比快慢指標來得多 :::danger 提供量化分析來確認這說法。 ::: 因此採取快慢指標的方式實作該函式 ```c struct list_head *find_mid_node(struct list_head *head) { struct list_head *fast, *slow, *tail; fast = slow = head->next; tail = head->prev; // queue size > 1 if (slow != tail) { while (fast != head && fast != tail) { fast = fast->next->next; if (fast != head) slow = slow->next; } } return slow; } ``` ### `q_delete_mid` 透過`find_mid_node`找到中間節點,再予以刪除 ```c /* Delete the middle node in queue */ bool q_delete_mid(struct list_head *head) { // https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/ if (!head || list_empty(head)) return false; struct list_head *mid; mid = find_mid_node(head); list_del(mid); element_t *mid_element = list_entry(mid, element_t, list); q_release_element(mid_element); return true; } ``` ### `q_delete_dup` 透過 `list_for_each_safe` 可以得到當前元素 `cur` 和下個元素 `next` 迴圈中會執行 `strcmp` 比較兩元素的 value ,相等則將 `cur` 進行刪除,並設置 `delete` 變數為 true 而 `delete` 變數的目為確保每個 value 重複出現的節點都能確實的被刪掉 ```c /* Delete all nodes that have duplicate string */ bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (!head) return false; struct list_head *node, *safe; bool delete = false; list_for_each_safe (node, safe, head) { element_t *cur = list_entry(node, element_t, list); element_t *next = list_entry(safe, element_t, list); if (safe != head && strcmp(cur->value, next->value) == 0) { list_del(node); q_release_element(cur); delete = true; } else if (delete) { list_del(node); q_release_element(cur); delete = false; } } return true; } ``` ### `q_swap` 主要透過 `node` (當前節點), `safe` (下個節點), `pre` (當前節點的前一個節點)來操作 而 [list.h](https://github.com/Ackerman666/lab0-c/blob/644c4d3ef473b6a365bfa401d5684d9b87bd3464/list.h#L409-L411) 定義的巨集 `list_for_each_safe` 中每進行一次迭代,會將 `safe` <s>assign</s> 給 `node` 完成交換後,為了使 `node` 指向位置是下一個要交換的節點,必須將 `node->next` assign給 `safe` :::danger 改進你的漢語表達。 ::: ```c /* Swap every two adjacent nodes */ void q_swap(struct list_head *head) { // https://leetcode.com/problems/swap-nodes-in-pairs/ struct list_head *node, *safe, *pre = head; list_for_each_safe (node, safe, head) { if (node->next != head) { pre->next = safe; node->next = safe->next; node->prev = safe; safe->next = node; safe->prev = pre; pre = node; safe = node->next; } if (safe == head) head->prev = node; } } ``` ### `q_reverse` 主要透過 `node` (當前節點), `safe` (下個節點), `tmp` (當前節點的前一個節點) 來操作 在 `list_for_each_safe` 執行完畢後,還需要將 `head` 指向 reverse 過後的首尾節點 ```c /* Reverse elements in queue */ void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *node, *safe, *tmp = head; list_for_each_safe (node, safe, head) { node->next = tmp; node->prev = safe; tmp = node; } head->prev = head->next; head->next = tmp; } ``` ### `q_reverseK` :::danger access 的翻譯是「存取」,而非「訪問」(visit) > 已更正 ::: 用 `times` 紀錄<s>拜訪</s> **存取**的節點數量,當等於 k 時,用 `list_cut_position` 將節點切開,並移至 `tmp` 再對 `tmp` 執行 `q_reverse` ,並用 `list_splice_tail_init` 將 `tmp` 指向的 list 移動至 `rk_head` 上述執行完畢後,會反轉 k 個節點,為了要繼續反轉下組節點,必須在反轉完畢後將 `times` 設成 0 最後當迴圈執行完畢時,將 `rk_head` 指向的 list 再移回 `head` :::danger HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」 ::: ```c /* Reverse the nodes of the list k at a time */ void q_reverseK(struct list_head *head, int k) { // https://leetcode.com/problems/reverse-nodes-in-k-group/ if (!head || list_empty(head) || k == 1) return; int times = 1; struct list_head *node, *safe; struct list_head tmp, rk_head; INIT_LIST_HEAD(&tmp); INIT_LIST_HEAD(&rk_head); list_for_each_safe (node, safe, head) { if (times == k) { list_cut_position(&tmp, head, node); q_reverse(&tmp); list_splice_tail_init(&tmp, &rk_head); times = 0; } ++times; } list_splice(&rk_head, head); } ``` ### `merge_sorted_queues` 利用 merge sort 在 merge 階段的方式,將兩個已排序的佇列合併 **思路 :** 過程會兩兩比較節點,並依 `descend` 與否來選出排序較前面的節點 `selected` 並透過 `list_move_tail` 移到 `head` 上 最後再將 `head` 透過 `list_splice_tail`將結點移至 `l` 為了讓程式碼更精簡,我在判斷 `descend` 與否時使用了 <s>[三元表達式](https://shengyu7697.github.io/cpp-ternary-operator/)</s> **條件運算子 (conditional operator )** :::danger 參照 C 語言規格書! > 已更正 ::: 後來想到`l` 與 `r` 的升降序方向要相同,否則結果會錯,這部分我再思考如何更 general :::danger 改進你的漢語表達。 ::: ```c /* Merge tow sorted queues and set l to the queue head*/ void merge_sorted_queues(struct list_head *l, struct list_head *r, bool descend) { struct list_head head; INIT_LIST_HEAD(&head); while (!list_empty(l) && !list_empty(r)) { struct list_head *selected, *l_first = l->next, *r_first = r->next; element_t *left_e = list_first_entry(l, element_t, list); element_t *right_e = list_first_entry(r, element_t, list); int cmp_result = strcmp(left_e->value, right_e->value); selected = descend ? ((cmp_result < 0) ? r_first : l_first) : ((cmp_result > 0) ? r_first : l_first); list_move_tail(selected, &head); } list_splice_tail_init(list_empty(l) ? r : l, &head); list_splice_tail(&head, l); } ``` ### `q_sort` 透過 merge sort 來對佇列排序 **思路** 先用 `find_mid_node` 找到中間節點 `mid` 再透過 `list_cut_position` 將原本的佇列切分成兩份 `l` 指向以 `mid` 為結尾的左半部, `head` 則為剩餘的右半部 `l` 與 `head` 再個別進行 `q_sort` 來進行排序 將排序好的`l` 和 `head` 利用 `merge_sorted_queues` 來 merge **執行過程中,資料結構皆保持環狀鏈結串列,不用特地拆開變成單向鏈結串列** ```c /* Sort elements of queue in ascending/descending order */ void q_sort(struct list_head *head, bool descend) { if (!head || list_empty(head) || list_is_singular(head)) return; /* left and right sub queue*/ struct list_head l, r; struct list_head *mid; INIT_LIST_HEAD(&l); INIT_LIST_HEAD(&r); mid = find_mid_node(head); /* After cut * l will point to the left part of the queue relative to the mid position. * otherwise head will point to the right part (excluding mid) */ list_cut_position(&l, head, mid); q_sort(&l, descend); q_sort(head, descend); merge_sorted_queues(&l, head, descend); list_splice_tail(&l, head); } ``` 精簡程式碼: ```diff /* left and right sub queue*/ - struct list_head l, r; + struct list_head l; struct list_head *mid; INIT_LIST_HEAD(&l); - INIT_LIST_HEAD(&r); ``` :::danger 你如何確保目前的測試已涵蓋排序演算法的最差狀況? ::: ### `q_descend` 函式功能註解講得不夠明確,`Remove` 一詞根據 [queue.h](https://github.com/Ackerman666/lab0-c/blob/b324db1ac5ed804a4f61ee2e3e3a22a48f9f69d0/queue.h#L90C1-L92C52) 不該將 space freed 掉 但實際上要執行 `q_release_element` 否則無法通過<s>測資</s>**測試項目** :::danger 此處為「測試項目」,著重在 item 一詞,不是「資料」。避免歧義,「測試資料」不要簡寫。 > 已更正 ::: **錯誤訊息如下** +++ TESTING trace trace-06-ops: # Test of insert_head, insert_tail, delete duplicate, sort, descend and reverseK ERROR: There is no queue, but 4 blocks are still allocated ERROR: Freed queue, but 4 blocks are still allocated :::danger 程式碼註解不該出現中文,總是以美式英語書寫。 > 已更正 ::: **主要思路** : 反向走訪佇列,並透過 `max` 紀錄當前 value 值最大之節點 當有節點的 value 小於所紀錄 `max` 的 value時,就將該點 remove 反之代表該節點的 value 比當前記錄的大,因此將 `max` 進行更新 ```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) { // https://leetcode.com/problems/remove-nodes-from-linked-list/ if (!head || list_empty(head)) return 0; int amount = 1; struct list_head *max = head->prev, *node = head->prev->prev, *next; for (next = node->prev; node != (head); node = next, next = node->prev) { element_t *element = list_entry(node, element_t, list); element_t *cur_max_element = list_entry(max, element_t, list); if (strcmp(cur_max_element->value, element->value) > 0) { list_del(&element->list); /* should "delete" the element, otherwise cant pass trace-06-ops */ q_release_element(element); } else { max = node; ++amount; } } return amount; } ``` :::danger HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」 ::: ### `q_ascend` 程式邏輯和 `q_descend` 本質上相同 ```c /* Remove every node which has a node with a strictly less value anywhere to * the right side of it */ int q_ascend(struct list_head *head) { // https://leetcode.com/problems/remove-nodes-from-linked-list/ if (!head || list_empty(head)) return 0; int amount = 1; struct list_head *min = head->prev, *node = head->prev->prev, *next; for (next = node->prev; node != (head); node = next, next = node->prev) { element_t *element = list_entry(node, element_t, list); element_t *cur_min_element = list_entry(min, element_t, list); if (strcmp(cur_min_element->value, element->value) < 0) { list_del(&element->list); q_release_element(element); } else { min = node; ++amount; } } return amount; } ``` ### `q_merge` 因為給定的所有 `queues` 皆是排序好的狀態,因此直接用利用 `list_for_each_entry` 遞迴 並以 `merge_sorted_queues` 兩兩合併至 `target` 中 原先我想使用的做法是將整個佇列合併成大佇列再執行一次 `q_sort` 但是 `q_sort` 在過程中會反覆的進行遞迴,並多次的 `merge_sorted_queues` 以記憶體的觀點來看,這樣會在 Stack memory 有多次的allocation and deallocation 操作 所以最後我改用逐一合併的方式來避免過多的函式呼叫 :::danger 改進你的漢語表達。 ::: ```c /* Merge all the queues into one sorted queue, which is in ascending/descending * order */ int q_merge(struct list_head *head, bool descend) { // https://leetcode.com/problems/merge-k-sorted-lists/ if (!head || list_empty(head)) return 0; queue_contex_t *target = list_entry(head->next, queue_contex_t, chain); queue_contex_t *other = NULL; list_for_each_entry (other, head, chain) { if (target == other) continue; merge_sorted_queues(target->q, other->q, descend); other->size = 0; } return q_size(target->q); } ``` ## 實作 shuffle 命令 > 參考 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 原理是每次隨機挑選一個陣列元素 並將該元素與陣列最尾端**未置換**過的元素做交換 因過程不需額外記憶體空間,因此除線性時間外也達到 [in place](https://en.wikipedia.org/wiki/In-place_algorithm)的效果 :::danger 優先參照權威材料,例如你的演算法教科書和指定教材,退而求其次,才是英文 Wikipedia 條目。 > 了解,已將連結替換 ! ::: 參考之虛擬碼 <s>pseudocode</s> -- To shuffle an array a of n elements (indices 0..n-1): for i from n−1 down to 1 do j ← random integer such that 0 ≤ j ≤ i exchange a[j] and a[i] ### 統計結果 腳本程式碼來自[作業說明](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-d) :::danger 這個次數怎麼決定呢?能否用你的統計知識來解釋? 要有多少次測試才能夠總結呢? > 假說檢定有兩個重要的錯誤指標 >* 型一錯誤 ( alpha(α)) : A/B 兩組本質上沒差異,卻被統計成有差異的機率 >* 型二錯誤 ( beta (β)) : A/B 兩組本質上有差異,卻無法偵測到顯著差異的機率 > >通常會希望這兩種錯誤越低越好,最直觀的方式就是去增加樣本數,來貼近實際情況 >譬如有相同數量的黑球白球放在一個箱子中,如只取出10顆,結果為7顆黑球與3顆白球 >那麼就會造成型一錯誤,而如果將抽取次數增多,則可避免因型一錯誤造成的誤判 > >目前已知樣本數和型一錯誤、型二錯誤、效力量(Effect Size)有關 >但相關推算公式,我目前還沒找到資料 > > 你的統計學教科書呢?去讀書! :notes: jserv ::: 在執行該腳本時要注意 `qtest ` 執行檔的位置,原先我是複製一份到腳本資料夾底下 但跳出以下錯誤 stderr: FATAL: You should run qtest in the directory containing valid git workspace 最後我是透過 `cwd` 更改 `subprocess.run()` 執行的工作目錄位置,切換至根目錄來執行 ```diff +cwd = '../' -completedProcess = subprocess.run(clist, capture_output=True, text=True, input=input) +completedProcess = subprocess.run(clist, capture_output=True, text=True, input=input, cwd = cwd) ``` 執行 scripts/shuffle.py 進行實驗 總共執行1000000 shuffle Expectation: 41666 Observation: {'1234': 41503, '1243': 41338, '1324': 41708, '1342': 41853, '1423': 42100, '1432': 41624, '2134': 41583, '2143': 41588, '2314': 41966, '2341': 41518, '2413': 41603, '2431': 41547, '3124': 41929, '3142': 41890, '3214': 41663, '3241': 41660, '3412': 41767, '3421': 41783, '4123': 41462, '4132': 41573, '4213': 41858, '4231': 41376, '4312': 41432, '4321': 41676} chi square sum: 20.96140738251812 * Expectation : 預期每種組合出現的次數 * Observation : 每種組合在該次測試中實際出現次數 * chi square sum : 20.96 下圖為該次測試的統計圖 可以看出每種組合出現次數都相當均勻 ![image](https://hackmd.io/_uploads/rk-TXIQ0T.png) ## 研讀 Linux [`list_sort.c`](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 並引入專案 將 [`list_sort.c`](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 和 [`list_sort.h`](https://github.com/torvalds/linux/blob/master/include/linux/list_sort.h) 加入專案 `list_sort.h` 中會用到 `likely` 和 `unlikely` 兩個巨集 而該巨集是被定義在 [`linux/compiler.h`](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h) 底下 但本次專案並不包含 `compiler.h`,因此我將該巨集定義在 `list_sort.h` 中 ```diff +#define likely(x) __builtin_expect(!!(x), 1) +#define unlikely(x) __builtin_expect(!!(x), 0) ``` **likely 和 unlikely 巨集用意** 兩個巨集由 `__builtin_expect()` 而來,該函式由 gcc 內建 該函式會用到兩個 not operator,目的就是確保 `!!(x)` 的結果一定會是 0 或者是 1 巨集目的是要讓 branch 期望的比較結果讓編譯器知道,並做進一步優化 下述程式中,透過 `unlikely(x)` 可以表示 `x` 這個敘述為否的機會較大 因此編譯器在編譯程式後,會使 `bar()` 的 assembly code 執行順序較 `foo()` 來的前面 以此來減少 branch hazarad 的發生 if (unlikely(x)) { // foo() } else { // bar() } `list_sort.h` 中有定義如下的函式指標 __attribute__((nonnull(2, 3))) (*list_cmp_func_t)(void *, const struct list_head *, const struct list_head *); 因此我在 [`custom_cmp.c`](https://github.com/Ackerman666/lab0-c/blob/master/custom_cmp.c) 實作了對應到 queue element 的升降序比較函式 **發生的錯誤** 在 commit `custom_cmp.c` 程式碼時,在 Cppcheck 靜態分析中會發生 nullPointer error 原因和 [jasperlin1996](https://hackmd.io/@jasperlin1996/linux2022-lab0#Cppcheck-Null-pointer-dereference:~:text=Cppcheck%3A%20Null%20pointer%20dereference) 指出的[問題](https://hackmd.io/@jasperlin1996/linux2022-lab0#Cppcheck-Null-pointer-dereference:~:text=Cppcheck%3A%20Null%20pointer%20dereference)一樣 :::danger 「呼叫」(call) 和「使用」(use) 不同,請查閱辭典以理解二者落差。 巨集不能「呼叫」,因為前置處理器會在真正編譯 C 程式碼之前就展開巨集內容。 $\to$ [你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor) > 已修正 ::: 以下會<s>呼叫</s> **使用** `list_entry` 的巨集 ```c int q_asc_cmp(void *, const struct list_head *l1, const struct list_head *l2) { if (l1 == NULL || l2 == NULL) return 0; return strcmp(list_entry(l1, element_t, list)->value, list_entry(l2, element_t, list)->value); } ``` `list_entry` 由 `container_of` 而來 問題關鍵在於 `container_of` 中有以下程式碼 `(type *) 0` 為 null pointer,因而導致在靜態檢查中,偵測到 **Null pointer dereference** 的錯誤 const __typeof__(((type *) 0)->member).... **解法** 在 [`lab0-c/scripts/pre-commit.hook`](https://github.com/Ackerman666/lab0-c/blob/master/scripts/pre-commit.hook) 中已經有對特定的檔案做 nullPointer 的壓制 因此仿照同樣做法新增以下程式即可解決 ```diff CPPCHECK_suppresses="--inline-suppr harness.c \ --suppress=missingIncludeSystem \ --suppress=noValidConfiguration \ --suppress=unusedFunction \ --suppress=identicalInnerCondition:log2_lshift16.h \ --suppress=nullPointerRedundantCheck:report.c \ --suppress=nullPointerRedundantCheck:harness.c \ --suppress=nullPointer:queue.c \ --suppress=nullPointer:qtest.c \ +--suppress=nullPointer:custom_cmp.c \ --suppress=returnDanglingLifetime:report.c \ --suppress=constParameterCallback:console.c \ --suppress=constParameterPointer:console.c \ --suppress=checkLevelNormal:log2_lshift16.h" CPPCHECK_OPTS="-I. --enable=all --error-exitcode=1 --force $CPPCHECK_suppresses $CPPCHECK_unmatched ." ``` ### 增加 `list_sort` 指令 對 `sort` 指令進行修改,加入第二個參數來指定排序模式 改動細節 : [commit 8b5af29](https://github.com/Ackerman666/lab0-c/commit/8b5af29e9b92259ba547c79138fff270771e5684) ```diff -ADD_COMMAND(sort, "Sort queue in ascending/descening order", ""); +ADD_COMMAND(sort,"Sort queue in ascending/descening order (sort type : [0] ""merge_sort, [1] list_sort)","[t]"); ``` ### 比較 `list_sort` 與 `q_sort` 先參考 [Linux 效能分析工具: Perf](https://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) 進行 Perf 工具安裝 新增 `trace-sort.cmd` 隨機新增1000000筆資料到佇列,執行排序,並記錄所耗費時間 option fail 0 option malloc 0 new ih RAND 1000000 time sort 1 time free 執行下列指令來重複執行5次 `qtest` 在這邊實際上會用到該指令兩次,第一次 `qtest` 執行 `q_sort` ,第二次為 `list_sort` sudo perf stat --repeat 5 ./qtest -f traces/trace-sort.cmd **執行時間比較** 由下表可看出每次執行 `list_sort` 耗時皆比 `q_sort` 少 | Exp No | q_sort | list_sort| | -------- | -------- | -------- | | 1 | 1.079 | 0.749 | | 2 | 1.099 | 0.755 | | 3 | 1.073 | 0.746 | | 4 | 1.065 | 0.751 | | 5 | 1.106 | 0.743 | **perf stat 分析情況** my_sort Performance counter stats for './qtest -f traces/trace-sort.cmd' (5 runs): 1640.86 msec task-clock # 1.000 CPUs utilized ( +- 0.39% ) 3 context-switches # 1.828 /sec ( +- 24.94% ) 0 cpu-migrations # 0.000 /sec 3,5234 page-faults # 21.473 K/sec ( +- 0.00% ) 75,6362,5118 cycles # 4.610 GHz ( +- 0.37% ) 49,5211,3455 instructions # 0.65 insn per cycle ( +- 0.00% ) 6,9795,4553 branches # 425.358 M/sec ( +- 0.00% ) 1220,5845 branch-misses # 1.75% of all branches ( +- 0.33% ) list_sort Performance counter stats for './qtest -f traces/trace-sort.cmd' (5 runs): 1307.84 msec task-clock # 1.000 CPUs utilized ( +- 0.61% ) 2 context-switches # 1.529 /sec ( +- 29.15% ) 0 cpu-migrations # 0.000 /sec 3,5233 page-faults # 26.940 K/sec ( +- 0.00% ) 60,0273,3421 cycles # 4.590 GHz ( +- 0.16% ) 48,3465,9132 instructions # 0.81 insn per cycle ( +- 0.00% ) 7,2748,9750 branches # 556.254 M/sec ( +- 0.00% ) 2101,7155 branch-misses # 2.89% of all branches ( +- 0.13% ) 1.30832 +- 0.00820 seconds time elapsed ( +- 0.63% ) 將差距較明顯的4個數據挑出 | | q_sort | list_sort | | -------- | -------- | -------- | | task-clock | 1640.86 msec | 1307.84 msec | | instructions | 49,5211,3455 | 48,3465,9132 | | cycles | 75,6362,5118 | 60,0273,3421 | | branch-misses | 1.75% of all branches | 2.89% of all branches| 可以看到雖 `q_sort` 在 `branch-misses` 的比例上少於 `list_sort` 但主要 `list_sort` 在執行的 `cycles` 數量少 `q_sort` 滿多的 因此整體 `list_sort` 表現優秀於 `q_sort` ## 論文閱讀 :〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉 ### Abstract 該論文透過 leakage detection test 的方式,結合統計上的 [Welch’s t-test](https://en.wikipedia.org/wiki/Welch's_t-test) 去判斷程式執行是否為常數時間,因是透過統計手法來測量,所以使用上不侷限於特定的硬體平台 ### Step 1: Measure execution time 進行 leakage detection test 時會準備兩種 class 測試資料,這邊是以 fix-vs-random 的方式 * 第一個 class : 全部的輸入資料會被設定成固定值 * 第一個 class : 全部的輸入資料會以隨機的方式來產生 最後將兩種 class 的資料輸入進程式,並記錄執行時間 ### Step 2: Apply post-processing 對記錄到的時間進行後處理 * Cropping : 在程式執行時,有機會受到外部的影響 (系統中斷等),造成記錄的時間涵蓋了一些與實驗無關的部分,因此透過 Cropping 將異常值去除 * Higher-order preprocessing : 根據應用的統計測試不同,可以採取一些其他處理機制 ### Step 3: Apply statistical test 建立虛無假說 : 兩個 class 執行時間分布一致 (因要判斷是否為常數時間,因此在實驗設計上,固定值的 class 要能真的是以常數時間執行) 透過 Welch’s t-test 進行假說檢定,當檢定結果拒絕虛無假說,則可判定該執行時間不為常數 ### 修正 `fixture.c` [commit 855889d](https://github.com/Ackerman666/lab0-c/commit/855889d5ef4ec1872ee83c8b3a2aad2aa4620275) 與原版 `dudect` 最大差別是我發現 `update_statistics()` 沒有做 Cropping 的動作 因此參考 [`dudect/src/dudect.h`](https://github.com/oreparaz/dudect/blob/dc269651fb2567e46755cfb2a13d3875592968b5/src/dudect.h#L302) 的實作 新增 `cmp`, `percentile`, `prepare_percentiles` 來對 `update_statistics` 做 Cropping 處理 最後通過 trace-17-complexity ![image](https://hackmd.io/_uploads/H1kZX-yyR.png)