# 2023q1 Homework1 (lab0) contributed by < `Korin777` > ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 20 On-line CPU(s) list: 0-19 Vendor ID: GenuineIntel Model name: 12th Gen Intel(R) Core(TM) i7-12700H CPU family: 6 Model: 154 Thread(s) per core: 2 Core(s) per socket: 14 Socket(s): 1 Stepping: 3 CPU max MHz: 4700.0000 CPU min MHz: 400.0000 BogoMIPS: 5376.00 Caches (sum of all): L1d: 544 KiB (14 instances) L1i: 704 KiB (14 instances) L2: 11.5 MiB (8 instances) L3: 24 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-19 ``` ## 開發過程 ### `q_new` ```c struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (!head) return NULL; INIT_LIST_HEAD(head); return head; } ``` 建立一個新的佇列, 透過 `INIT_LIST_HEAD` 巨集來初始化佇列的 `head` , 這個 `head` 不儲存值且在新增及移除時不會改變 , 它用來對應到 `queue_contex_t` 的 `q` ### q_free ```c void q_free(struct list_head *l) { if (!l) return; element_t *itr, *safe; list_for_each_entry_safe (itr, safe, l, list) { q_release_element(itr); } free(l); } ``` * 透過 `list_for_each_entry_safe` 來走訪佇列並釋放 memory , 最後在釋放 `head` * `list_for_each_entry_safe` 透過 `save` pointer 來事先存好要被釋放的 element 的下一個 element , 讓我們可以釋放記憶體 ### q_insert_head ```c bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *ele = malloc(sizeof(element_t)); if (!ele) return false; ele->value = strdup(s); if (!ele->value) { free(ele); return false; } list_add(&ele->list, head); return true; } ``` * 配置 `element_t` 及 `value` 的記憶體,透過 `list_add` 將它插入佇列的開頭(head->next) ### q_insert_tail ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; element_t *ele = malloc(sizeof(element_t)); if (!ele) return false; ele->value = strdup(s); if (!ele->value) { free(ele); return false; } list_add_tail(&ele->list, head); return true; } ``` * 配置 `element_t` 及 `value` 的記憶體,透過 `list_add_tail` 將它插入佇列的尾端(head->prev) ### q_remove_head ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *ele = list_first_entry(head, element_t, list); list_del(&ele->list); if (sp) { size_t n = strlen(ele->value) + 1; n = bufsize < n ? bufsize : n; strncpy(sp, ele->value, n); sp[n - 1] = '\0'; } return ele; } ``` * 透過 `list_first_entry` 取出開頭的值並複製到 `sp` ,在透過 `list_del` 移除開頭 * `n` 用來決定要複製多少字元,並確保包含結束字元 ### q_remove_tail ```c element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *ele = list_last_entry(head, element_t, list); list_del(&ele->list); if (sp) { size_t n = strlen(ele->value) + 1; n = bufsize < n ? bufsize : n; strncpy(sp, ele->value, n); sp[n - 1] = '\0'; } return ele; } ``` * 與 `q_remove_head` 類似 , 這裡用 `list_last_entry` 取出尾端 ### q_size ```c int q_size(struct list_head *head) { if (!head || list_empty(head)) return 0; size_t n = 0; struct list_head *itr; list_for_each (itr, head) ++n; return n; } ``` * 走訪整個佇列來記錄經過的節點數 ### q_descend ```c int q_descend(struct list_head *head) { // https://leetcode.com/problems/remove-nodes-from-linked-list/ if (!head || list_empty(head)) return 0; int count = 1; struct list_head *itr = head->prev, *safe; char *cur_max = list_entry(itr, element_t, list)->value; for (itr = itr->prev, safe = itr->prev; itr != head; itr = safe, safe = itr->prev) { char *tmp = list_entry(itr, element_t, list)->value; if (strcmp(tmp, cur_max) >= 0) { cur_max = tmp; ++count; } else { list_del(itr); q_release_element(list_entry(itr, element_t, list)); } } return count; } ``` * 透過 prev 從佇列尾端開始走訪,記下當前的最大值並把後續遇到比它小的節點刪除,確保佇列是遞減的 ### q_delete_mid ```c bool q_delete_mid(struct list_head *head) { // https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/ if (!head || list_empty(head)) return false; struct list_head *ahead = head->next, *back = head->prev; while (ahead != back && ahead != back->prev) { ahead = ahead->next; back = back->prev; } list_del(ahead); element_t *ele = list_entry(ahead, element_t, list); q_release_element(ele); return true; } ``` * 透過一個從開頭一直往後走的 pointer 及從尾端一直往前走的 pointer 找出 middle node *節點數為奇數時 , middle節點就是相遇的 node *節點數為偶數時 , middle節點是在兩 pointer 相鄰時往前的 pointer ### q_merge :::warning 避免張貼完整程式碼,你只要列出值得討論以及後續可改進的部分即可。 :notes: jserv ::: ```c struct list_head *itr, *safe, *q_finalhead = list_entry(head->next,queue_contex_t,chain)->q; struct list_head *tmp = q_finalhead->next; while(head-> next != head->prev) { if(tmp != q_finalhead) { char *cur_min = list_entry(tmp,element_t,list)->value; struct list_head *cur_q = q_finalhead, *cur_chain = NULL; for (itr = head->next->next, safe = itr->next; itr != head; itr = safe, safe = itr->next) { struct list_head *q_head = list_entry(itr,queue_contex_t,chain)->q; char *q_min = list_entry(q_head->next,element_t,list)->value; if(strcmp(q_min,cur_min) < 0) { cur_min = q_min; cur_q = q_head; cur_chain = itr; } } if(q_finalhead != cur_q) { list_move(cur_q->next,tmp); if(list_empty(cur_q)) list_del(cur_chain); } else { tmp = tmp->next; } } else { char *cur_min = NULL; struct list_head *cur_q= NULL, *cur_chain = NULL; for (itr = head->next->next, safe = itr->next; itr != head; itr = safe, safe = itr->next) { struct list_head *q_head = list_entry(itr,queue_contex_t,chain)->q; char *q_min = list_entry(q_head->next,element_t,list)->value; if(!cur_min) { cur_min = q_min; cur_q = q_head; cur_chain = itr; } else if(strcmp(q_min,cur_min) < 0) { cur_min = q_min; cur_q = q_head; cur_chain = itr; } } list_move_tail(cur_q->next,tmp); if(list_empty(cur_q)) { list_del(cur_chain); } } } ``` * 每次找出所有佇列中最小值 `cur_min` ,若 `cur_min` 不再第一個佇列中,就把它所在的節點移到 `tmp(第一個佇列尚未 merge 完成的 node)` 前面,否則將 `tmp` 移動到下一個節點。當 `tmp` 回到開頭表示第一個佇列的值都 merge 完成,之後 `cur_min` 所在的節點都改為移到 `tmp` 的尾端。 * 測試時發現會因為 `queue_contex_t` 的 size 沒有被改到而產生錯誤,沒被改到的原因為我將第一個以外的佇列都移除,所以 chain 的 size 剩 1,導致 `do_merge` 無法更新 merge 後的 size 也無法釋放被我移除的佇列的 head 記憶體 ```c struct list_head *q_merge = list_entry(head->next, queue_contex_t, chain)->q; for (struct list_head *c_itr = head->next->next; c_itr != head; c_itr = c_itr->next) { struct list_head *q_itr, *safe, *q_other = list_entry(c_itr, queue_contex_t, chain)->q, *node_merge = q_merge->next; list_for_each_safe (q_itr, safe, q_other) { while (node_merge != q_merge) { if (strcmp(list_entry(q_itr, element_t, list)->value, list_entry(node_merge, element_t, list)->value) <= 0) { break; } else node_merge = node_merge->next; } list_add_tail(q_itr, node_merge); } INIT_LIST_HEAD(q_other); } ``` * 改為將其他的佇列依序跟第一個佇列merge,由於其他佇列的節點最後都會被移除,所以直接用 `INIT_LIST_HEAD` 來讓佇列不再與任何節點連接 ### q_sort ```c void q_sort(struct list_head *head) { if (!head || list_empty(head) || head->next == head->prev) return; struct list_head less, greater; INIT_LIST_HEAD(&less); INIT_LIST_HEAD(&greater); struct list_head *pivot = head->next; char *pivot_val = list_entry(pivot, element_t, list)->value; list_del(pivot); struct list_head *itr, *safe; list_for_each_safe (itr, safe, head) { if (strcmp(list_entry(itr, element_t, list)->value, pivot_val) <= 0) list_add(itr, &less); else list_add(itr, &greater); } q_sort(&less); q_sort(&greater); INIT_LIST_HEAD(head); list_add(pivot, head); list_splice(&less, head); list_splice_tail(&greater, head); } ``` * 參考[第 1 週測驗題](https://hackmd.io/@sysprog/linux2023-quiz1)實作的 quick sort , 將佇列的第一個節點當作 pivot , 其餘的節點分別放入 `greater`(value 比 pivot 大)、`less`(value 比 pivot 小) 這兩個佇列, 遞迴去繼續將這兩個佇列分割直到佇列不超過一個節點,最後將 `less` 、 `pivot` 、 `greater` 接起來的佇列就會是排序好的 * 這裡使用到 `list_for_each_safe` 是由於 `list_add` 會更換 `itr` 的下一個節點, 所以先將它儲存在 `safe` 。在測驗題中最後連接排序好的佇列之前漏掉了 `INIT_LIST_HEAD` , 由於 `head` 連接的節點已經被連接到排序好的 `less` 及 `greater` 中,所以要先斷開原本的連接。 * 這個實作在跑測試案例 `trace14` 、 `trace15` 會產生 `ERROR: Time limit exceeded` 。在 `trace14` 中,插入了許多相同的資料,因此在分割時會集中在其中一個 queue; 在 `trace15` 中,經過 sort 後資料就已經被排序完成, 因為這裡 pivot 固定選擇第一個節點, 所以資料也會集中在其中一個佇列。 當分割的資料幾乎都在其中一個佇列中,就使 quick sort 的時間複雜度從 $O(nlogn)$ 變成 $O(n^2)$ ```c void q_sort(struct list_head *head) { if (!head || list_empty(head) || head->next == head->prev) return; struct list_head less, greater, middle; INIT_LIST_HEAD(&less); INIT_LIST_HEAD(&greater); INIT_LIST_HEAD(&middle); struct list_head *pivot = head->next; char *pivot_val = list_entry(pivot, element_t, list)->value; list_del(pivot); struct list_head *itr, *safe; list_for_each_safe (itr, safe, head) { int result = strcmp(list_entry(itr, element_t, list)->value, pivot_val); if (result < 0) list_add(itr, &less); else if(result > 0) list_add(itr, &greater); else { list_add(itr,&middle); } } q_sort(&less); q_sort(&greater); INIT_LIST_HEAD(head); list_add(pivot, head); list_splice(&middle,head); list_splice(&less, head); list_splice_tail(&greater, head); } ``` * 為了解決大量相同資料的問題,新增一個佇列 `middle` 用來儲存與 pivot 相同的節點, middle 後續不在進行 quick sort , 測試後可以通過 `trace14` ```c void q_sort(struct list_head *head) { if (!head || list_empty(head) || head->next == head->prev) return; struct list_head less, greater, middle; INIT_LIST_HEAD(&less); INIT_LIST_HEAD(&greater); INIT_LIST_HEAD(&middle); struct list_head *pivot = head->next; char *pivot_val = list_entry(pivot, element_t, list)->value; list_del(pivot); struct list_head *itr, *safe; bool less_change = true, greater_change = true; list_for_each_safe (itr, safe, head) { int result = strcmp(list_entry(itr, element_t, list)->value, pivot_val); if (result < 0) { if (less_change) list_add(itr, &less); else list_add_tail(itr, &less); less_change = !less_change; } else if (result > 0) { if (greater_change) list_add(itr, &greater); else list_add_tail(itr, &greater); greater_change = !greater_change; } else { list_add(itr, &middle); } } q_sort(&less); q_sort(&greater); INIT_LIST_HEAD(head); list_add(pivot, head); list_splice(&middle, head); list_splice(&less, head); list_splice_tail(&greater, head); } ``` * 因為選的 pivot 固定是第一個節點, 當佇列接近排序好的狀態(遞增或遞減) 就會使目前及後續的分割高度集中於其中一個 queue * 透過交互插入開頭及尾端的方式來打亂佇列中的節點, 使得下次選擇 pivot 時不會固定是佇列中最大或最小的來避來避免分割不平均的問題。測試後可以通過 `trace15` > [commit e7a6b70](https://github.com/Korin777/lab0-c/commit/e7a6b70cca766e02cf67b2d2c00dd2fba6d7bcd2) 參考 [linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list),新增 merge sort 的實作,並在 `qtest` 新增 `do_sortOption` 命令來切換排序的演算法 :::warning 若要改寫為非遞迴的排序,你打算如何進行? :notes: jserv ::: #### 非遞迴 buttom-up mergesort [commit c44b1f4](https://github.com/Korin777/lab0-c/commit/c44b1f4daf1b7f64015ea41821be3d2add3ac895) 我的第一個想法是改用 buttom-up 去實作,去年[實作](https://hackmd.io/ae7DfZuiRW6wyn83y4UjNw?view#q_sort)的結果會因為 linked list 無法像陣列那樣直接取得某個位置的值,而產生許多不必要的走訪。 現在想想其實可以先不要將 `prev` 節點連接好,因為除了最後一次 merge , 前面連接好的節點都可能會被重連 , 所以可以等到最後再去重連 `prev` 節點 , 有了 `prev` 節點我們就可以拿他來直接連到下一個要 merge 的區塊,來避免原本要經過多個節點才能到達下一個 merge 區塊 | bottom-up | time | cycles | instructions | cache-misses | cache-refrences | strcmp call | |--|--|--|--|--|--|--| | 改進前 |1.43996s|54,3498,5237|20,3342,1308|3180,7854|6983,8528|18715660| | 改進後 |1.09312s|43,7429,6136|17,7845,9395|2541,9759|4210,4296|18715660| 上面是排序一百萬資料的結果,執行時間降低了不少,但比遞迴 top-down merge sort 還慢 #### 非遞迴 top-down mergesort [commit fff99f4](https://github.com/Korin777/lab0-c/commit/fff99f4d2dab3c9b1764b7ce2722f17af7f93164) 這裡試著實現非遞迴的 top-down mergesort , 想法也是跟上面一樣透過 `prev` 節點來連接合併好的區塊,並且額外在利用合併好的區塊的第二個節點的 `prev` 來紀錄這個區塊的大小,每次合併完就去看前面有沒有大小相同的區塊,有的話就繼續合併下去,主要是想模擬 top-down merge sort 會跟前一個區塊合併的行為 | top-down | time | cycles | instructions | cache-misses | cache-refrences | strcmp call | |--|--|--|--|--|--|--| | 遞迴 |0.71662s |29,7889,3727|18,9145,7316|1373,0805|3967,0762|18673584| | 非遞迴 |0.69153s|29,2025,8297|17,8725,7508|1306,4755|3145,8093|18715660| 可以觀察到 `strcmp` 的次數不一樣,想想當節點有奇數個時,這個方法最後一個孤立的節點會跟 buttom-up 一樣固定在最後,但 top-down 的位置卻不一定,顯然我的方法只是一個會額外先合併相同大小的 buttom-up merge sort , 而跟原本遞迴的實作相比有好一些 不過若跟上面改進後的 buttom-up 實作相比,這裡的實作執行時間又降低了不少,可以觀察到 cache-misses 低了不少,我覺得應該跟先去合併相同大小的區塊這點有關 --- ### 引入 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) [commit 471ded9](https://github.com/Korin777/lab0-c/commit/471ded9a390aa75591110937848758df3d1e9754) 一開始 git commit 時無法成功,錯誤訊息如下: ``` list_sort.c:15:19: error: Null pointer dereference: (struct element_t*)0 [nullPointer] return strcmp(list_entry(a, element_t, list)->value, ^ list_sort.c:16:19: error: Null pointer dereference: (struct element_t*)0 [nullPointer] list_entry(b, element_t, list)->value); ``` 上面的 `(struct element_t*)0` 對應到 `container_of` 巨集的 `(((type *) 0))` ,而 cppcheck 所檢測出來的問題就是我們對 0 dereference `(((type *) 0)->member)`,不過之前在 queue.c 也有用到 `list_entry` 卻還是可以 commit 成功,看了 `scripts/pre-commit.hook` 可以發現裡面有一行 `--suppress=nullPointer:qtest.c` , 參閱 [Cppcheck manual](https://cppcheck.sourceforge.io/manual.pdf) 了解到他的用途是過濾掉產生的錯誤 , 而透過 `--inline-suppr` 我們也可以透過 `suppression comment` 過濾掉 cppcheck 偵測出來的錯誤,最後也透過 Inline suppressions 的方式成功將 `list_sort` 引入,另外也在 `qtest.c` 中新增 `list_sort` 命令來使用它 #### 效能比較 [commit e87aba6](https://github.com/Korin777/lab0-c/commit/e87aba629e912497cde69ee91935d02d08a70cc4) 參考 [komark06](https://hackmd.io/@komark06/SyCUIEYpj#%E6%AF%94%E8%BC%83-liblist_sortc-%E5%92%8C%E8%87%AA%E5%B7%B1%E5%AF%A6%E4%BD%9C%E7%9A%84-merge-sort) 撰寫測試程式產生排序用的資料,確保比較時排序的資料是相同的,並透過 perf 工具比較效能,排序一百萬筆隨幾產生的字串(每個字串為 5~10 個小寫字母組成)之結果如下 | 排序方式 | time | cycles | instructions | cache-misses | cache-refrences | strcmp call | |--|--|--|--|--|--|--| |my quicksort|0.73528s|30,5216,0933|21,6207,6539|3036,8419|6249,1175|24880386| | linux list_sort |0.43839s|18,5576,2153|18,6235,4913|1262,4588|3827,9864|18686563| | my_mergesort |0.71662s |29,7889,3727|18,9145,7316|1373,0805|3967,0762|18673584| | my_mergesort 更改後 |0.52061s |21,2301,7352|17,8158,6690|1591,8822|5125,5537|18673584| 可以發現雖然 instructions 差不多,但我的實作 cycles 卻高出 `list_sort` 許多,顯然我的實作未考慮現代處理器特性,而在 cache-misses 的變現在我的實作偏高,至於排序過程中比較的次數,兩者都相當接近於 $O(n \ log n)$ ```diff static struct list_head *merge(struct list_head *a, struct list_head *b) { - // ptr : Update list_head->next to link node - // node : Each comparison will advance one either a or b - struct list_head *head = NULL, **ptr = &head, **node; - for (; a && b; *node = (*node)->next) { - node = strcmp(list_entry(a, element_t, list)->value, - list_entry(b, element_t, list)->value) <= 0 - ? &a - : &b; - *ptr = *node; - ptr = &(*ptr)->next; + struct list_head head; + struct list_head *h = &head; + + while(a && b) { + if(strcmp(list_entry(a, element_t, list)->value, + list_entry(b, element_t, list)->value) <= 0) { + h->next = a; + a = a->next; + a = a->next; + } else { + h->next = b; + b = b->next; + } + h = h->next; } - *ptr = (struct list_head *) ((uintptr_t) a | (uintptr_t) b); - return head; + + h->next = (struct list_head *) ((uintptr_t)a | (uintptr_t)b); + return head.next; } ``` 觀摩 [chiangkd](https://hackmd.io/@chiangkd/2023spring-lab0-c#q_sort) 的開發紀錄,發現他 merge sort 的實作執行時間跟 cycle 與 linux `list_sort` 相當接近,而他跟我 merge sort 的不同主要在 `merge` 的實作,這裡也改進了最後 `h->next` 所造成的分支 從上面的表格可以發現,執行時間與 cycles 確實降低了,但 cache-misses 卻增加了,不知道是什麼原因造成的 我也發現如果透過 `-O3` 最佳化來編譯,運用指標得指標就不會比更改後的 merge sort 差了,同時又能精簡程式碼