# 2023q1 Homework1 (lab0) contributed by<`aaron881011`> ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0 Copyright (C) 2019 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 Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 16 On-line CPU(s) list: 0-15 Thread(s) per core: 2 Core(s) per socket: 8 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 141 Model name: 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz ``` ## 開發紀錄 ### Function實作 #### q_new 此函式回傳配置記憶體後指向該記憶體指標,在配置失敗的時候則回傳空指標。 由於 `malloc` 失敗時也會回傳空指標,透過判斷該回傳指標是否為空便可以知道是否該對其進行初始化,之後將指標回傳即可。 ```cpp struct list_head *q_new() { struct list_head *head; head = malloc(sizeof(struct list_head)); if (head) { INIT_LIST_HEAD(head); } return head; } ``` #### q_free 使用 linux list api 中的 `list_for_each_entry_safe` 便可以在迭代時對此代的節點進行操作而不必擔心未定義情況發生。 ```cpp void q_free(struct list_head *l) { element_t *entry, *safe; list_for_each_entry_safe (entry, safe, l, list) { free(entry->value); free(entry); }; free(l); } ``` #### q_insert_head (q_insert_tail) 此處只須將節點插入`head->next` 處即可(在 `q_insert_tail` 的情況下則插入 `head->prev` )。需要注意的是由於此函式需要進行許多記憶體操作,可能會在 `malloc` 或是 `strdup` 時失敗,此時需要釋放在失敗前所配置的記憶體避免佔用。 ```cpp bool q_insert_head(struct list_head *head, char *s) { if (!head || !s) { return false; } element_t *element = malloc(sizeof(element_t)); if (!element) { return false; } char *tmp_str = strdup(s); if (!tmp_str) { free(element); return false; } element->value = tmp_str; INIT_LIST_HEAD(&element->list); list_add(&element->list, head); return true; } ``` #### q_remove_head (q_remove_tail) 將位於 `head->next` 的節點移除( `q_remove_tail` 則是移除位於 `head->prev` 的節點), 並複製所代表的元素所包含的 string 至指定的指標下,再將該元素回傳。對 string 進行複製時需要注意其目的指標下所配置的記憶體並加入中止字元 `\0` 。 ```cpp 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(head->next); if (sp && bufsize > 0) { strncpy(sp, element->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return element; } ``` #### q_size 透過迭代過整個 list 並在每一代將長度 `len` +1 便可以得到 list 的長度。 ```cpp 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 使用快慢指標的技巧可以快速的得到 list 的中間項,將該項從 list 中移除後再將其元素何其包含的 string 所配置之記憶體釋放即可。 ```cpp bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) { return false; } struct list_head *slow = head, *fast = head; do { fast = fast->next->next; slow = slow->next; } while (fast != head && fast->next != head); list_del(slow); element_t *ele = list_entry(slow, element_t, list); free(ele->value); free(ele); return true; } ``` #### q_delete_dup 此函式須將 list 中其元素擁有重複 string 的節點刪除,在呼叫此函式前將 list 進行排序,此函式擁有 $O(n)$ 的時間複雜度。在每一次迭代時將此代的元素與後續的元素進行比較,若相同便將其刪除;反之則代表 list 中沒有與此代元素擁有相同 string 的其他元素,直接進行下一次迭代。在下一次迭代前會先判斷是否有刪除過元素,若有刪除過責將此代元素一併刪除。另外未避免未定義情況發生,所有的節點刪除和記憶體釋放都會在下一代執行。 ```cpp bool q_delete_dup(struct list_head *head) { if (!head) return false; if (list_empty(head) || list_is_singular(head)) return true; struct list_head *cur = head->next; int flag = 0; while (cur != head) { element_t *ele_cur = list_entry(cur, element_t, list); struct list_head *tmp_node = cur->next; while (tmp_node != head) { element_t *ele_tmp = list_entry(tmp_node, element_t, list); tmp_node = tmp_node->next; if (strcmp(ele_cur->value, ele_tmp->value) == 0) { flag = 1; list_del(tmp_node->prev); free(ele_tmp->value); free(ele_tmp); } else break; } cur = cur->next; if (flag) { flag = 0; list_del(cur->prev); free(ele_cur->value); free(ele_cur); } } return true; } ``` #### q_swap 此函式將 list 中每兩個節點進行反轉。透過 list api 進行迭代,將此代節點移至其後一個節點之後,下一次迭代便會是元節點的下下個節點,請參考以下示意圖。若 list 長度為 $n$ , 此法僅迭代過 $n/2$ 個節點。 * 此代節點為 A ![](https://i.imgur.com/ifImEAX.png) * 將 A 移至其後一節點( B )之後 ![](https://i.imgur.com/BIamxtC.png) * 下一次迭代節點為 A 的下一節點, 即 C ![](https://i.imgur.com/7IskY7Y.png) ```cpp void q_swap(struct list_head *head) { if (!head) { return; } struct list_head *node; list_for_each (node, head) { if (node->next == head) { break; } list_move(node, node->next); } } ``` #### q_reverse 此函式會將整個 list 反轉。透過 list api 裡的 `list_for_each_safe` 可以操作該次迭代的節點而保證不影響迭代順序,因此調用該巨集並將該次迭代之節點前後反指即可。 ```cpp void q_reverse(struct list_head *head) { if (!head || list_empty(head)) { return; } struct list_head *node, *safe; list_for_each_safe (node, safe, head) { struct list_head *tmp = node->next; node->next = node->prev; node->prev = tmp; } struct list_head *tmp = head->next; head->next = head->prev; head->prev = tmp; } ``` #### q_reverseK 此函式會將 list 中每 $k$ 個節點進行反轉。首先紀錄 list 原先的結尾位置,將在那之前的每 $k$ 個節點使用 `q_reverse` 進行反轉後移至 list 尾端,最後再將湊不齊 $k$ 個的節點們直接移至 list 尾端即可。 ```cpp void q_reverseK(struct list_head *head, int k) { if (!head || list_empty(head) || list_is_singular(head)) { return; } struct list_head left, *tail = head->prev; int len = q_size(head); while (len >= k) { struct list_head *node = head; for (int i = 0; i < k; i++) { node = node->next; } list_cut_position(&left, head, node); q_reverse(&left); list_splice_tail_init(&left, head); len -= k; } list_cut_position(&left, head, tail); list_splice_tail_init(&left, head); } ``` #### q_descend 此函式會將 list 中在節點右端仍有大於節點值的節點的節點刪除,形成一遞減的 list。 從 list 尾端進行迭代,將此次迭代的節點與其前一個節點進行比較,若前一節點的值大於該代節點則刪除前一節點,如此便能保證list為遞減形式。 ```cpp int q_descend(struct list_head *head) { if (!head || list_empty(head)) { return 0; } if (list_is_singular(head)) { return 1; } struct list_head *node = head->prev; while (node != head && node != head->next) { struct list_head *prev = node->prev; element_t *cur_ele = list_entry(node, element_t, list); element_t *prev_ele = list_entry(prev, element_t, list); if (strcmp(cur_ele->value, prev_ele->value) > 0) { list_del(prev); free(prev_ele->value); free(prev_ele); } else { node = prev; } } return q_size(head); } ``` #### q_sort 此函式以 merge sort 實作排序問題,其時間複雜度為 $O(nlog(n))$ 。首先用快慢指標將 list 分成兩部分,再使用 list api 將 list 分成兩個 list,以遞迴方式將其切到長度為 1 後再兩個兩個將其合併。 ```cpp void q_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) { return; } struct list_head *slow = head, *fast = head; do { fast = fast->next->next; slow = slow->next; } while (fast != head && fast->next != head); assert(slow != head); LIST_HEAD(left); list_cut_position(&left, head, slow); q_sort(head); q_sort(&left); q_merge_two(head, &left); } ``` #### q_merge_two 此函式將兩個 list 合併,將 `L2` 合併至 `L1` 中。這裡使用間接指標用來指向指向 list 的第一個節點的指標,藉此可以透過操作間接指標來進行迭代。另外,在這裡加入了 gcc builtin exepctation 來進行判斷以加速表現效果。 ```cpp #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) void q_merge_two(struct list_head *L1, struct list_head *L2) { if (likely(L1 && L2)) { if (unlikely(L1 == L2)) { return; } else if (unlikely(list_empty(L2))) { return; } else if (unlikely(list_empty(L1))) { list_splice_tail_init(L2, L1); return; } else { struct list_head *tail = L1->prev, *node = NULL; while (node != tail && !list_empty(L2)) { element_t *ele_1 = list_first_entry(L1, element_t, list); element_t *ele_2 = list_first_entry(L2, element_t, list); node = strcmp(ele_1->value, ele_2->value) < 0 ? &ele_1->list : &ele_2->list; list_move_tail(node, L1); assert(list_entry(node, element_t, list)->value != NULL); } if (list_empty(L2)) { LIST_HEAD(left); list_cut_position(&left, L1, tail); list_splice_tail_init(&left, L1); } else { list_splice_tail_init(L2, L1); } } } return; } ``` #### q_merge 此函式將複數個 list 進行合併,採用兩兩合併的策略來維持合併後 list 的長度不至於差距太大。此實作法分別從 list 的頭 `head->next` 尾 `head->prev` 以進行合併,並在每次合併後更新 list 的長度。 ```cpp int q_merge(struct list_head *head) { // https://leetcode.com/problems/merge-k-sorted-lists/ if (!head || list_empty(head)) { return 0; } if (list_is_singular(head)) { queue_contex_t *qct = list_first_entry(head, queue_contex_t, chain); return qct->size; } struct list_head *end = head->prev; while (end != head->next) { for (struct list_head *start = head->next; start->prev != end && start != end; start = start->next, end = end->prev) { queue_contex_t *qct_s = list_entry(start, queue_contex_t, chain); queue_contex_t *qct_e = list_entry(end, queue_contex_t, chain); q_merge_two(qct_s->q, qct_e->q); qct_s->size += qct_e->size; qct_e->size = 0; } } queue_contex_t *qct_res = list_first_entry(head, queue_contex_t, chain); return qct_res->size; } ``` ### linux list_sort 閱讀/引入 程式碼解讀參考了 <`eric88525`> 學長的[共筆](https://hackmd.io/@eric88525/list_sort),而比較函數 `cmp` 定義如下: ```cpp #include "list.h" int cmp(void *priv, const struct list_head *a, const struct list_head *b) { element_t *a_entry = list_entry(a, element_t, list); element_t *b_entry = list_entry(b, element_t, list); return strcmp(a_entry->value, b_entry->value); } ``` #### perf 由於 `q_sort` 的實作採用了 top down 的模式,與 `list_sort` 的 bottom up 相比 cache 被參照的次數更少,不斷的遷入遷出容易造成 cache thrashing,測試用指令如下。 * `perf.cmd` 使用 `q_sort` ```shell option fail 0 option malloc 0 new it dolphin 1000000 ih gerbil 1000000 sort ``` * `perf_linux.cmd` 使用 `list_sort` ```shell option fail 0 option malloc 0 new it dolphin 1000000 ih gerbil 1000000 sort_linux ``` 上述所提到的 cache thrashing 可以觀察 cache-misses 的次數,首先使用 `perf stat` 比較執行結果 ```shell $ perf stat -e cache-misses -r 100 ./qtest -v 0 -f perf.cmd Performance counter stats for './qtest -f perf.cmd' (100 runs): 56,959,286 cache-misses ( +- 0.17% ) 0.96087 +- 0.00360 seconds time elapsed ( +- 0.37% ) $ perf stat -e cache-misses -r 100 ./qtest -v 0 -f perf_linux.cmd Performance counter stats for './qtest -f perf_linux.cmd' (100 runs): 46,542,039 cache-misses ( +- 0.12% ) 0.67788 +- 0.00308 seconds time elapsed ( +- 0.45% ) ``` 可以看到兩邊的執行結果,使用 `q_sort` 的 cache-misses 發生次數高上許多,再來使用 `perf record` 細看 ```shell $ perf record -e cache-misses ./qtest -v 0 -f perf.cmd; perf report [ perf record: Woken up 1 times to write data ] [ perf record: Captured and wrote 0.131 MB perf.data (2816 samples) ] Samples: 2K of event 'cache-misses', Event count (approx.): 56888841 Overhead Command Shared Object Symbol 38.82% qtest qtest [.] q_sort ◆ 35.86% qtest qtest [.] q_show ▒ 8.73% qtest libc-2.31.so [.] __strcmp_avx2 ▒ 4.83% qtest qtest [.] q_size ▒ 2.16% qtest qtest [.] q_merge_two ▒ 1.70% qtest [kernel.kallsyms] [k] try_charge_memcg ▒ 1.03% qtest [kernel.kallsyms] [k] kthread_blkcg ▒ 1.00% qtest [kernel.kallsyms] [k] mem_cgroup_charge_statistics.isra.0 ▒ 0.99% qtest [kernel.kallsyms] [k] charge_memcg ▒ 0.96% qtest [kernel.kallsyms] [k] __count_memcg_events ▒ 0.74% qtest qtest [.] do_sort ▒ 0.74% qtest [kernel.kallsyms] [k] cgroup_rstat_updated ▒ 0.48% qtest [kernel.kallsyms] [k] __cgroup_throttle_swaprate ``` ```shell $ perf record -e cache-misses ./qtest -v 0 -f perf_linux.cmd; perf report [ perf record: Woken up 1 times to write data ] [ perf record: Captured and wrote 0.120 MB perf.data (2526 samples) ] Samples: 2K of event 'cache-misses', Event count (approx.): 46093836 Overhead Command Shared Object Symbol 44.55% qtest qtest [.] q_show 24.18% qtest libc-2.31.so [.] __strcmp_avx2 6.01% qtest qtest [.] q_size 4.71% qtest qtest [.] list_sort 4.65% qtest qtest [.] cmp 3.07% qtest qtest [.] merge 2.50% qtest [kernel.kallsyms] [k] try_charge_memcg 1.33% qtest [kernel.kallsyms] [k] __count_memcg_events 1.32% qtest [kernel.kallsyms] [k] kthread_blkcg 1.15% qtest qtest [.] do_sort_linux 1.14% qtest qtest [.] 0x0000000000002710 1.00% qtest [kernel.kallsyms] [k] cgroup_rstat_updated 0.92% qtest [kernel.kallsyms] [k] mem_cgroup_charge_statistics.isra.0 ``` 可以看到兩種函式佔據整個程式的 cache-misses 的百分比相差巨大,`list_sort` 函式所發生的 cache-misses 次數預估(此處僅包含呼叫到的函式,並不嚴謹)為 ${ \frac{4.71+4.65+3.07}{100} * 46093836 = 5729463}$ 次, 而 `q_sort`則為 ${\frac{38.82+2.16}{100}*56888841=23313047}$,差距可達到四倍之多。 ### shuffle 在 `q_test` 中實作了 Fisher-Yates shuffle 的演算法如下,為了維持 coding-style 的一致性,這裡分成 `q_shuffle` 的洗牌方法跟 do_shuffle 的執行洗牌方法。 #### q_shuffle 這裡紀錄了兩個實作方法。由於初次的實作過於 naïve ,在實驗後發現其可被證明不符合 uniform distribution,因此再度實作了新的方法。 * **failed ver.** 此實作將隨機選擇到的節點放至 queue 的結尾,是 [Fisher-Yates 原來的方法](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#Fisher_and_Yates'_original_method)。 :::danger 此實作方法在經過實驗測試後證實不為 uniform distribution。 ::: ```cpp void q_shuffle(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) { return; } int len = q_size(head); for (int i = len - 1; i > 0; i--) { int j = rand() % (i + 1); struct list_head *node = head->next; for (int k = 0; k < j; k++) { node = node->next; } list_move_tail(node, head); } } ``` * **new ver.** ([參考連結](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm)) 此實作將隨機選到的節點與尚未被選擇過的最後方的節點互換,增加了隨機性。 ```cpp! /* Shuffle queue using Fisher-Yates shuffle*/ void q_shuffle(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) { return; } int len = q_size(head); int j = random() % len; struct list_head *node = head->next; if (j != len - 1) { for (int k = 0; k < j; k++) { node = node->next; } struct list_head *tmp = head->prev; list_move(tmp, node); } list_del_init(node); q_shuffle(head); list_add_tail(node, head); } ``` #### do_shuffle ```cpp! static 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 (exception_setup(true)) q_shuffle(current->q); exception_cancel(); set_noallocate_mode(false); q_show(3); return !error_check(); } ``` #### 洗牌的亂度驗證 卡方分佈 使用了皮爾森卡方檢定來驗證 `q_shuffle` 的結果是否遵守 Uniform distribution。 設虛無假說: * $H_0$(虛無假說): shuffle 結果機率相同,符合 uniform distribution * $H_1$(對立假說): shuffle 結果至少有一個機率不同 首先計算 chi-square test statics $X^2 = \sum_{i=1}^{n}\frac{(O_i-E_i)^2}{E_i}$,其中 $O_i$ 為第 i 項的發生次數, $E_i$ 為第 i 項的期望值(由於是 uniform distribution,這裡的期望值為所有情況數量 $n$ 的倒數 $\frac1n$)。 因為所有情況的機率總和為 1 ,其中一種情況的機率將會是 $1-\sum{P(其他情況)}$,所以自由度為 $1-n$,對應的 P-value 則可以透過卡方分佈表來查詢。若 p-value 低於所選擇之信心水準, 則可以證明虛無假說 $H_0$ 為偽,反之則沒有證據證明其為偽。 #### 實驗 執行以下程式: ```python! import subprocess import re from itertools import permutations import random # 測試 shuffle 次數 test_count = 1000000 input = "new\nit 1\nit 2\nit 3\nit 4\n" for i in range(test_count): input += "shuffle\n" input += "free\nquit\n" # 取得 stdout 的 shuffle 結果 command='./qtest -v 3' clist = command.split() completedProcess = subprocess.run(clist, capture_output=True, text=True, input=input) s = completedProcess.stdout startIdx = s.find("l = [1 2 3 4]") endIdx = s.find("l = NULL") s = s[startIdx + 14 : endIdx] Regex = re.compile(r'\d \d \d \d') result = Regex.findall(s) def permute(nums): nums=list(permutations(nums,len(nums))) return nums def chiSquared(observation, expectation): return ((observation - expectation) ** 2) / expectation # shuffle 的所有結果 nums = [] for i in result: nums.append(i.split()) # 找出全部的排序可能 counterSet = {} shuffle_array = ['1', '2', '3', '4'] s = permute(shuffle_array) # 初始化 counterSet for i in range(len(s)): w = ''.join(s[i]) counterSet[w] = 0 # 計算每一種 shuffle 結果的數量 for num in nums: permutation = ''.join(num) counterSet[permutation] += 1 # 計算 chiSquare sum expectation = test_count // len(s) c = counterSet.values() chiSquaredSum = 0 for i in c: chiSquaredSum += chiSquared(i, expectation) print("Expectation: ", expectation) print("Observation: ", counterSet) print("chi square sum: ", chiSquaredSum) print("Degree of freedom: ", len(s) - 1) ``` #### 執行結果: * failde ver. ```shell! lab0-c$ python scripts/shuffle_test.py Expectation: 41666 Observation: {'1234': 41799, '1243': 41782, '1324': 41353, '1342': 41975, '1423': 41501, '1432': 41697, '2134': 41675, '2143': 41581, '2314': 41638, '2341': 41389, '2413': 41212, '2431': 41271, '3124': 41987, '3142': 41747, '3214': 41767, '3241': 41840, '3412': 41865, '3421': 41600, '4123': 42000, '4132': 41286, '4213': 42087, '4231': 41992, '4312': 41595, '4321': 41361} chi square sum: 36.75217203475256 Degree of freedom: 23 ``` 透過計算出了 chi square sum 和自由度[計算 $X^2$](http://courses.atlas.illinois.edu/spring2016/STAT/STAT200/pchisq.html),P-value 在 $0.03$ 左右,低於信心水準 $\alpha = 0.05$。 * new ver. ```shell! Expectation: 41666 Observation: {'1234': 41761, '1243': 41652, '1324': 41504, '1342': 41676, '1423': 42133, '1432': 41460, '2134': 41890, '2143': 41412, '2314': 41528, '2341': 41545, '2413': 41631, '2431': 41642, '3124': 41917, '3142': 41717, '3214': 41604, '3241': 41933, '3412': 41564, '3421': 41580, '4123': 41619, '4132': 41647, '4213': 41560, '4231': 41678, '4312': 41792, '4321': 41555} chi square sum: 15.527048432774931 Degree of freedom: 23 ``` 同樣計算 p value 約等於 $0.8748$, 大於所選的信心水準 $\alpha = 0.05$, 沒有證據顯示虛無假說 $H_0$ 為偽。 #### 繪圖 ```python! import matplotlib.pyplot as plt import numpy as np permutations = counterSet.keys() counts = counterSet.values() x = np.arange(len(counts)) plt.bar(x, counts) plt.xticks(x, permutations) plt.xlabel('permutations') plt.ylabel('counts') plt.title('Shuffle result') plt.savefig("shuffle.png") plt.show() ``` 使用新版本實作來繪圖,各情況分佈如下圖,大致呈 uniform distribution: ![](https://i.imgur.com/bmaOiXn.png) ### dudect #### 概念介紹 本篇論文的概念非常簡單,透過比較程式對於固定和隨機資料所執行的時間來進行統計測試,證明此程式對於兩種類資料所耗費的執行時間分佈是否相同,本篇作者使用的是 t-test 中的 [Welch's t-test ](https://en.wikipedia.org/wiki/Welch%27s_t-test)。 #### t test