--- tags: Linux核心設計 --- # 2023q1 Homework1 (lab0) contributed by < [Tonr01](https://github.com/Tonr01/lab0-c) > ## 開發環境 ``` gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 架構: x86_64 CPU 作業模式: 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 供應商識別號: GenuineIntel Model name: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz CPU 家族: 6 型號: 141 每核心執行緒數: 2 每通訊端核心數: 8 Socket(s): 1 製程: 1 CPU max MHz: 4600.0000 CPU min MHz: 800.0000 BogoMIPS: 4608.00 ``` ## 作業要求 * **q_new**: 建立新的「空」佇列; * **q_free**: 釋放佇列所佔用的記憶體; * **q_insert_head**: 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則); * **q_insert_tail**: 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則); * **q_remove_head**: 在佇列開頭 (head) 移去 (remove) 給定的節點; * **q_remove_tail**: 在佇列尾端 (tail) 移去 (remove) 給定的節點; * **q_release_element**: 釋放特定節點的記憶體空間; * **q_size**: 計算目前佇列的節點總量; * **q_delete_mid**: 移走佇列中間的節點, * 詳見 [LeetCode 2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/) * **q_delete_dup**: 在已經排序的狀況,移走佇列中具備重複內容的節點 * 詳見 [LeetCode 82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/) * **q_swap**: 交換佇列中鄰近的節點 * 詳見 [LeetCode 24. Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/) * **q_reverse**: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點; * **q_reverseK**: 類似 q_reverse,但指定每 k 個節點為一組,進行反向順序排列 * 詳見 [LeetCode 25. Reverse Nodes in k-Group](https://leetcode.com/problems/reverse-nodes-in-k-group/) * **q_sort**: 以遞增順序來排序鏈結串列的節點 * 參閱 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 得知實作手法; * **q_merge**: 合併所有已排序的佇列,並合而為一個新的已排序佇列 * 詳見 [LeetCode 23. Merge k Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists/) * **q_descend**: * 參閱 [LeetCode 2487. Remove Nodes From Linked List](https://leetcode.com/problems/remove-nodes-from-linked-list/) ## queue.c 開發過程 ### q_new #### 思路 一開始最簡單的想法是,配置記憶體給head,並檢查是否配置成功。 #### 程式碼 :::danger 出現了 `Segmentation fault occurred. You dereferenced a NULL or invalid pointermake` ,再看完程式碼的 comment `Notice: sometimes, Cppcheck would find the potential NULL pointer bugs` 與巨集後,發現我忽略了 pointer 的指向。 ```c struct list_head *q_new() { struct list_head *head = (struct list_head *) malloc(sizeof(struct list_head)); if (!head) { free(head); return NULL; } return head; } ``` ::: :::success 於是加入 `INIT_LIST_HEAD(head);` ,將 head 的 prev 與 next 都指向自己。 ```diff struct list_head *q_new() { struct list_head *head = (struct list_head *) malloc(sizeof(struct list_head)); if (!head) { free(head); return NULL; } + INIT_LIST_HEAD(head); return head; } ``` ::: ### q_free #### 思路 一開始先檢查 list 是否存在,若不存在則無須進行 `q_free` ,再看完 `list.h` 定義的巨集後,發現 `list_for_each_entry_safe` 適合用來走訪每個 `entry` ,再使用 `q_release_element(entry);` 來釋放該 `entry` 的 `entry->value` 及 `entry->list` ,最後將 head 也釋放掉,釋放整個 list。 #### 程式碼 ```c void q_free(struct list_head *l) { if (!l) return; element_t *entry, *safe; list_for_each_entry_safe (entry, safe, l, list) q_release_element(entry); free(l); } ``` ### q_insert_head #### 思路 首先先檢查 list 是否存在,再配置記憶體給新的 `entry`,並檢查 `entry` 與 `entry->value` 的空間是否配置成功,將節點的value值用`strdup(s);`將s的值複製給它,最後將`new_element`用`list_add`加入到list開頭的位置上。 #### 程式碼 :::danger 有出現 `Test of malloc failure` 錯誤訊息,才發現 `entry` 裡的 `entry->value` 的空間也可能配置失敗,於是加入配置記憶體的檢查。 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *new_element = malloc(sizeof(element_t)); if (!new_element) return false; new_element->value = strdup(s); list_add(&new_element->list, head); return true; } ``` ::: :::success 修改後。 ```diff bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *new_element = malloc(sizeof(element_t)); if (!new_element) return false; new_element->value = strdup(s); + if (!new_element->value) { + free(new_element); + return false; + } list_add(&new_element->list, head); return true; } ``` ::: ### q_insert_tail #### 思路 大致上與 `q_insert_head` 思路一樣,首先先檢查 list 是否存在,再配置記憶體給新的 `entry`,並檢查 `entry` 與 `entry->value` 的空間是否配置成功,將 `entry->value` 用 `strdup(s);` 將s的值複製給它,最後將 `new_element` 用 `list_add_tail` 加入到list最後的位置上。 #### 程式碼 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *new_element = malloc(sizeof(element_t)); if (!new_element) return false; new_element->value = strdup(s); if (!new_element->value) { free(new_element); return false; } list_add_tail(&new_element->list, head); return true; } ``` ### q_remove_head #### 思路 一開始先檢查 list 使否存在與 list 是否為空,再使用 `list_first_entry` 找出第一個 `entry` 的位置,再使用 `list_del_init` 將此 `entry` remove ,並將此 `entry` 的 prev 與 next 都指向自己,最後將 `entry->value` 用 `strncpy` 複製到 sp 裡。 #### 程式碼 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *target = list_first_entry(head, element_t, list); list_del_init(&target->list); if (sp) { strncpy(sp, target->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return target; } ``` ### q_remove_tail #### 思路 大致上與 `q_remove_head` 思路一樣,一開始先檢查 list 使否存在與 list 是否為空,再使用 `list_last_entry` 找出最後一個 `entry` 的位置,再使用 `list_del_init` 將此 `entry` remove ,並將此 `entry` 的 prev 與 next 都指向自己,最後將 `entry->value` 用 `strncpy` 複製到 sp 裡。 #### 程式碼 ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *target = list_last_entry(head, element_t, list); list_del_init(&target->list); if (sp) { strncpy(sp, target->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return target; } ``` ### q_size #### 思路 一開始先檢查 list 使否存在與 list 是否為空,再宣告一個節點用 `list_for_each` 去走訪 list ,並紀錄 list 大小。 #### 程式碼 ```c int q_size(struct list_head *head) { if (!head || list_empty(head)) return 0; int size = 0; struct list_head *li; list_for_each (li, head) size++; return size; } ``` ### q_delete_mid #### 思路 我的想法是用 two-pointer 的方式實作,首先宣告兩個 pointer `pre` 與 `nex` 分別代表 list 的頭跟尾的位置,這兩個 pointer 會一直往中心移動,會有兩種 case * Case 1:Size of list 為奇數 (5個節點),此時當 `pre` 與 `nex` 在同個位置的節點即為中心點。 ![](https://i.imgur.com/vS6ukn6.png) * Case 2:Size of list 為偶數 (6個節點),此時當 `pre` 與 `nex` 交錯時, `pre` 即為中心點。 ![](https://i.imgur.com/yzc8iYZ.png) 用 `list_del` 將該節點從 list 中分離出來,再用 `list_entry` 取得 `entry` 位置,最後釋放該 `entry` 。 #### 程式碼 ```c bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head *pre = head->prev; struct list_head *nex = head->next; // Find the middle of list do { pre = pre->prev; nex = nex->next; } while (pre != nex && pre != nex->next); list_del(pre); element_t *target = list_entry(pre, element_t, list); q_release_element(target); return true; } ``` ### q_delete_dup #### 思路 先用 `list_for_each_entry_safe` 去走訪每個元素 ,分別用 `target` 與 `temp` 去紀錄第一個元素與下一個元素,再檢查它們是否相同,若相同,則釋放掉 `target`,用一個二元變數 `dup` 去紀錄是否有重複,若 `dup == true` ,則 `target` 也為重複的元素,再將其釋放。 在實作時出現 `Segmentation fault occurred. You dereferenced a NULL or invalid pointer` ,發現是因為在比較時, `temp` 存取去到 `head` ,所以加入了 `target->list.next != head` 去使 `temp` 不會去存取到 `head` 。 #### 程式碼 ```c bool q_delete_dup(struct list_head *head) { if (!head || list_empty(head)) return false; bool dup = false; element_t *target, *temp; list_for_each_entry_safe (target, temp, head, list) { if (target->list.next != head && !strcmp(target->value, temp->value)) { dup = true; list_del(&target->list); q_release_element(target); } else if (dup) { dup = false; list_del(&target->list); q_release_element(target); } } return true; } ``` ### q_reverse #### 思路 想到的方法為逐一將 list 走訪,每走訪一個 `entry` 就將該 `entry` 從 list 中分離出來,再加入到 list 的第一個位置。 #### 程式碼 ```c void q_reverse(struct list_head *head) { element_t *entry, *safe; list_for_each_entry_safe (entry, safe, head, list) list_move(&entry->list, head); } ``` ### q_reversek #### 思路 作法跟 `q_reverse` 類似,但不像 `q_reverse` 總是將節點插入第一個位置,這裡使用一個 pointer `insert` 指向一組 k 個節點的第一個插入位置,再使用 `count` 去紀錄走訪的節點數,當走訪的節點數恰 k 個時,將 pointer `insert` 指向下一組 k 個節點的第一個插入位置,也就是上一組的最後一個節點(目前節點的 prev)。 #### 程式碼 ```c void q_reverseK(struct list_head *head, int k) { if (!head || list_empty(head)) return; struct list_head *node = NULL, *safe = NULL, *insert = head; int count = 0; list_for_each_safe(node, safe, head) { if (k > count) list_move(node, insert); else { count = 0; insert = node->prev; } count++; } } ``` ### q_swap #### 思路 `q_swap` 其實就是 `q_reverseK(2)` ,於是用`q_reverseK` 去實作。 #### 程式碼 ```c void q_swap(struct list_head *head) { if (!head || list_empty(head)) return; q_reverseK(head, 2); } ``` ### q_descend #### 思路 從 list 最尾端開始往前做,設 `target` 為最後一個元素、 `prev` 為 `target` 的前一個元素、 `size` 紀錄 list 中剩餘元素個數。 * 若後面的元素比前一個元素大,則將前一個元素移出 list。 * 繼續往前找,直到找到比現在的 `target` 還大的元素,再將 `target` 指向此元素。 * 重複以上步驟直到到 `head` 為止。 #### 程式碼 :::danger 在跑測試時,在執行 `q_free` 後尚有未被刪除的元素,發現因為將元素移出 list 後,並未將其釋放而造成錯誤。 ```c int q_descend(struct list_head *head) { if (!head || list_empty(head)) return 0; int size = q_size(head); struct list_head *target = head->prev, *prev = target->prev; while (target->prev !=head) { element_t *t = list_entry(target, element_t, list); element_t *p = list_entry(prev, element_t, list); if (strcmp(p->value, t->value) < 0) { list_del_init(&p->list); prev = target->prev; size--; } else { target = prev; prev = target->prev; } } return size; } ``` ::: :::success 修改後 ```diff int q_descend(struct list_head *head) { if (!head || list_empty(head)) return 0; int size = q_size(head); struct list_head *target = head->prev, *prev = target->prev; while (target->prev !=head) { element_t *t = list_entry(target, element_t, list); element_t *p = list_entry(prev, element_t, list); if (strcmp(p->value, t->value) < 0) { - list_del_init(&p->list); + list_del(&p->list); + q_release_element(p); prev = target->prev; size--; } else { target = prev; prev = target->prev; } } return size; } ``` > 使用 `diff` 或 `git diff` 產生程式碼差異的列表 > :notes: jserv ::: ### q_sort #### 思路 我選擇採用 merge sort,因為 merge sort 為 stable sorting algorithm ,而其最壞的時間複雜度只有 $O(nlogn)$ ,這裡參考了 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 的實作方法,將 merge sort 分成三個部份,分別是: * `mergelist` * 主要是做 **merge** 的部份,採用非遞迴的方式將兩個 list 以遞增的形式串接在一起。 * `mergesort` * 主要是做 **split** 的部份,使用兩個指標,分別是 `fast` 與 `slow` ,採用 [Floyd Cycle Detection Algorithm](https://medium.com/@orionssl/%E6%8E%A2%E7%B4%A2-floyd-cycle-detection-algorithm-934cdd05beb9) ,當較快的指標到達終點時,較慢的指標會恰好在中心位置,並以遞迴的方式將 list 拆解,再呼叫 `mergelist` 將拆解後的 list 排序並重新組合成一條 list。 * `q_sort` * 因為採用 [Floyd Cycle Detection Algorithm](https://medium.com/@orionssl/%E6%8E%A2%E7%B4%A2-floyd-cycle-detection-algorithmm-934cdd05beb9) 的方式,所以原本的環狀佇列結構會導致找不到中心位置,於是先將原本的環狀 doubly-linked list 轉換成 singly-linked list,等做完 merge sort 後,再將 singly-linked list 轉回環狀 doubly-linked list。 :::warning 注意連字號的位置,書寫為 "doubly-linked list" :notes: jserv 👌 (以修改) ::: #### 程式碼 * `mergelist` ```c struct list_head *mergelist(struct list_head *l1, struct list_head *l2) { struct list_head temp; struct list_head *t = &temp; while (l1 && l2) { element_t *ele1 = list_entry(l1, element_t, list); element_t *ele2 = list_entry(l2, element_t, list); if (strcmp(ele1->value, ele2->value) < 0) { t->next = l1; t = t->next; l1 = l1->next; } else { t->next = l2; t = t->next; l2 = l2->next; } } // Connect the remain list if (l1) t->next = l1; if (l2) t->next = l2; return temp.next; } ``` * `mergesort` ```c struct list_head *mergesort(struct list_head *head) { if (!head->next) return head; struct list_head *fast = head->next; struct list_head *slow = head; // split list while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } // Fast is the middle of list fast = slow->next; slow->next = NULL; struct list_head *left = mergesort(head); struct list_head *right = mergesort(fast); // Merge sorted left and sorted right return mergelist(left, right); } ``` * `q_sort` ```c void q_sort(struct list_head *head) { if (!head || list_empty(head)) return; // Turn the list into Singly linked list head->prev->next = NULL; head->next = mergesort(head->next); // Turn the list into doubly circular linked list struct list_head *temp = head, *next = head->next; while (next) { next->prev = temp; temp = next; next = next->next; } temp->next = head; head->prev = temp; ``` ### q_merge #### 思路 一開始有兩種想法,第一種是使用 `mergelist` 迭代的將兩條 list 合併成一條,但因為 `mergelist` 實作的關係,輸入需要是沒有 `head` 的兩條 list ,所以在迭代前都需要先對 list `head` 做處理,而在合併後還須將 list 變回 doubly-linked list,以上步驟會讓程式碼變得較為冗長。於是採用第二種方法,第二種為先將所有 list 串接在一起,再對這條 list 做 `q_sort` ,這樣一來就無須對 `head` 做變更,整體程式碼可以較為簡短。 `cur` 表示第一條 queue, `temp` 為指向下一條佇列的指標,迭代的將後面的佇列連接到第一條佇列後面,最後再對這條佇列做 `q_sort` ,最後回傳佇列長度。 #### 程式碼 ```c int q_merge(struct list_head *head) { if (!head || list_empty(head)) return 0; queue_contex_t *cur = list_entry(head->next, queue_contex_t, chain); for (struct list_head *temp = head->next->next; temp != head; temp = temp->next) { queue_contex_t *t = list_entry(temp, queue_contex_t, chain); list_splice_init(t->q, cur->q); } q_sort(cur->q); return q_size(cur->q); } ``` ## 引入 list_sort.c 比較自己的 sort.c 效能差異 ### 引入`list_sort` 檔案及修改 首先引入 `list_sort.h` ,刪除不必要的 `header` 。 ```c /* SPDX-License-Identifier: GPL-2.0 */ #ifndef _LINUX_LIST_SORT_H #define _LINUX_LIST_SORT_H #include <stdint.h> #include "list.h" ``` `list_sort.c` 使到兩個巨集 `likely` 與 `unlikely`,定義於`<linux/compiler.h>`,將其定義加入 `list_sort.h` 中,在 `merge_final` 函式中宣告了 `u8 count = 0;` ,將其改成 `uint_8 count = 0;` 。 ```c # define likely(x) __builtin_expect(!!(x), 1) # define unlikely(x) __builtin_expect(!!(x), 0) ``` 最後是將 `list_sort.o` 寫入 `makefile` 裡。 ```c OBJS := qtest.o report.o console.o harness.o queue.o list_sort.o\ random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \ shannon_entropy.o \ linenoise.o web.o ``` 在 `qtest` 中先複製一份 `do_sort` 給 `list_sort` 使用,在 `list_sort.h` 中有定義函式,於是需要自己實作,這個函式是用來比較字串大小,回傳值為 `int` 。 ```c typedef int __attribute__((nonnull(2,3))) (*list_cmp_func_t)(void *, const struct list_head *, const struct list_head *); ``` 所以在 `qtest` 中加入 `cmp` 函式實作。 ```c int cmp(void *priv, const struct list_head *list1, const struct list_head *list2) { element_t *list1_entry = list_entry(list1, element_t, list); element_t *list2_entry = list_entry(list2, element_t, list); return strcmp(list1_entry->value, list2_entry->value) ; } ``` 在 `qtest` 中的命令 `mysort` 與 `list_sort` 都能正常執行。 ### 效能比較 這裡用 `qtest` 中的 `time` 進行時間的比較,有三種測資,分別用命令 `RAND` 加入100000、300000、500000筆隨機資料給 queue 。 #### 測試指令 加入自己寫的 `trace-mysort.cmd` 與 `trace-list_sort.cmd` 到測試資料中, `./qtest -v 3 -f traces/trace-list_sort.cmd` 會執行以下命令。 ```c option fail 0 option malloc 0 new ih RAND 100000/300000/500000 time mysort/list_sort free ``` #### my_sort |測試次數|100000筆|300000筆|500000筆| |-|-|-|-| |1|0.042|0.196|0.407| |2|0.041|0.194|0.419| |3|0.042|0.200|0.430| |average|0.0417|0.1967|0.4187| #### list_sort |測試次數|100000筆|300000筆|500000筆| |-|-|-|-| |1|0.041|0.170|0.368| |2|0.043|0.170|0.388| |3|0.039|0.171|0.364| |average|0.041|0.1703|0.3733| #### 效能差距 ||100000筆|300000筆|500000筆| |-|-|-|-| |效能差距|2%|14%|11%| 因為測試資料都是隨機生成的,加上測試次數不多,所以以 Perf 來分析效能。 :::warning 測試方式不足以反映出上述實作的特性。 :notes: jserv 👌 (以修改) ::: ### 以 [Linux 效能分析工具: Perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) 來分析效能 #### 環境安裝 ``` $ cat "/boot/config-`uname -r`" | grep "PERF_EVENT" ``` 用此指令確認是否安裝 perf。 ``` $ uname -r 5.19.0-43-generic ``` 確認 kernel 版本。 ``` $ sudo apt-get install linux-tools-5.19.0-43-generic linux-cloud-tools-5.19.0-43-generic ``` 安裝 perf。 ``` $ cat /proc/sys/kernel/perf_event_paranoid ``` 確認 perf 權限。 * 2 : 不允許任何量測。但部份用來查看或分析已存在的紀錄的指令仍可使用,如 perf ls、perf report、perf timechart、 perf trace。 * 1 : 不允許 CPU events data。但可以使用 perf stat、perf record 並取得 Kernel profiling data。 * 0 : 不允許 raw tracepoint access。但可以使用 perf stat、perf record 並取得 CPU events data。 * -1: 權限全開。 ``` $ sudo sh -c " echo -1 > /proc/sys/kernel/perf_event_paranoid" ``` 將權限全開 ``` $ sudo sh -c " echo 0 > /proc/sys/kernel/kptr_restrict" ``` 最後如果要檢測 cache miss event,需要先取消 kernel pointer 的禁用。 #### 測試指令 使用`./qtest -v 3 -f traces/trace-list_sort.cmd` 會執行以下命令,這裡以500000筆隨機資料做測試。 ```c option fail 0 option malloc 0 new ih RAND 500000 time mysort/list_sort free ``` 執行 `perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles ./qtest -f traces/trace-mysort.cmd` ,會執行5次上述命令,再去觀察其效能。 #### my_sort ``` Performance counter stats for './qtest -f traces/trace-mysort.cmd' (5 runs): 16,049,539 cache-misses # 48.666 % of all cache refs ( +- 0.18% ) 32,954,397 cache-references ( +- 0.04% ) 2,203,807,317 instructions # 0.74 insn per cycle ( +- 0.00% ) 2,958,082,501 cycles ( +- 0.37% ) 0.65370 +- 0.00448 seconds time elapsed ( +- 0.69% ) ``` #### list_sort ``` Performance counter stats for './qtest -f traces/trace-list_sort.cmd' (5 runs): 14,022,127 cache-misses # 54.505 % of all cache refs ( +- 0.43% ) 25,688,333 cache-references ( +- 0.10% ) 2,251,606,244 instructions # 0.81 insn per cycle ( +- 0.00% ) 2,867,416,957 cycles ( +- 0.90% ) 0.61738 +- 0.00504 seconds time elapsed ( +- 0.82% ) ``` #### 效能 |event data|my_sort|list_sort| |-|-|-| |cache-misses|16,049,539|14,022,127| |cache-references|32,954,397|25,688,333| |instructions|2,203,807,317|2,251,606,244| |cycles|2,958,082,501|2,867,416,95| #### 花費時間 |測試次數|my_sort 花費時間|list_sort 花費時間| |-|-|-| |1|0.357|0.398| |2|0.361|0.412| |3|0.362|0.407| |4|0.364|0.411| |5|0.365|0.402| |average|0.3618|0.406| 可以看出 `list_sort` 的 cache-misses 比 `my_sort` 少很多, `list_sort` 所需的 instructions 與 cycles 都比 `my_sort` 少, `my_sort` 只能達到 `list_sort` 的 89% 效能 (0.3618/0.406 = 0.89) 。 ## 在 qtest 提供新的命令並實作 shuffle ### 利用 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法來實作洗牌(shuffle) 1. 先用 `q_size` 取得佇列的大小 len。 2. 隨機從 0 ~ (len - 1) 中抽出一個數字 random,抽到的數字為節點的位置,再將該節點加到佇列最後位置。 3. 隨著 len 大小變小,直到所有的節點都已經被抽到過,shuffle 就結束。 ### shuffle 流程 | len | random | queue | result | |-|-|-|-| |0–5|5|A B C D E ~~F~~|F| |0–4|3|A B C ~~D~~ E|F D| |0–3|1|A ~~B~~ C E|F D B| |0–2|1|A ~~C~~ E|F D B C| |0–1|0|~~A~~ E|F D B C A| |0–0|0|~~E~~|F D B C A E| ### shuffle 實作 `len` 為佇列的大小,`random` 為隨機數,也同時代表被抽到的節點位置,將 `node` 移動到第 random 個節點位置,再將該節點移動到佇列最後的位置,當每個節點都做過一輪就結束。 ```c void shuffle(struct list_head *head) { if (!head || list_empty(head)) return; int len = q_size(head); while (len) { int random = rand() % len; struct list_head *node = head->next; while (random) { node = node->next; random--; } list_move_tail(node, head); len--; } } ``` 將 `shuffle` 加入到 `qtest` 裡面,這裡的 `do_shuffle` 是參考 `do_reverse` 的實作,因為這兩個函式都是用來改變佇列的位置。 ```c bool do_shuffle(int argc, char *argv[]) { if (argc != 1) { report(1, "%s takes no arguments", argv[0]); return false; } if (!current || !current->q) report(3, "Warning: Calling shuffle on null queue"); error_check(); set_noallocate_mode(true); if (current && exception_setup(true)) shuffle(current->q); exception_cancel(); set_noallocate_mode(false); q_show(3); return !error_check(); } ``` ### 執行結果 ``` # do 5 times shuffle l = [] l = [A] l = [A B] l = [A B C] l = [A B C D] l = [A B C D E] l = [A B C D E F] # shuffle l = [E F D C B A] l = [B A F E C D] l = [E F B C A D] l = [C F D E A B] l = [B D F A E C] l = NULL Freeing queue ``` ## 研讀論文[〈Dude, is my code constant time?〉](https://eprint.iacr.org/2016/1123.pdf),解釋本程式的 “simulation” 模式 ### 研讀論文[〈Dude, is my code constant time?〉](https://eprint.iacr.org/2016/1123.pdf) #### Introduction 這篇論文主要闡述提供了一個新的工具 dudect ,可以不限定任何平台去檢測部份的程式碼是否達到 constant time ,提供 leakage detection 的方法。 #### Leakage detection 這個方法會先去測量兩種不同的輸入的執行時間,並用統計學去判定這兩種執行時間的差異性,其中一種輸入為固定的數值,另一種為隨機數值。在進行統計分析前會先裁掉極值,這些極值可能是測量的誤差或是程式執行被中斷導致。 #### Statistical test 這裡採用 [Welch's t-test](https://en.wikipedia.org/wiki/Welch%27s_t-test) ,為 [Student's t-test](https://en.wikipedia.org/wiki/Student%27s_t-distribution) 的改編版, Welch's t-test 處理兩個樣本間不同的標準差與樣本大小會比較可靠,這個方法會去計算兩個樣本的均值是否相等,並計算出 t 值。其中 $\bar{X}$ 為樣本均值, $s_{\bar{X}}$ 為標準差。 ![](https://i.imgur.com/00ekpCY.png) #### Pre-processing 這裡提到的預處理指的是在進行統計分析前裁剪掉極值的動作,而裁剪的方法為裁剪掉大於 $p$ 百分比的測量值。這部份也就是 simulation 的缺陷,因為沒有將極值裁剪掉,導致影響到測量的速度。 ### 解釋 Simulation 模式 首先在 `trace-17-complexity.cmd` 會先執行 `option simulation 1` ,先開啟 simulation mode ,再呼叫 `it`, `ih`, `rh`, `rt` 四個函式。以呼叫 `ih` 為例,在 `qtest` 中會執行 `bool do_ih` 函式。 ```c if (simulation) { if (argc != 1) { report(1, "%s does not need arguments in simulation mode", argv[0]); return false; } bool ok = is_insert_head_const(); if (!ok) { report(1, "ERROR: Probably not constant time or wrong implementation"); return false; } report(1, "Probably constant time"); return ok; } ``` 可以看到決定執行時間是否為 constant time 且程式正確執行,由函式 `is_insert_head_const` 決定,此函式在 `fixture.h` 定義。 ```c bool is_##op##_const(void) { return test_const(#op, DUT(op)); } ``` 在 `fixture.c` 中 `is_##op##_const` 會回傳函式 `test_const` 的結果 `result` , `result` 為函式 `doit` 的回傳值。 ```c result = doit(mode); ``` 其中 `result` 由兩個函式的回傳值做 AND 而來, `measure()`, `report()`。 ```c static bool doit(int mode) { ... bool ret = measure(before_ticks, after_ticks, input_data, mode); ... ret &= report(); ... return ret; } ``` 首先為函式`measure` ,主要對四種模式 `it`, `ih`, `rh`, `rt` 進行執行正確性的判定,判定依據為執行完該函式,其改變後的佇列大小是否正確。 ```c if (before_size != after_size - 1) return false; ``` 再來是函式 `report` ,主要是用來判定執行時間是否為 constant time 。 ```c /* 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; ``` ### 解決 Simulation 實作的缺陷 首先將參數 $p$ 百分比 `percentiles` 加入到宣告中,但因為作者有多宣告了一種結構,而 Simulation 實作是將宣告直接寫在函式中,於是這裡將 `percentiles` 在 `t_context_t` 結構中宣告。 ```c /* 作者宣告 */ typedef struct { int64_t *ticks; int64_t *exec_times; uint8_t *input_data; uint8_t *classes; dudect_config_t *config; ttest_ctx_t *ttest_ctxs[DUDECT_TESTS]; int64_t *percentiles; <------ } dudect_ctx_t; ``` ```diff typedef struct { double mean[2]; double m2[2]; double n[2]; + int64_t *percentiles; } t_context_t; ``` 再將相關程式引入到專案中,詳細修改在 [commit](https://github.com/sysprog21/lab0-c/commit/01fea1727467d6843ac984652a6e00f35e50d5e2) 中。 ### 測試結果 加入了 `percentiles` 的實作後,測試都能在 constant time 內完成。 ``` --- Trace Points +++ 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 Probably constant time Probably constant time Probably constant time --- trace-17-complexity 5/5 --- TOTAL 5/5 ```