# 2024q1 Homework1 (lab0) contributed by < [TSAICHUNGHAN](https://github.com/jeremy90307) > ### Reviewed by `jimmy01240397` 1. 善用巨集減少重複的程式碼 - q_insert_head - q_insert_tail - q_remove_head - q_remove_tail 2. [q_delete_mid](#q_delete_mid):沒必要使用 NULL,可以直接判斷是否回到 head 即可。 3. [q_delete_dup](#q_delete_dup):可使用變數儲存比較結果來減少多餘的分支 ```diff - bool flag = false; + bool now_del = false; element_t *entry, *safe; list_for_each_entry_safe (entry, safe, head, list) { + now_del = entry->list.next != head && + strcmp(entry->value, safe->value) == 0; + if (now_del || flag) { - if (entry->list.next != head && - strcmp(entry->value, safe->value) == 0) { list_del(&entry->list); q_release_element(entry); + flag = now_del; - flag = true; - } else if (flag) { - list_del(&entry->list); - q_release_element(entry); - flag = false; } } ``` >2. 已修改沒必要的 NULL ,我只需將我的迴圈改成判斷是否回到 `head` 即可,因此也不須將環狀鏈結串列斷開。 ```diff - while (fast != NULL && fast->next != NULL) + while (fast != head && fast->next != head) ``` >3. 在 `q_delete_dup` 裡我確實寫的過於冗長,我已改進我的程式碼,下次 coding 會避免沒必要的分支以精簡程式碼。 > 修改後 [commit:579a2ef](https://github.com/sysprog21/lab0-c/commit/579a2ef222b1e07f065f80f99f7c1d96402d0322) > 謝謝 `jimmy01240397` 同學抽空 Review 。 > [name=jeremy] ### Reviewed by `jujuegg` <s> - 不須列出完整程式碼,列出重點部分討論即可 - 有列出實作函式的設計流程,讚 - 可以在程式碼中加入適當的空行,增加可讀性 </s> > 你的洞見呢?與其談「心法」,不如設想「如果我是你」,我會怎麼做,示範一次給同學看,而且檢討自己哪裡沒做好,從而呼應原本學員的不足處。清楚標註學員的不足處 (git commits 和描述)。 > [軟體工程師要學會說故事](https://ruddyblog.wordpress.com/2016/06/18/),本作業要求學員從良性詳盡的批評開始。繼續檢討! > :notes: jserv ## 實驗環境 ```shell $ gcc --version gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 Copyright (C) 2021 Free Software Foundation, Inc. $ lscpu | less 架構: x86_64 CPU 作業模式: 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 供應商識別號: GenuineIntel Model name: Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz CPU 家族: 6 型號: 158 每核心執行緒數: 1 每通訊端核心數: 8 Socket(s): 1 製程: 12 CPU max MHz: 4900.0000 CPU min MHz: 800.0000 BogoMIPS: 7200.00 ``` :::danger 對照 Dictionary.com 對 [implement](https://www.dictionary.com/browse/implement) 一詞的解釋: * to fulfill; perform; carry out: * to put into effect according to or by means of a definite plan or procedure. * Computers. to realize or instantiate (an element in a program), often under certain conditions as specified by the software involved. * to fill out or supplement 「實作」會是符合上述 "implement" 語意的譯詞,取「起」以「創作」的意思,而「製造」本質上是固定輸入並通常產生固定輸出的行為。 $\to$ [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary) ::: ## 佇列實作 - 參考資料: 1. [The Linux 核心API - List Management Functions](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html) 2. [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#Linked-list-%E5%9C%A8-Linux-%E6%A0%B8%E5%BF%83%E5%8E%9F%E5%A7%8B%E7%A8%8B%E5%BC%8F%E7%A2%BC) - 導入作業規範的程式碼縮排風格 clang-format的使用範例 ``` $ clang-format -i queue.c ``` ### q_new 建立一個空佇列,初始化 `struct list_head` ,須先將 `next` 與 `prev` 都指向自身。 :::danger 避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。 ::: 設計流程: 用 malloc 分配記憶體空間給 head 若 head 分配空間無效,則回傳 `NULL` 使用 `list.h` 中的 `INIT_LIST_HEAD()` 來初始化 head 程式碼 ```c struct list_head *q_new() { struct list_head *new = (struct list_head *) malloc(sizeof(struct list_head)); if (!new) return NULL; INIT_LIST_HEAD(new); return new; } ``` :::danger 使用作業規範的程式碼縮排風格。 ::: ### q_free :::danger 避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。 ::: 若 head 為無效,回傳 新增兩個 element 指標 使用 `list_for_each_entry_safe` 可以訪問每個鏈節,且儲存下個連結的<s>鏈節</s> ??? 使用 `list_del` 移除鏈節後,此時還未釋放節點的記憶體空間 使用 free() 使放鏈節記憶體空間 ```c /* Free all storage used by queue */ void q_free(struct list_head *head) { if (!head) return; element_t *node, *temp; list_for_each_entry_safe (node, temp, head, list) { list_del(&node->list); q_release_element(node); } free(head); } ``` ### `q_insert_head`, `q_insert_tail` :::danger 避免非必要的項目列表 (即 `* `),使用清晰明確的漢語書寫。 ::: - 設計流程: 1. 若 head 為無效,則回傳 `false` 2. 分配記憶體空間給新的結構體,若無效則回傳 `false` 3. 使用 `strdup(s)` 將 `char *s` 裡的字串複製給 node->value 指定的位置 4. 檢查 `strdup` 返回的值是否無效,若無效則釋放記憶體空間,並回傳 false 5. 將新結構體用 `list.h` 中的 `list_add` 插到鏈結頭部 :::danger 中文和英文字元之間要有空白字元 (對排版和文字搜尋有利) ::: `q_insert_head` 程式碼 ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *node = malloc(sizeof(element_t)); if (!node) return false; node->value = strdup(s); if (!node->value) { free(node); return false; } list_add(&node->list, head); return true; } ``` `q_insert_tail` 程式碼 ```diff /* Insert an element at tail of queue */ bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; element_t *node = malloc(sizeof(element_t)); if (!node) return false; node->value = strdup(s); if (!node->value) { + free(node); return false; } list_add_tail(&node->list, head); return true; } ``` ### `q_remove_head`,`q_remove_tail` - 設計流程: 1. 檢查 head 是否無效,若無效則傳 `NULL` 2. 用 `list_first_entry` 來取得 list 的第一個 element 3. 若 `sp==NULL && bufsize <= 0` 則回傳 NULL 4. 用 `strncpy()` 將 `node->value` 所指向的字串符複製到 `sp` ,最多複製bufsize個 5. 用 `list_del` 移除list中的節點 `q_remove_head` 程式碼 ```diff /* 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 *node = list_first_entry(head, element_t, list); if (!sp && bufsize <= 0) return NULL; - strncpy(sp, node->value, bufsize); + strncpy(sp, node->value, bufsize - 1); + sp[bufsize - 1] = 0; list_del(&node->list); return node; } ``` `q_remove_tail` 程式碼 ```diff /* 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 *node = list_last_entry(head, element_t, list); if (!sp && bufsize <= 0) return NULL; - strncpy(sp, node->value, bufsize); + strncpy(sp, node->value, bufsize - 1); + sp[bufsize - 1] = 0; list_del(&node->list); return node; } ``` 更動: 在字串的結尾補上 `\0` 表示結束 ### q_size - 設計流程: 1. 檢查head是否無效,若無效則回傳0 2. 建立新的結構體,使用 `list_for_each_entry` 從 head 開始訪問每個鏈節,每經過一個總數加一 程式碼 ```diff /* Return number of elements in queue */ int q_size(struct list_head *head) { int sum = 0; - if (!head) + if (!head || list_empty(head)); return 0; - element_t *node = malloc(sizeof(element_t)); + element_t *node; list_for_each_entry (node, head, list) { sum++; } return sum; } ``` 更動: 此部份更動已寫在後續章節「使用 Valgrind 分析記憶體問題」中 ### q_delete_mid - 設計流程: 1. 先判斷若鏈節為空或只有單一個,則回傳 `false` 2. 使用快慢指標,當快指標到尾時,慢指標會剛好到中間 3. 此時使用 `list_entry` 由慢指標得知結構體,再用 `q_release_element` 進行記憶體的釋放 程式碼 ```diff /* Delete the middle node in queue */ if (!head || list_empty(head)) return false; - struct list_head *prev = head->prev; - head->prev->next = NULL; struct list_head *fast = head->next; struct list_head *slow = head->next; - while (fast != NULL && fast->next != NULL) { + while (fast != head && fast->next != head) { fast = fast->next->next; slow = slow->next; } list_del(slow); // prev->next = slow->next; q_release_element(list_entry(slow, element_t, list)); - head->prev = prev; - prev->next = head; return true; } ``` 更動 1: 當初在設計時忘記是個循環的雙向鏈結串列,因此將鏈結串列的循環給斷開,再最後才把循環接起來。 更動 2: 修改自 `jimmy01240397` 的建議,只須將迴圈條件設置成當 `fast` 指標指向 `head` 即可。 ### q_delete_dup - 設計流程 1. 若 head 為無效或空的鏈節串列,回傳 false 2. 用 `list_for_each_entry_safe` 訪問每個節點,並紀錄下個節點跟當下的節點的字串進行比較 3. 若重複則用 `flag` 作記號來判斷是否刪除 程式碼 ```diff /* 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 || list_empty(head)) return false; bool flag = false; + bool now_del = false; element_t *entry, *safe; list_for_each_entry_safe (entry, safe, head, list) { - if (entry->list.next != head && - strcmp(entry->value, safe->value) == 0) { + now_del = entry->list.next != head && strcmp(entry->value, safe->value) == 0; + if(now_del || flag) + { list_del(&entry->list); q_release_element(entry); - flag = true; - } else if (flag) { - list_del(&entry->list); - q_release_element(entry); - flag = false; + flag = now_del; } } return true; } ``` 修改自 `jimmy01240397` 的建議,減少沒必要的分支以精簡程式碼。 ### q_swap - 設計流程: 1. 若 head 為無效或為空的鏈節串列,則回傳 2. 使用 `list_move` 將 `cur` 的下個節點移至 `newhead` 的下個節點來進行 swap ,再將 `newhead` 設為 `cur` 來進行下組的 swap 程式碼 ```diff /* Swap every two adjacent nodes */ void q_swap(struct list_head *head) { // https://leetcode.com/problems/swap-nodes-in-pairs/ - if (head == NULL) + if (!head || list_empty(head)) return; - struct list_head *cur = head->next; - struct list_head *newhead = cur; + struct list_head *newhead = head, *cur = newhead->next; - for (; cur != head && cur->next != head; - cur = cur->next, newhead = newhead->next->next) { - list_move(cur, newhead->next); - } + for (; cur != head && cur->next != head; newhead = cur, cur = cur->next) { + list_move(cur->next, newhead); + } } ``` 更動: 參考 [chiangkd](https://hackmd.io/@chiangkd/2023spring-lab0-c#q_swap),更改swap實做使程式更直觀。 ### q_reverse - 設計流程: 1. 若head為無效或鍊結串列為空則回傳 2. 用 `list_for_each_safe` 可以訪問每個節點的同時保留下個節點並進行 reverse 3. 最後因為 `list_for_each_safe` 定義上的關係 `node != head` ,因此必須另外定義head的 `next` 及 `prev` 程式碼 ```c void q_reverse(struct list_head *head) { if (!head || head->next == NULL) return; struct list_head *node, *safe; list_for_each_safe (node, safe, head) { node->next = node->prev; node->prev = safe; }; safe = head->next; head->next = head->prev; head->prev; } ``` 待改進: 使用 `list_move` 使程式碼更簡潔易讀 :::danger 中文和英文字元之間要有空白字元 (對排版和文字搜尋有利) ::: ### q_reversek 與 `q_swap` 實作相似,利用 `list_move` 可以用更簡潔的方式表示。以 k 個節點為一組進行 reverse ,在將 `newhead` 變更為前一組的最後一個節點。 - 設計流程: 1. 若 head 為無效或為空的鏈結串列,則回傳 2. 宣告 `list_head` 的指標 `curr` 及 `newhead` 指向 `head` 3. 以 k 個節點一組,計算總共會進行幾組的 reverse 4. 在每組 reverse 中, `curr->next`每次會被移除到`newhead`的下個節點,經過(k-1)次的循環後`curr`會間接的變成尾巴的節點。 5. 將 `curr` 變為新的頭節點 程式碼: ```diff /* 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)) return; struct list_head *newhead = head, *curr = newhead->next; int times = q_size(head) / k; for (int i = 0; i < times; i++) { for (int j = 1; j < k; j++) { list_move(curr->next, newhead); } newhead = curr; + curr = newhead->next; } } ``` ### q_sort 參考[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-LeetCode-21-Merge-Two-Sorted-Lists)和學長 [wanghanchi](https://hackmd.io/@wanghanchi/linux2023-lab0#q_sort) 中合併兩個鏈節串列並作排序的方法。 :::danger 避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。 ::: - 設計流程: - `mergesort` 1.檢查鏈節串列是否為空或無效 2.利用快慢指標找到中間的節點並且切成兩個獨立的 `list_head` 3.呼叫自己進行迭代,最後傳入 `q_merge_two` - `q_merge_two` 1.使用 indirect pointer 來訪問每個節點 2.逐一比較`list1`與`list2`字串大小 3.若 `list1` 或 `list2` 走到盡頭,剩餘的一方將會接續排序在尾巴 - `q_sort` 1.檢查鏈節串列是否為空或無效 2.將環狀結構斷開 3.將 `head->next` 的值帶入 `mergesort` 函式 4.最後因為雙向鏈結的 `prev` 已亂掉,因此重新定義,並把環狀結構接回 #### mergesort ```c struct list_head *mergesort(struct list_head *l) { if (!l || l->next == NULL) return l; struct list_head *fast = l, *slow = l; while (fast->next != NULL && fast->next->next != NULL) { fast = fast->next->next; slow = slow->next; } struct list_head *mid = slow->next; slow->next = NULL; return (q_merge_two(mergesort(l), mergesort(mid))); } ``` #### q_merge_two ```diff /*Merge the two lists in a one sorted list*/ struct list_head *q_merge_two(struct list_head *list1, struct list_head *list2) { struct list_head *new_head = NULL; struct list_head **ptr = &new_head; - element_t *list1_entry = list_entry(list1, element_t, list); - element_t *list2_entry = list_entry(list2, element_t, list); for (struct list_head **node = NULL; list1 && list2; + *node = (*node)->next) { + node = strcmp(list_entry(list1, element_t, list)->value, + list_entry(list2, element_t, list)->value) >= 0 + ? &list2 + : &list1; *ptr = *node; ptr = &(*ptr)->next; } *ptr = (struct list_head *) ((uint64_t) list1 | (uint64_t) list2); return new_head; } ``` 更動: 在我執行`trace-04-ops`時 sort 總是無法順利進行,輸出如下: ``` $ ./qtest -v 3 -f traces/trace-04-ops.cmd # Test of insert_head, insert_tail, size, swap, and sort l = [] l = [gerbil] l = [bear gerbil] l = [dolphin bear gerbil] l = [bear dolphin gerbil] Removed bear from queue l = [dolphin gerbil] l = [bear dolphin gerbil] Queue size = 3 l = [bear dolphin gerbil] l = [bear dolphin gerbil meerkat] l = [bear dolphin gerbil meerkat bear] l = [bear dolphin gerbil meerkat bear gerbil] Queue size = 6 l = [bear dolphin gerbil meerkat bear gerbil] ERROR: Not sorted in ascending order l = [bear meerkat gerbil bear dolphin gerbil] Removed bear from queue l = [meerkat gerbil bear dolphin gerbil] Removed meerkat from queue l = [gerbil bear dolphin gerbil] Removed gerbil from queue l = [bear dolphin gerbil] Removed bear from queue l = [dolphin gerbil] Queue size = 2 l = [dolphin gerbil] Freeing queue ``` 此時我才發現佇列並不會跟著鏈結串列一起訪問節點,因此把 `list_entry` 帶入迴圈跟著存取每個節點。 :::danger access 的翻譯是「存取」,而非「訪問」(visit) ::: #### q_sort ```c /* Sort elements of queue in ascending/descending order */ void q_sort(struct list_head *head, bool descend) { if (head == NULL || list_empty(head)) return; head->prev->next = NULL; head->next = mergesort(head->next); struct list_head *cur = head; struct list_head *next = head->next; while (next) { next->prev = cur; cur = next; next = next->next; } cur->next = head; head->prev = cur; } ``` ### q_descend 題目說明 : [2487. Remove Nodes From Linked List](https://leetcode.com/problems/remove-nodes-from-linked-list/description/) 參考 [wanghanchi](https://hackmd.io/@wanghanchi/linux2023-lab0#q_descend) 的解法,在單向鏈節串列中可以使用`reverse`來更加簡潔的表達,在環狀雙向鏈節串列中使用`prev`就能更輕易的解決。 - 設計流程: 1. 若 `head` 為無效或為空的鏈節串列,回傳零 2. 使用 `list_head` 的指標 `curr` 指向 `head->prev` 及 `prev` 指向 `curr->prev` 3. 使用 `list_entry` 得到佇列裡的資訊 4. 在迴圈裡使用 `strcmp()` 進行比較,若 `curr_entry` 的字串大於 `prev_entry` ,則刪除 `prev` 節點,反之使 `curr` 移動到 `prev` 所在節點 程式碼 ```diff int q_descend(struct list_head *head) { // https://leetcode.com/problems/remove-nodes-from-linked-list/ if (!head || head->next == NULL) return 0; - struct list_head *curr = head->prev, *prev = curr->prev; + struct list_head *curr = head->prev; - element_t *curr_entry = list_entry(curr, element_t, list); - element_t *prev_entry = list_entry(prev, element_t, list); - while (prev != head) { + while (curr != head && curr->prev != head) { + element_t *curr_entry = list_entry(curr, element_t, list); + element_t *prev_entry = list_entry(prev, element_t, list); if (strcmp(curr_entry->value, prev_entry->value) > 0) { - list_del(prev); + list_del(&prev_entry->list); q_release_element(prev_entry); } else { - curr = prev; + curr = curr->prev; } } return q_size(head); } ``` 更動: 1. 減少指向 `list_head` 的指標使用,使程式碼更精簡 2. 變更 `list_entry` 的位置,使每次迴圈都能訪問到佇列位置 ### q_merge 參考 [chiacyu](https://hackmd.io/@chiacyu/SJxLeQt6i#q_merge) :::danger 避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。 ::: 檢查 head 是否為空或無效 新增一個 `list_head` 名為 `temp` 使用 `list_for_each_entry_safe` 訪問每個佇列, 用 `list_splice_init` 將每個節點移動到 `temp` 的鏈結串列,並將佇列 `curr->q` 的頭指標初始化為空佇列 用 `q_sort` 進行排序 使用 `list_splice_init` 將排序後的 `temp` 鏈結串列合併到 `head` 鏈結串列的第一個隊列中 回傳合併後的節點數量 ```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; } struct list_head temp; INIT_LIST_HEAD(&temp); queue_contex_t *curr, *safe; list_for_each_entry_safe(curr, safe, head, chain){ list_splice_init(curr->q, &temp); } q_sort(&temp,false); list_splice_init(&temp, list_first_entry(head, queue_contex_t, chain)->q); return q_size(head); } ``` :::danger 1. 移除非必要的 `:::info` 和 `:::spoiler`,讓行文清晰且流暢。 2. 中文和英文字元之間要有空白字元 (對排版和文字搜尋有利) 3. HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」 4. 改進漢語表達。 ::: ### 目前跑分 ``` $ make test scripts/driver.py -c ... +++ 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 ERROR: Probably not constant time or wrong implementation ERROR: Probably not constant time or wrong implementation --- trace-17-complexity 0/5 --- TOTAL 95/100 ``` ## 研讀論文並改進 dudect 作業要求:[論文〈Dude, is my code constant time?〉重點提示](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-d#-%E8%AB%96%E6%96%87%E3%80%88Dude-is-my-code-constant-time%E3%80%89%E9%87%8D%E9%BB%9E%E6%8F%90%E7%A4%BA) 論文相關筆記紀錄 : [筆記](https://hackmd.io/@jeremytsai/is_my_code_constant_time) ### 解析 `LAB0-C/dudect` **`dudect/constant.c`** `measure` 主要測試 `insert` 和 `remove` 功能是否正常。 其測試的方法為用佇列的大小判斷,下方程式碼從 `case DUT(insert_head)` 中擷取。 ```c 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; ``` **`dudect/ttest.c`** 先看結構體 ```c typedef struct { double mean[2]; double m2[2]; double n[2]; } t_context_t; ``` `t_init` 主要用作初始化 `t_context_t`這個結構體 ```c void t_init(t_context_t *ctx) { for (int class = 0; class < 2; class ++) { ctx->mean[class] = 0.0; ctx->m2[class] = 0.0; ctx->n[class] = 0.0; } return; } ``` `t_push` 看似用來確認穩定狀態 ```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]); } ``` `t_compute` 下方程式碼為 Welch's test 公式 : $t=\frac{\overline{X_0}-\overline{X_1}}{\sqrt{\frac{s^2_1}{N_1}+\frac{s^2_2}{N_2}}}$ ```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; } ``` **`dudect/fixture.c`** ```c /* Interface to test if function is constant */ #define _(x) bool is_##x##_const(void); DUT_FUNCS #undef _ ``` `is_##x##_const` 函式由前置處理器 [concatenation](https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html) 處理,當中 `DUT_FUNCS` 包含 `insert_head`、 `insert_tail`、`remove_head`、`remove_tail` > 相關知識可以參考 [你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor) `differentiate()` 主要用作計算執行時間 ```c for (size_t i = 0; i < N_MEASURES; i++) exec_times[i] = after_ticks[i] - before_ticks[i]; ``` `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; ``` `doit()` `ret` 接收 `measure` 測試 `insert` 、 `remove` 是否都正常的布林值。 `update_statistics` 接收來自 `differentiate` 的執行時間結果和 classes。 最後 `ret` 檢查是否為 constant time 。 ```c bool ret = measure(before_ticks, after_ticks, input_data, mode); differentiate(exec_times, before_ticks, after_ticks); update_statistics(exec_times, classes); ret &= report(); ... return ret; ``` ### dudect實作原理 在 `traces/trace-17-complexity.cmd` 中看到第一行為 `option simulation 1` ,可見 simulation 為檢驗時間複雜度的開關,因此先來了解 simulation 與 dudect 的關聯。 在 `qtest.c` 中看到這段 ```c if (simulation) { if (argc != 1) { report(1, "%s does not need arguments in simulation mode", argv[0]); return false; } bool ok = pos == POS_TAIL ? is_insert_tail_const() : 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; ``` 程式碼更新於 [commit : bdcfae3](https://github.com/sysprog21/lab0-c/commit/bdcfae37505e671c5ba028b8d95fe0e7e40b2afc) 用 `pos == POS_TAIL ? is_insert_tail_const()` 可以用 `pos` 選擇要插入的位置。 從上述程式碼中看到似乎是用 `is_insert_head(tail)_const` 去回傳是否為 constant time 。 ```c 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; } ``` 預設測試 `TEST_TRIES` 次,每次測試時先呼叫 `init_once()` 對 `t_context_t` 結構體進行初始化,接著進入 `doit` ,由 `prepare_inputs` 輸出提供的值 ,經由 `measure` 測試功能是否正常及 `differentiate` 得到執行時間,再將計算得到的執行時間、 classes 輸入給 `update_statistics` ,若函式都成功執行且為 constant time ( `report()` 回傳 `true` ) 則 `ret` 回傳 `true` 。 ### 修改 `LAB0-C/dudect` 達到時間常數 在 `LAB0-C/dudect` 中相較原程式碼 [dudect.h](https://github.com/oreparaz/dudect/blob/master/src/dudect.h) 少了 `percentile()` 功能,即少了裁切測量值。 因此將原始碼的 percentile 加入 `LAB0-C/dudect` 中並修改,最後終於讓我看到了卡比之星。 > 修改內容 [commit d04d313](https://github.com/sysprog21/lab0-c/commit/d04d3133f5b95285e83302f96ed18cd861b2e936) > ![image](https://hackmd.io/_uploads/rJkbLsfJR.png) ### 檢討 讀了一遍這篇論文,對於完全沒有統計基礎的人來說十分挫折,我並不能完全理解裡面分析及理論想表達的內容,反而是透過看同學們的筆記一點一點紀錄才大致了解實作原理,後續有時間再回來複習。 ## 在 qtest 提供新的命令 shuffle 詳細內容可以參考我的 [commit:d0493b2](https://github.com/sysprog21/lab0-c/commit/d0493b28ac9c10fbde4d0ea442f62d9e2619ea74) 和 [commit:c091ea3](https://github.com/jeremy90307/lab0-c/commit/c091ea3db01600d9b2f41bc9b6a16d64930ad411) 設計邏輯遵循 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm) 裡的 `The modern algorithm` 下方表格為實作概念: |Times| Range | Roll | Scratch |Result | |--| -------- | -------- | -------- |-- | |0| | | 1 2 3 4 5 6 7 | | |1| 0~6 | 2 | 1 2 7 4 5 6 | 3 | |2| 0~5 | 5 | 1 2 7 4 5 | 6 3 | |3| 0~4 | 3 | 1 2 7 5 | 4 6 3 | |4| 0~3 | 1 | 1 5 7 |2 4 6 3 | |5| 0~2 | 1 | 1 7 |5 2 4 6 3 | |6| 0~1 | 1 | 1 |7 5 2 4 6 3 | |7| 0~0 | 0 | |1 7 5 2 4 6 3 | 在 `qtest` 中輸出結果 ``` cmd> new l = [] cmd> it 1 l = [1] cmd> it 2 l = [1 2] cmd> it 3 l = [1 2 3] cmd> it 4 l = [1 2 3 4] cmd> it 5 l = [1 2 3 4 5] cmd> it 6 l = [1 2 3 4 5 6] cmd> it 7 l = [1 2 3 4 5 6 7] cmd> shuffle l = [5 6 3 2 1 7 4] ``` 接著導入 [M01: lab0](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-d#%E6%B8%AC%E8%A9%A6%E7%A8%8B%E5%BC%8F) 所提供的驗證程式輸出 ``` Expectation: 41666 Observation: {'1234': 41792, '1243': 41748, '1324': 41661, '1342': 41497, '1423': 41519, '1432': 41770, '2134': 41175, '2143': 41992, '2314': 41550, '2341': 41643, '2413': 41633, '2431': 41949, '3124': 41780, '3142': 41597, '3214': 41703, '3241': 41754, '3412': 41483, '3421': 41258, '4123': 41648, '4132': 41651, '4213': 41983, '4231': 41864, '4312': 41753, '4321': 41597} chi square sum: 21.73297172754764 ``` ![Figure_1](https://hackmd.io/_uploads/Sk_otx7yR.png) shuffle 的頻率是大致符合 Uniform distribution ## 使用 Valgrind 分析記憶體問題 ### 使用案例 由於我在 `$make check` 總是出現有 `ERROR: Freed queue, but 2 blocks are still allocated` 。 ``` $ make check ./qtest -v 3 -f traces/trace-eg.cmd # Demonstration of queue testing framework # Use help command to see list of commands and options # Initial queue is NULL. l = NULL # Create empty queue l = [] # See how long it is Queue size = 0 l = [] # Fill it with some values. First at the head l = [dolphin] l = [bear dolphin] l = [gerbil bear dolphin] # Now at the tail l = [gerbil bear dolphin meerkat] l = [gerbil bear dolphin meerkat bear] # Reverse it l = [bear meerkat dolphin bear gerbil] # See how long it is Queue size = 5 l = [bear meerkat dolphin bear gerbil] # Delete queue. Goes back to a NULL queue. l = NULL ERROR: There is no queue, but 2 blocks are still allocated # Exit program Freeing queue ERROR: Freed queue, but 2 blocks are still allocated make: *** [Makefile:57:check] 錯誤 1 ``` 一開始看完以為問題出在 `q_free` 上,因此花費了大量時間反覆修改測試,直到我看了 [以 Valgrind 分析記憶體問題](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-b#%E6%AA%A2%E6%B8%AC%E9%9D%9E%E9%A0%90%E6%9C%9F%E7%9A%84%E8%A8%98%E6%86%B6%E9%AB%94%E6%93%8D%E4%BD%9C%E6%88%96%E7%A8%8B%E5%BC%8F%E5%9F%B7%E8%A1%8C%E9%80%BE%E6%99%82) 才開始使用到工具分析計體資訊,以下為輸出結果。 ``` $ valgrind ./qtest < traces/trace-eg.cmd l = NULL l = [] Queue size = 0 l = [] ==26769== Conditional jump or move depends on uninitialised value(s) ==26769== at 0x484ED28: strlen (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==26769== by 0x10DC6B: strsave_or_fail (report.c:254) ==26769== by 0x10E551: parse_args (console.c:152) ==26769== by 0x10E551: interpret_cmd (console.c:200) ==26769== by 0x10F1F0: run_console (console.c:691) ==26769== by 0x10D3C3: main (qtest.c:1258) ==26769== ==26769== Conditional jump or move depends on uninitialised value(s) ==26769== at 0x484F02A: strncpy (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==26769== by 0x10DCE5: strncpy (string_fortified.h:95) ==26769== by 0x10DCE5: strsave_or_fail (report.c:266) ==26769== by 0x10E551: parse_args (console.c:152) ==26769== by 0x10E551: interpret_cmd (console.c:200) ==26769== by 0x10F1F0: run_console (console.c:691) ==26769== by 0x10D3C3: main (qtest.c:1258) ==26769== l = [dolphin] l = [bear dolphin] l = [gerbil bear dolphin] l = [gerbil bear dolphin meerkat] l = [gerbil bear dolphin meerkat bear] l = [bear meerkat dolphin bear gerbil] Queue size = 5 l = [bear meerkat dolphin bear gerbil] l = NULL ERROR: There is no queue, but 2 blocks are still allocated Freeing queue ERROR: Freed queue, but 2 blocks are still allocated ==26769== 128 bytes in 2 blocks are still reachable in loss record 1 of 1 ==26769== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==26769== by 0x10F2E7: test_malloc (harness.c:133) ==26769== by 0x10F90F: q_size (queue.c:104) ==26769== by 0x10B60F: do_size (qtest.c:560) ==26769== by 0x10DFD1: interpret_cmda (console.c:181) ==26769== by 0x10E586: interpret_cmd (console.c:201) ==26769== by 0x10F1F0: run_console (console.c:691) ==26769== by 0x10D3C3: main (qtest.c:1258) ==26769== ``` 這時我才發現可能是我的 `q_size` 造成的問題,以下是我當時的 `q_size` 及更改後: ```diff /* Return number of elements in queue */ int q_size(struct list_head *head) { int sum = 0; - if (!head) + if (!head || list_empty(head)); return 0; - element_t *node = malloc(sizeof(element_t)); + element_t *node; list_for_each_entry (node, head, list) { sum++; } return sum; } ``` 原來我這時在設計 `q_size` 時自作聰明的多幫 `node` 設置 malloc ,下次在設計時更應該思考配置記憶體的必要。 這問題現在看似很簡單,但在當時 `$ make check` 的情況看來,我只會認為一定是我的 `q_free` 在某個地方出錯,我更應該善用工具來分析問題。 --- ## 實作 list_sort.c ### 研讀 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 感謝 [RinHizakura](https://hackmd.io/@RinHizakura/HkEuhNwGO#list_sort), [chiangkd](https://hackmd.io/@chiangkd/2023spring-lab0-c#%E7%A0%94%E8%AE%80-list_sortc-%E4%B8%A6%E5%BC%95%E5%85%A5%E5%B0%88%E6%A1%88), [Risheng](https://hackmd.io/@Risheng/list_sort) :::danger 不該只有致謝,你的洞見呢? ::: #### `__attribute` 可以設置函式屬性(Function Attribute)、變量屬性(Variable Attribute)和類型屬性(Type Attribute)。 > 其中函式屬性可以幫助開發者把一些特性添加到函數聲明中,從而可以使編譯器在錯誤檢查方面的功能更強大。`__attribute__` 機制也很容易同非GNU應用<s>程序</s>> 做到<s>兼容</s> 之功效。 :::danger 研讀[資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)及[詞彙對照表](https://hackmd.io/@l10n-tw/glossaries),調整用語,並改進你的漢語表達。 ::: <s> 參考自 [GNU C `__attribute__` 機制簡介](https://huenlil.pixnet.net/blog/post/26078382) </s> :::danger 為何不查閱 GCC 手冊呢? ::: ```c __attribute__((nonnull(2,3))) ``` 其中 2、3 代表地二、三個引數不能為空 參考 [\_\_attribute__((nonnull)) function attribute](https://developer.arm.com/documentation/dui0375/g/Compiler-specific-Features/--attribute----nonnull---function-attribute) > This function attribute specifies function parameters that are not supposed to be null pointers. This enables the compiler to generate a warning on encountering such a parameter #### merge ```c __attribute__((nonnull(2,3,4))) static struct list_head *merge(void *priv, list_cmp_func_t cmp, struct list_head *a, struct list_head *b) { struct list_head *head, **tail = &head; for (;;) { /* if equal, take 'a' -- important for sort stability */ if (cmp(priv, a, b) <= 0) { *tail = a; tail = &a->next; a = a->next; if (!a) { *tail = b; break; } } else { *tail = b; tail = &b->next; b = b->next; if (!b) { *tail = a; break; } } } return head; } ``` 先定義函式第 2、3、4 不能為 `NULL` 在 [linux/include/linux/list_sort.h](https://github.com/torvalds/linux/blob/master/include/linux/list_sort.h) 中找到 cmp 的 `typedef` ```c typedef int __attribute__((nonnull(2,3))) (*list_cmp_func_t)(void *, const struct list_head *, const struct list_head *); ``` 可以確定 `cmp` 是函數指標,且回傳 `int` ,參數分別為 `void *`、兩個 `struct list_head *` - `cmp` > 0 ,即為 `a` 在 `b` 之後 - `cmp` <= 0 ,即為 `a` 在 `a` 之前 因 list_sort 為 stable sort ,故不需特別探討小於和等於 0 的區別。 > [stable sort](https://en.wikipedia.org/wiki/Category:Stable_sorts) : Stable sorting algorithms maintain the relative order of records with equal keys (i.e. values). That is, a sorting algorithm is stable if whenever there are two records R and S with the same key and with R appearing before S in the original list, R will appear before S in the sorted list. See here for a more complete description. > ex : > 排序前 : 3,5,19,1,3*,10 > 排序後 : 1,3,3*,5,10,19 > 兩個3、3*排序前後順序相同 #### merge_final ``` * Combine final list merge with restoration of standard doubly-linked * list structure. This approach duplicates code from merge(), but * runs faster than the tidier alternatives of either a separate final * prev-link restoration pass, or maintaining the prev links * throughout. ``` 與 `merge` 作法相似,差別在於最終連結 `prev` 回上一個節點,形成雙向環狀鏈結串列,在速度上比起每次 merge 後再接 `prev` 來的更快。 ```c __attribute__((nonnull(2,3,4,5))) static void merge_final(void *priv, list_cmp_func_t cmp, struct list_head *head, struct list_head *a, struct list_head *b) { struct list_head *tail = head; u8 count = 0; for (;;) { /* if equal, take 'a' -- important for sort stability */ if (cmp(priv, a, b) <= 0) { tail->next = a; a->prev = tail; tail = a; a = a->next; if (!a) break; } else { tail->next = b; b->prev = tail; tail = b; b = b->next; if (!b) { b = a; break; } } } /* Finish linking remainder of list b on to tail */ tail->next = b; do { /* * If the merge is highly unbalanced (e.g. the input is * already sorted), this loop may run many iterations. * Continue callbacks to the client even though no * element comparison is needed, so the client's cmp() * routine can invoke cond_resched() periodically. */ if (unlikely(!++count)) cmp(priv, b, b); b->prev = tail; tail = b; b = b->next; } while (b); /* And the final links to make a circular doubly-linked list */ tail->next = head; head->prev = tail; } ``` 程式碼的註釋提到若合併是極度不平衡(像是輸入已經排序),則會經過多次的迭代,儘管這些迭代不需要。因此 `cmp` 可以週期性的呼叫 `cond_resched()`。 <s> `cond_resched()` 的介紹參考 [cond_resched的使用](https://www.twblogs.net/a/5e533156bd9eee2117c39e55) > 在可搶佔內核中,在內核態有很多搶佔點,在有高優先級的進程需要運行時,就會在搶佔點到來時執行搶佔;而在內核不可搶佔系統中(如centos系統),在內核態運行的程序可調用cond_resched主動讓出cpu,防止其在內核態執行時間過長導致可能發生的soft lockup或者造成較大的調度延遲。 </s> :::danger 查閱 Linux 核心原始程式碼裡頭的文件和程式碼註解,上述描述不精確。 ::: 接著討論 `likely` 與 `unlikely` ,在 [linux/include/linux/compiler.h](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h) 可以找到 ```c #define likely_notrace(x) __builtin_expect(!!(x), 1) #define unlikely_notrace(x) __builtin_expect(!!(x), 0) ``` > `__built_expect()` 是 gcc 的內建 function ,用來將 branch 的相關資訊提供給 compiler ,告訴 compiler 設計者期望的比較結果,以便對程式碼改變分支順序來進行優化。 - `!!(x)` 用來確保值一定是 0 或 1 - 因為 if 內邏輯敘述的值可以是 0 或是非 0 的整數的,所以如果不做 `!!(x)` 就無法確保值一定是0或1 ```c if (likely(x)) { /* execute when x is true */ } else { /* execute when x is false */ } ``` - 使用 likely macro 表示這段敘述(x)為 true 的機率比較大(比較常發生),告訴 compiler 將 `x==ture` 的對應執行程式碼接在判斷後面 - 使用 unlikely macro 表示這段敘述(x)為 true 的機率比較小(不常發生),告訴 compiler 將 `x==false` 的對應執行程式碼接在判斷後面 參考自 [chiangkd](https://hackmd.io/@chiangkd/2023spring-lab0-c#%E7%A0%94%E8%AE%80-list_sortc-%E4%B8%A6%E5%BC%95%E5%85%A5%E5%B0%88%E6%A1%88) 提供的參考資料 [likely / unlikely macro introduction](https://meetonfriday.com/posts/cecba4ef/) #### list_sort 在看程式碼之前先來看註解的說明 : ``` * The comparison function @cmp must return > 0 if @a should sort after * @b ("@a > @b" if you want an ascending sort), and <= 0 if @a should * sort before @b *or* their original order should be preserved. It is * always called with the element that came first in the input in @a, * and list_sort is a stable sort, so it is not necessary to distinguish * the @a < @b and @a == @b cases. * * This is compatible with two styles of @cmp function: * - The traditional style which returns <0 / =0 / >0, or * - Returning a boolean 0/1. * The latter offers a chance to save a few cycles in the comparison * (which is used by e.g. plug_ctx_cmp() in block/blk-mq.c). * * A good way to write a multi-word comparison is:: * * if (a->high != b->high) * return a->high > b->high; * if (a->middle != b->middle) * return a->middle > b->middle; * return a->low > b->low; ``` 若 `a` 需被排序在 `b` 之後,則 `@cmp > 0` (`@a > @b`),反之若 `a` 需被排序在 `b` 之前,則 `@cmp <= 0` 。list_sort 是 stable sort 因此在這裡我們不需特別探討小於和等於 0 的區別。 ``` * This mergesort is as eager as possible while always performing at least * 2:1 balanced merges. Given two pending sublists of size 2^k, they are * merged to a size-2^(k+1) list as soon as we have 2^k following elements. ``` 從這裡開始說明 list_sort 的實作方法,兩個要被 merge 的 list 始終保持 2 : 1 的大小,這可以降低比較次數。 相較 fully-eager bottom-up mergesort 只要出現兩個 $2^k$ 大小的 list 就會立刻被合併。而 list_sort 是在出現兩個 $2^k$ 大小的 list 的時候不立即合併,而是等到下一個 $2^k$ 的 list 被蒐集起來時,前者的兩個 linked list 才會被合併。 :::danger 避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景。 ::: ``` * Thus, it will avoid cache thrashing as long as 3*2^k elements can * fit into the cache. Not quite as good as a fully-eager bottom-up * mergesort, but it does use 0.2*n fewer comparisons, so is faster in * the common case that everything fits into L1. ``` 只要這 2 : 1 的 list (即 $3*2^k$ 的 list )可以完全被放到 cache 裡,就可以避免 cache thrashing。 ``` * The merging is controlled by "count", the number of elements in the * pending lists. This is beautifully simple code, but rather subtle. ``` - `count` 用來計算在 pending lists 的 `element` 數量 ``` * Each time we increment "count", we set one bit (bit k) and clear * bits k-1 .. 0. Each time this happens (except the very first time * for each bit, when count increments to 2^k), we merge two lists of * size 2^k into one list of size 2^(k+1). ``` - 每當 `count` 增加時,第 k 位的 bit 設為 1 ,並清除 k-1 位的 bit 為 0 :::danger 不懂就說不懂,沒有「不太明白」這回事。 ::: 後續的註解說明看不懂,因此直接參考 [kdnvt](https://hackmd.io/@kdnvt/linux2022_lab0#%E7%A0%94%E8%AE%80-liblist_sortc) 的**確保 2 : 1 的實作方法**中有非常清楚的說明 引述其的表格 : |count decimal|count binary|merge|before merge|after merge | -------- | -------- | -------- |---|---| 0|0000 |No| NULL| X 1|0001 |No| 1 |X 2|0010 |Yes| 1-1 |2 3|0011 |No| 1-2 |X 4|0100 |Yes| 1-1-2 |2-2 5|0101 |Yes| 1-2-2 |1-4 6|0110 |Yes| 1-1-4 |2-4 7|0111 |No| 1-2-4 |X 8|1000 |Yes| 1-1-2-4 |2-2-4 9|1001 |Yes| 1-2-2-4 |1-4-4 10|1010 |Yes | 1-1-4-4| 2-4-4 11|1011 |Yes | 1-2-4-4| 1-2-8 12|1100 |Yes| 1-1-2-8| 2-2-8 13|1101 |Yes | 1-2-2-8 |1-4-8 14|1110|Yes | 1-1-4-8 |2-4-8 15|1111 |No | 1-2-4-8 |X 16|10000 |Yes| 1-1-2-4-8| 2-2-4-8 list_sort 實作 : ```c do { size_t bits; struct list_head **tail = &pending; /* Find the least-significant clear bit in count */ for (bits = count; bits & 1; bits >>= 1) tail = &(*tail)->prev; /* Do the indicated merge */ if (likely(bits)) { struct list_head *a = *tail, *b = a->prev; a = merge(priv, cmp, b, a); /* Install the merged result in place of the inputs */ a->prev = b->prev; *tail = a; } /* Move one element from input list to pending */ list->prev = pending; pending = list; list = list->next; pending->next = NULL; count++; } while (list); ``` **`count = 0`** 初始狀態如下圖所示,這邊假設一共有 5 個節點,其中 `node1` 為 `head` 此時 `bit` 為 0 因此不進入 `for()` 迴圈也不進行 `merge ` ```graphviz digraph list { rankdir = LR; node[shape = "record"] node1[label = "<m>node1|{<p>prev|<n>next}"] node2[label = "<m>node2|{<p>prev|<n>next}"] node3[label = "<m>node3|{<p>prev|<n>next}"] node4[label = "<m>node4|{<p>prev|<n>next}"] node5[label = "<m>node5|{<p>prev|<n>next}"] head[shape = plaintext, label = "head"] pending[shape = plaintext, label = "pending"] tail[shape = plaintext, label = "tail"] list[shape = plaintext, label = "list"] NULL[shape = plaintext, label = "NULL"] node1:n -> node2:m node2:n -> node3:m node3:n -> node4:m node4:n -> node5:m node5:n -> NULL head -> node1:m tail -> pending -> NULL list -> node2:m } ``` ```graphviz digraph list { rankdir = LR; node[shape = "record"] node1[label = "<m>node1|{<p>prev|<n>next}"] node2[label = "<m>node2|{<p>prev|<n>next}"] node3[label = "<m>node3|{<p>prev|<n>next}"] node4[label = "<m>node4|{<p>prev|<n>next}"] node5[label = "<m>node5|{<p>prev|<n>next}"] head[shape = plaintext, label = "head"] pending[shape = plaintext, label = "pending"] tail[shape = plaintext, label = "tail"] list[shape = plaintext, label = "list"] NULL[shape = plaintext, label = "NULL"] node1:n -> node2:m node2:n -> NULL node3:n -> node4:m node4:n -> node5:m node5:n -> NULL head -> node1:m tail -> pending list -> node3:m node2:p -> NULL pending -> node2:m node3:p -> node2:m } ``` **`count = 1`** 此時 `bits` 為 1 經過一次 `for` 迴圈 `tail` 指向 `node2` 的 `prev` ,不進行 `merge` 。 ```graphviz digraph list { rankdir = LR; node[shape = "record"] node1[label = "<m>node1|{<p>prev|<n>next}"] node2[label = "<m>node2|{<p>prev|<n>next}"] node3[label = "<m>node3|{<p>prev|<n>next}"] node4[label = "<m>node4|{<p>prev|<n>next}"] node5[label = "<m>node5|{<p>prev|<n>next}"] head[shape = plaintext, label = "head"] pending[shape = plaintext, label = "pending"] tail[shape = plaintext, label = "tail"] list[shape = plaintext, label = "list"] NULL[shape = plaintext, label = "NULL"] node1:n -> node2:m node2:n -> NULL node3:n -> node4:m node4:n -> node5:m node5:n -> NULL head -> node1:m tail -> node2:p list -> node3:m node2:p -> NULL pending -> node2:m node3:p -> node2:m } ``` ```graphviz digraph list { rankdir = LR; node[shape = "record"] node1[label = "<m>node1|{<p>prev|<n>next}"] node2[label = "<m>node2|{<p>prev|<n>next}"] node3[label = "<m>node3|{<p>prev|<n>next}"] node4[label = "<m>node4|{<p>prev|<n>next}"] node5[label = "<m>node5|{<p>prev|<n>next}"] head[shape = plaintext, label = "head"] pending[shape = plaintext, label = "pending"] tail[shape = plaintext, label = "tail"] list[shape = plaintext, label = "list"] NULL[shape = plaintext, label = "NULL"] node1:n -> node2:m node2:n -> NULL node3:n -> NULL node4:n -> node5:m node5:n -> NULL head -> node1:m tail -> node2:p list -> node4:m node2:p -> NULL pending -> node3:m node3:p -> node2:m node4:p -> node3:m } ``` **`count = 2`** 此時 `bits` 為 $10_2$ ,不進入 `for` 迴圈,但進行 `merge ` ```graphviz digraph list { rankdir = LR; node[shape = "record"] node1[label = "<m>node1|{<p>prev|<n>next}"] node2[label = "<m>node2|{<p>prev|<n>next}"] node3[label = "<m>node3|{<p>prev|<n>next}"] node4[label = "<m>node4|{<p>prev|<n>next}"] node5[label = "<m>node5|{<p>prev|<n>next}"] head[shape = plaintext, label = "head"] pending[shape = plaintext, label = "pending"] tail[shape = plaintext, label = "tail"] list[shape = plaintext, label = "list"] NULL[shape = plaintext, label = "NULL"] node1:n -> node2:m node2:n -> NULL node3:n -> NULL node4:n -> node5:m node5:n -> NULL head -> node1:m tail -> pending list -> node4:m node2:p -> NULL pending -> node3:m node3:p -> node2:m node4:p -> node3:m } ``` 此時 `a(node3)` 跟 `b(node2)` 已經合併完成,合併後用 `node2,3` 來表示 ```graphviz digraph list { rankdir = LR; node[shape = "record"] node1[label = "<m>node1|{<p>prev|<n>next}"] node23[label = "<m>node2,3|{<p>prev|<n>next}"] node4[label = "<m>node4|{<p>prev|<n>next}"] node5[label = "<m>node5|{<p>prev|<n>next}"] head[shape = plaintext, label = "head"] pending[shape = plaintext, label = "pending"] tail[shape = plaintext, label = "tail"] list[shape = plaintext, label = "list"] NULL[shape = plaintext, label = "NULL"] node1:n -> node23:m node23:p -> NULL node23:n -> NULL node4:n -> NULL node5:n -> NULL head -> node1:m tail -> pending list -> node5:m pending -> node4:m node4:p -> node23:m } ``` **`count = 3`** count = 3 = $11_2$ 因此會經過 2 次 `for` 迴圈,此時 `tail` 指向 `node2,3` 的 `prev` ,因 `bit` 為 0 故此階段不進行合併。 ```graphviz digraph list { rankdir = LR; node[shape = "record"] node1[label = "<m>node1|{<p>prev|<n>next}"] node23[label = "<m>node2,3|{<p>prev|<n>next}"] node4[label = "<m>node4|{<p>prev|<n>next}"] node5[label = "<m>node5|{<p>prev|<n>next}"] head[shape = plaintext, label = "head"] pending[shape = plaintext, label = "pending"] tail[shape = plaintext, label = "tail"] list[shape = plaintext, label = "list"] NULL[shape = plaintext, label = "NULL"] NULL1[shape = plaintext, label = "NULL"] node1:n -> node23:m node23:p -> NULL node23:n -> NULL node4:n -> NULL node5:n -> NULL1 head -> node1:m tail -> node23:p list -> node5:m pending -> node4:m node4:p -> node23:m node5:p -> node4:m } ``` ```graphviz digraph list { rankdir = LR; node[shape = "record"] node1[label = "<m>node1|{<p>prev|<n>next}"] node23[label = "<m>node2,3|{<p>prev|<n>next}"] node4[label = "<m>node4|{<p>prev|<n>next}"] node5[label = "<m>node5|{<p>prev|<n>next}"] head[shape = plaintext, label = "head"] pending[shape = plaintext, label = "pending"] tail[shape = plaintext, label = "tail"] list[shape = plaintext, label = "list"] NULL[shape = plaintext, label = "NULL"] node1:n -> node23:m node23:p -> NULL node23:n -> NULL node4:n -> NULL node5:n -> NULL head -> node1:m tail -> node23:p list -> NULL node5:p -> node4:m pending -> node5:m node4:p -> node23:m } ``` **`count = 4`** `count` 為 $100_2$ `bits` 不為 0 進行合併 ```graphviz digraph list { rankdir = LR; node[shape = "record"] node1[label = "<m>node1|{<p>prev|<n>next}"] node23[label = "<m>node2,3|{<p>prev|<n>next}"] node4[label = "<m>node4|{<p>prev|<n>next}"] node5[label = "<m>node5|{<p>prev|<n>next}"] head[shape = plaintext, label = "head"] pending[shape = plaintext, label = "pending"] tail[shape = plaintext, label = "tail"] list[shape = plaintext, label = "list"] NULL[shape = plaintext, label = "NULL"] node1:n -> node23:m node23:p -> NULL node23:n -> NULL node4:n -> NULL node5:n -> NULL head -> node1:m tail -> pending list -> NULL node5:p -> node4:m pending -> node5:m node4:p -> node23:m } ``` 此時 `a(node5)` 跟 `b(node4)` 已經合併完成,合併後用 node4,5 來表示 ```graphviz digraph list { rankdir = LR; node[shape = "record"] node1[label = "<m>node1|{<p>prev|<n>next}"] node23[label = "<m>node2,3|{<p>prev|<n>next}"] node45[label = "<m>node4,5|{<p>prev|<n>next}"] head[shape = plaintext, label = "head"] pending[shape = plaintext, label = "pending"] tail[shape = plaintext, label = "tail"] list[shape = plaintext, label = "list"] NULL[shape = plaintext, label = "NULL"] node1:n -> node23:m node23:p -> NULL node23:n -> NULL node45:n -> NULL head -> node1:m list -> NULL tail -> pending -> node45:m node45:p -> node23:m } ``` 上述節點都已被加到 pending list ,接著將所有的 pending list 合併在一起 ```c /* End of input; merge together all the pending lists. */ list = pending; pending = pending->prev; for (;;) { struct list_head *next = pending->prev; if (!next) break; list = merge(priv, cmp, pending, list); pending = next; } ``` 最後將 prev 重新接回變成雙向鏈結串列 ```c /* The final merge, rebuilding prev links */ merge_final(priv, cmp, head, pending, list); ``` ### 將 `list_sort` 加入到專案中 1. 將 [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) 加入到 `LAB0_C` 專案中 2. 將 `list_sort.c` 中用到的巨集加入到 `list_sort.h` 中 - `likely` 與 `unlikely` ,在 `list_sort.h` 中加入以下程式碼,可以在[linux/include/linux/compiler.h](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h)中找到 ```c #define likely_notrace(x) __builtin_expect(!!(x), 1) #define unlikely_notrace(x) __builtin_expect(!!(x), 0) ``` - 定義 `list_sort.c` 中的 `u8` 型別 ```c #include <stdint.h> typedef uint8_t u8; ``` 3. 在 `Makefile` 中的 OBJS 中新增 `list_sort.o` ```diff OBJS := qtest.o report.o console.o harness.o queue.o \ random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \ shannon_entropy.o \ linenoise.o web.o \ + list_sort.o ``` 4. 修改 `qtest.c` - 加入 `include "list_sort.h"` - 設定參數來決定是否使用 `list_sort` ```c static int use_list_sort = 0; ``` - 加入比較函式 `cmp` ```c static int cmp(void *priv, const struct list_head *a, const struct list_head *b) { return strcmp(list_entry(a, element_t, list)->value, list_entry(b, element_t, list)->value); } ``` 此時參考到 Linux 核心的比較函式撰寫 ```c /* `priv` is the test pointer so check() can fail the test if the list is invalid. */ static int cmp(void *priv, const struct list_head *a, const struct list_head *b) { struct debug_el *ela, *elb; ela = container_of(a, struct debug_el, list); elb = container_of(b, struct debug_el, list); check(priv, ela, elb); return ela->value - elb->value; } ``` 得知 `priv` 是個測試用的指標 - 將 `list_sort` 加入到 help 中,可以透過 `option list_sort [true/false]` 來啟用 `sort` 或 `list_sort` ```c add_param("list_sort", &use_list_sort, "The list_sort function used within the Linux kernel.", NULL); ``` ``` Options: descend 0 | Sort and merge queue in ascending/descending order echo 1 | Do/don't echo commands entropy 0 | Show/Hide Shannon entropy error 5 | Number of errors until exit fail 30 | Number of times allow queue operations to return false length 1024 | Maximum length of displayed string list_sort 0 | The list_sort function used within the Linux kernel. malloc 0 | Malloc failure probability percent simulation 0 | Start/Stop simulation mode verbose 4 | Verbosity level cmd> ``` 5. 編寫一個測試內容 `trace-sort.cmd` 將其放入 trace 目錄中 ``` # Testing the efficiency of list_sort and sort. # You can modify the option listsort and the RAND to meet your need option list_sort 1 new it RAND 1000000 time sort time ``` 輸入 ``` $ ./qtest -f traces/trace-sort.cmd ``` 即可得到 `list_sort` 或 `sort` 的處理時間 ### 使用 perf 分析工具 參考 [perf 安裝](https://hackmd.io/@sysprog/gnu-linux-dev/https%3A%2F%2Fhackmd.io%2Fs%2FB11109rdg#%E5%AE%89%E8%A3%9D) 輸入: ``` $ cat /proc/sys/kernel/perf_event_paranoid ``` kernel.perf_event_paranoid 的會是 4 輸入: ``` $ sudo sh -c "echo -1 > /proc/sys/kernel/perf_event_paranoid" ``` `-1` 可以將權限全開 ### `list_sort` 與 `q_sort` 效率分析 :::danger 使用 [Metric prefix](https://en.wikipedia.org/wiki/Metric_prefix),而非「萬」 ::: | 資料數 | q_sort | list_sort | | -------- | -------- | -------- | | 100k | 0.066 | 0.039 | | 200k | 0.167 | 0.128 | | 300k | 0.290 | 0.202 | | 400k | 0.414 | 0.290 | | 500k | 0.547 | 0.387 | | 600k | 0.687 | 0.479 | | 700k | 0.816 | 0.579 | | 800k | 0.951 | 0.682 | | 900k | 1.102 | 0.753 | | 1000k | 1.255 | 0.856 | 使用 [gnuplot](https://hackmd.io/@sysprog/gnu-linux-dev/https%3A%2F%2Fhackmd.io%2Fs%2FSkwp-alOg) 製圖工具畫成折線圖方便觀察 ![image](https://hackmd.io/_uploads/r1o54eXAa.png) 從圖看出當資料量越大 `list_sort` 的處理效率明顯更好 接著使用 `perf` 工具分析更詳細的內容 輸入 ``` $ perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles ./qtest -f traces/trace-sort.cmd ``` :::danger 不要急著比較程式效能,你該思考: 1. 目前的測試方法是否涵蓋排序演算法的最差狀況? 2. 是否考慮到 stable sorting 的判定 3. 資料分布要從隨機字串改為足以反映真實情況,要做哪些修改? 4. 是否引入統計模型來處理效能比較? 5. 你的數學分析呢? > 謝謝老師的指教,我會在更進一步的分析 > [name=jeremy] ::: <s>重複 5 次該程序</s> ,並顯示每個 event 的變化區間,接著分析對一百萬的資料做 sort ,並分析 `cache-misses` 、 `cache-references` 、`instructions` 、 `cycles` 。 得到以下結果 - `q_sort` ``` Performance counter stats for './qtest -f traces/trace-sort.cmd' (5 runs): 60,203,429 cache-misses # 65.85% of all cache refs ( +- 1.23% ) 91,423,384 cache-references ( +- 0.20% ) 4,732,222,486 instructions # 0.64 insn per cycle ( +- 0.01% ) 7,353,673,626 cycles ( +- 0.71% ) 1.7575 +- 0.0174 seconds time elapsed ( +- 0.99% ) ``` - `list_sort` ``` Performance counter stats for './qtest -f traces/trace-sort.cmd' (5 runs): 47,561,816 cache-misses # 66.22% of all cache refs ( +- 0.17% ) 71,827,278 cache-references ( +- 0.16% ) 4,693,241,118 instructions # 0.84 insn per cycle ( +- 0.03% ) 5,615,783,740 cycles ( +- 0.42% ) 1.32896 +- 0.00571 seconds time elapsed ( +- 0.43% ) ``` | 測試內容 | q_sort | list_sort | | -------- | -------- | -------- | | cache-misses | 60,203,429 | 47,561,816 | | cache-references | 91,423,384 | 71,827,278 | | instructions | 4,732,222,486 | 4,693,241,118 | | cycles | 7,353,673,626 | 5,615,783,740 | 由以上資料分析得知: - `list_sort` 相比 `q_sort` 少了 21% 的 `cache-misses` - 由 IPC(insns per cycle) 得知,執行速度也比 `q_sort` 快了約 31% :::danger 中文和英文字元之間要有空白字元 (對排版和文字搜尋有利) ::: :::danger 留意各式細節,唯有重視小處並步步為營,方可挑戰原始程式碼規模超越三千七百萬行的 Linux 核心 ::: > 收到 > [name=jeremy] --- ## Tic Tac Toe [M03: ttt](https://hackmd.io/@sysprog/linux2024-ttt#M03-ttt) ### Add the option of 'ttt' in the terminal 目前只是原封不動的加入到 `lab0` 後續再將其修改與解讀 在 `console.c` 中加入 ```c ADD_COMMAND(ttt, "Play Tic Tac Toe", ""); ``` 和 `do_ttt` 函式 ```c static bool do_ttt(int argc, char *argv[]) { /* .... */ } ``` 更多合併過程可以參考我的 > [commit 538e145](https://github.com/jeremy90307/lab0-c/commit/538e1453df11d2374b6adb0aeef7cbbdf2b21aae#diff-2bb15801e367b215fa5f060dc9d93329346560591dcdaaf21ac8f38fd8488a71) **在合併的過程中遇到的問題:Cppcheck: Null pointer dereference** ``` zobrist.c:63:21: error: Null pointer dereference: (struct zobrist_entry_t*)0 [nullPointer] entry = hlist_entry(hash_table[i].first, zobrist_entry_t, ht_list); ^ Fail to pass static analysis. ``` 探討: 1. 呼叫 `hlist_entry` 2. `hlist_entry` 為 `container_of` 的包裝 3. 在 `container_of` 中有這段 ```c __typeof__(((type *) 0)->member) ``` `(type *)0` 將 0 轉型成` null pointer` ,是為了取得 member 的 type 。 因此修改 `pre-commit.hook` ,在 `CPPCHECK_suppresses` 中加入這段 ```diff + --suppress=nullPointer:zobrist.c \ ``` > 在 [jasperlin1996](https://hackmd.io/@jasperlin1996/linux2022-lab0#q_remove_head) 的筆記中也有遇到相同問題,且探討的更深入。 ### ai vs ai 新增電腦對決電腦的功能至 qtest 中 > [commit:d7cd873](https://github.com/jeremy90307/lab0-c/commit/d7cd87353e8f660c188a2691c8a94f1450799f12) ``` ./qtest cmd> option ai_vs_ai 1 cmd> ttt 1 | × × 2 | × ○ 3 | ○ × ○ 4 | ○ ---+------------- A B C D O won! Moves: B2 -> C2 -> C3 -> D3 -> B1 -> B3 -> D1 -> A4 ``` 問題: 每次執行的結果都相同,應增加更多隨機性使其結果有更多變化 TODO: 在對弈的過程中,要在螢幕顯示當下的時間 (含秒數),並持續更新。 ### Monte Carlo Tree Search > 參考:[Monte Carlo Tree Search Wiki](https://en.wikipedia.org/wiki/Monte_Carlo_tree_search) / [Monte Carlo Tree Search 解說](https://youtu.be/J3I3WaJei_E?si=S5mu2CdVJ1Is5fFa) 四步驟: 1. Selection : Start from root R and select successive child nodes until a leaf node L is reached. The root is the current game state and a leaf is any node that has a potential child from which no simulation (playout) has yet been initiated. > 從根節點開始,由 UCB 判斷該往那的子節點進行移動,其中 UCB 將在最佳策略與探索新路徑做出平衡。 > $UCB=\dfrac{w_i}{n_i}+c\sqrt[2]{\dfrac{ln(N_i)}{n_i}}$ > $w_i$ : 該 node 贏得的次數 > $n_i$ : 該 node 總共進行次數 > $N_i$ : Parent 的模擬次數 > $c$ : 權重 (可以自行調整 c:$\sqrt{2}$ ) 2. Expansion : Unless L ends the game decisively (e.g. win/loss/draw) for either player, create one (or more) child nodes and choose node C from one of them. Child nodes are any valid moves from the game position defined by L. 3. Simulation : Complete one random playout from node C. This step is sometimes also called playout or rollout. A playout may be as simple as choosing uniform random moves until the game is decided (for example in chess, the game is won, lost, or drawn). 4. Backpropagation : Use the result of the playout to update information in the nodes on the path from C to R. ![image](https://hackmd.io/_uploads/r1Xv0Z3C6.png) 總結: $\dfrac{w_i}{n_i}$ => exploitation (選擇已知最好策略,即勝率最高) $c\sqrt[2]{\dfrac{ln(N_i)}{n_i}}$ => exploration (選擇探索已知路線) 此方程式不完全只走目前已知勝率最高的路線,而是在探索與最佳策略之間切換來達到平衡。 ### Fixed point implementation 為何不建議在 Linux 核心裡使用浮點數運算 : 拖慢執行速度,還需要手動儲存/取回相關的 FPU 暫存器。 > 其中 FPU 造成之問題:[Lazy FP state restore](https://en.wikipedia.org/wiki/Lazy_FP_state_restore) #### fixed_power_int 來自 Linux 核心原始程式碼的 Load Average 處理機制 =>[ linux/kernel/sched/loadavg.c ](https://elixir.bootlin.com/linux/v5.3/source/kernel/sched/loadavg.c#L110) 理解這定點數的乘冪花了我些時間了解,先從其註釋看起 ``` * By exploiting the relation between the definition of the natural power * function: x^n := x*x*...*x (x multiplied by itself for n times), and * the binary encoding of numbers used by computers: n := \Sum n_i * 2^i, * (where: n_i \elem {0, 1}, the binary vector representing n), * we find: x^n := x^(\Sum n_i * 2^i) := \Prod x^(n_i * 2^i), which is * of course trivially computable in O(log_2 n), the length of our binary * vector. ``` 寫成數學式:$x^n=x^{\sum\limits_{i = 0}^k{n_i*2^i}}$ 例如 ($n=6_{10}=110_2$) :$x^{6}=x^4*x^2$ 下面程式碼的目的為決定是否要將 $x^0,x^2,x^4$ 乘入,以 $n=110_2$ 為例,在第一次迴圈中 `n & 1` 為 `false` ,故 $x^0$ 不會乘入其中。 ```c if (n & 1) { result *= x; result += 1UL << (frac_bits - 1); result >>= frac_bits; } n >>= 1; ``` 到進行下次迴圈時 $x$ 變為 $x^2$ 再下次為 $x^2*x^2 = x^4$ 以此類推 ```c x *= x; x += 1UL << (frac_bits - 1); //carry x >>= frac_bits; ``` #### scaling factor 較大的縮放係數,可以使其數值範圍較大,但反之精度會較差,在這裡縮放係數表示為 $\dfrac{1}{frac\_bits}$ 。 在老師提供的 `mcts` 中 `score` 使用的是 `double` ,因此要將其改成定點數運算,使用到 stdint.h 中的 `uint64_t` 來取代 `double` 。 #### implementation > [commit:322411f](https://github.com/jeremy90307/lab0-c/commit/322411f0b04d7f6fce8b8ee9846851f1025d8591) 在 UCB 的計算中用到 log2 與 sqrt 的運算,需先將其改成使用定點數的運算。 **`log`** 參考< [jujuegg](https://hackmd.io/@jujuegg/HkYOnnBn6#Tic-Tac-Toe) >的實作使用泰勒級數展開來取得近似 $ln(x)$ 的值 : $ln(1+x)=x-\dfrac{x^2}{2!}+\dfrac{x^3}{3!}-....$ **`sqrt`** 接個引入第三週測驗中的開方根函式 ```c static uint64_t fixed_sqrt(uint64_t N) { uint64_t msb = 0; uint64_t n = N; while (n > 1) { n >>= 1; msb++; } uint64_t a = 1 << msb; uint64_t result = 0; while (a != 0) { if ((result + a) * (result + a) <= N) result += a; a >>= 1; } return result; } ``` #### 參照〈[並行程式設計:排程器原理](https://hackmd.io/@sysprog/concurrency/%2F%40sysprog%2Fconcurrency-sched)〉,引入若干 coroutine