--- tags: Linux_hw --- # 2023q1 Homework1 (lab0) contributed by < `milan0801` > ## 開發環境 ```bash $ 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): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz CPU family: 6 Model: 140 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 1 CPU max MHz: 4200.0000 CPU min MHz: 400.0000 BogoMIPS: 4838.40 ``` ## 開發過程 :::spoiler To-do List - [ ] 修改 queue.[ch] - [ ] 開啟 Address Sanitizer,修正 qtest 執行過程中的錯誤 - [ ] 運用 Valgrind 排除 qtest 實作的記憶體錯誤 - [ ] 透過 Massif 視覺化 “simulation” 過程中的記憶體使用量,需要設計對應的實驗 - [ ] 整合與比較 Linux kernl Linked List 的排序演算法 - [ ] 改善 web server (用 select 或 epoll) - [ ] 研讀論文〈Dude, is my code constant time?〉 - [ ] 解釋本程式的 “simulation” 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度 - [ ] 需要解釋 Student’s t-distribution 及程式實作的原理。 ::: ### 修改 `queue.c` :::danger 說好的進度呢? :notes: jserv ::: <s>這邊僅貼上部份 functions 在實作上須注意的地方:</s> #### q_free 這邊原先是使用 `list_for_each_entry`,但會出錯,後來改成使用 list_for_each_entry_safe 此 macro 就成功了,是因為迴圈內會用到 free 來釋放當前節點的空間,藉由新增 safe 這個變數來紀錄下一個節點,就可以成功在釋放當前節點後還能進入下個節點。 另外,我參考其他同學的 code,如 komark06、Ji Zhuang,發現他們使用 q_release_element() 來代替 free(),但我還不太清楚這個函式的使用方式,所以暫時先不用。 :::danger 倘若參考他人的成果,務必標注。將心比心,你不會希望自己被他人用一句「其他同學」帶過吧? :notes: jserv :hamster: 好的,會再補上! ::: ```c void q_free(struct list_head *l) { if (!l) return; element_t *node, *safe; list_for_each_entry_safe (node, safe, l, list) { free(node->value); free(node); } free(l); } ``` #### q_insert_head 這邊要注意的是,element_t 中的字串 `char *value` 也須使用 malloc 配置好空間,且若配置失敗,則須要釋放原先 `element_t` 已經配置好的空間,否則會產生 error message。 :writing_hand: 養成好習慣每次使用 malloc,都要去檢測是否有成功。 ```c bool q_insert_head(struct list_head *head, char *s) { element_t *node = malloc(sizeof(element_t)); if (!node) { return false; } /* Allocate space for value in element_te */ int s_len = strlen(s) + 1; node->value = malloc(s_len * sizeof(char)); if (!node->value) { free(node); return false; } memcpy(node->value, s, s_len); list_add(&node->list, head); return true; } ``` #### q_reverse 原本是從佇列的尾端針對每個節點交換他們各自的 next 與 prev,但會一直出現錯誤訊息,後來發現是因為這不適用於 prev pointer 不直接連接到前一個節點的情況,這樣會無法反轉那些被跳過得節點。 ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; element_t *item = NULL; struct list_head *curr = head->prev; struct list_head *tmp = NULL; for (; curr != head; curr = curr->next) { tmp = curr->next; curr->next = curr->prev; curr->prev = tmp; } tmp = head->prev; head->prev = head->next; head->next = tmp; } ``` 不適用的情境如下圖,head 的 prev 連接到 fish,而非 meekat,如此一來 meekat 會被跳過,無法順利反轉 prev 跟 next。 ![](https://i.imgur.com/Q97lHGM.png) 之後參考 @komark06 的做法修改成: ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *cur, *safe; list_for_each_safe (cur, safe, head) { list_move(cur, head); } } ``` 他的作法是依序走訪佇列的每個節點,移除當前節點再移至佇列的 head 之後,充分利用到 list.h 提供的 API,值得學習。 #### q_descend 參考 leetcode 的提示,先對佇列進行 reverse,之後設第一個節點為 max,如果後續節點的值大於 max 的值,則以此節點取代 max 原本指向的節點;如果後續節點的值小於 max 的值,則刪除此節點。 ```c int q_descend(struct list_head *head) { if (!head || list_empty(head)) return 0; q_reverse(head); element_t *max = list_entry(head->next, element_t, list); struct list_head *cur, *safe; list_for_each_safe (cur, safe, head) { element_t *cur_elem = list_entry(cur, element_t, list); char *cur_val = cur_elem->value; int cmp_res = strcmp(cur_val, max->value); if (cmp_res == 0) continue; else if (cmp_res > 0) max = cur_elem; else { list_del(cur); free(cur_elem->value); free(cur_elem); } } q_reverse(head); int final_cnt = q_size(head); return final_cnt; } ``` #### q_merge 寫函式前,先看 qtest.c 如何使用這個函式: ```c if (current && exception_setup(true)) len = q_merge(&chain.head); ``` > 函式回傳的資料是合併後的佇列的節點數。 ```c if (q_size(&chain.head) > 1) { chain.size = 1; current = list_entry(chain.head.next, queue_contex_t, chain); current->size = len; ``` > 這個函式的參數 head 跟其他函式不同,不是指到 element_t,而是 queue_contex_t,queue_contex_t 結構中的 q 指標才會指到佇列節點 element_t。 ```c struct list_head *cur = chain.head.next->next; while ((uintptr_t) cur != (uintptr_t) &chain.head) { queue_contex_t *ctx = list_entry(cur, queue_contex_t, chain); cur = cur->next; q_free(ctx->q); free(ctx); } ``` > 會將被合併的佇列釋放,合併時需要注意指標是否有效。 知道如何使用此函式後,可以開始撰寫了。首先,先找到第一個佇列並用一個指標 q1 指向它。之後在依序走訪後續的佇列,並用指標 q2 指向這些佇列。 走訪的過程中,會將 q2 合併到 q1 中。合併使用到的函式跟在 q_sort 使用的是同一個,參數皆為非循環的連結串列,所以這邊需要先將 q1->q->prev->next 和 q2->q->prev->next 都設為 NULL。合併完成後,因為 q2 的節點已經亂掉,可能有指向 q1 的節點,這樣釋放 q2 會導致後續存取 q1 產生 segmentation fault,所以需要將 q2 進行初始化,這邊使用 INIT_LIST_HEAD(q2->q)。 將全部 queues 都合併到 q1 後,因為 q1 的節點的 prev 指標是亂的,所以需要依序走訪設定指到前面節點。 ```c int q_merge(struct list_head *head) { // https://leetcode.com/problems/merge-k-sorted-lists/ if (!head || list_empty(head)) return 0; struct list_head *cur = head->next; queue_contex_t *q1 = list_entry(cur, queue_contex_t, chain); if (list_is_singular(head)) return q1->size; cur = cur->next; while (cur != head) { queue_contex_t *q2 = list_entry(cur, queue_contex_t, chain); cur = cur->next; if (!q1->q->next || !q2->q->next) continue; q1->q->prev->next = NULL; q2->q->prev->next = NULL; q1->q->next = mergeTwoLists(q1->q->next, q2->q->next); INIT_LIST_HEAD(q2->q); } /* Reconnect the prev pointer */ struct list_head *prev_tmp = q1->q, *cur_tmp = q1->q->next; while (cur_tmp->next != NULL) { cur_tmp->prev = prev_tmp; prev_tmp = cur_tmp; cur_tmp = cur_tmp->next; } cur_tmp->next = q1->q; q1->size = q_size(q1->q); return q1->size; } ``` #### q_sort 這邊使用 merge sort,除了教材有提供 merge sort 的示範程式碼外,也因為 quick sort、insertion sort、bubble sort 等常見排序演算法最差情況時間會到 $O(n^2)$ 等級,而 merge sort 最好/平均/最差皆為$O(nlog(n))$ 等級,故先用用看 merge sort。 使用 non-recursion 的方式,會在 nodes 數 為 2,000,000 的測試資料時失敗。 ```c static struct list_head *mergeTwoLists(struct list_head *list1, struct list_head *list2) { struct list_head *head = NULL, **ptr = &head; for (struct list_head **node = NULL; list1 && list2; *node = (*node)->next) { char *s1 = list_entry(list1, element_t, list)->value; char *s2 = list_entry(list2, element_t, list)->value; node = (strcmp(s1, s2) < 0) ? &list1 : &list2; *ptr = *node; ptr = &(*ptr)->next; } *ptr = (struct list_head *) ((uintptr_t) list1 | (uintptr_t) list2); return head; } void q_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; int top = 0; int listsSize = 0; /* The tested queue size is 2,000,000. * If lists size is changed to 2,000,000, it would be crashed. */ struct list_head *stack[1000] = {NULL}; struct list_head *lists[100000] = {NULL}; stack[top] = head->next; head->prev->next = NULL; while (top >= 0) { struct list_head *left = stack[top--]; if (left) { struct list_head *slow = left; struct list_head *fast; for (fast = left->next; fast && fast->next; fast = fast->next->next) { slow = slow->next; } struct list_head *right = slow->next; slow->next = NULL; stack[++top] = left; stack[++top] = right; } else { lists[listsSize++] = stack[top--]; } } while (listsSize > 1) { for (int i = 0, j = listsSize - 1; i < j; i++, j--) lists[i] = mergeTwoLists(lists[i], lists[j]); listsSize = (listsSize + 1) / 2; } head->next = lists[0]; struct list_head *ptr = head; for (; ptr->next; ptr = ptr->next) { ptr->next->prev = ptr; } ptr->next = head; head->prev = ptr; } ``` 故參考 @komark06,改成遞迴的方式: ```c struct list_head *merge_sort(struct list_head *head) { if (!head || !head->next) return head; struct list_head *slow = head; struct list_head *fast; for (fast = head->next; fast && fast->next; fast = fast->next->next) { slow = slow->next; } fast = slow->next; slow->next = NULL; return mergeTwoLists(merge_sort(head), merge_sort(fast)); } /* Recursive method */ void q_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; head->prev->next = NULL; head->next = merge_sort(head->next); struct list_head *cur = head; for (; cur->next; cur = cur->next) { cur->next->prev = cur; } cur->next = head; head->prev = cur; } ``` 目前這樣在 local 端可以通過測試,但 github action 在 trace-14-perf 與 trace-17-complexity 卻失敗(89/100),詳細原因還需要再研究。