# 2020q1 Homework1 (lab0) contributed by < `ire33164` > ###### tags: `Linux Kernel` # 作業說明 這是 [linux 核心設計課程](http://wiki.csie.ncku.edu.tw/linux/schedule) 的第一次作業,主要用來加深對 singly linked list 的概念與實做 [作業要求](https://hackmd.io/@sysprog/linux2020-lab0#-%E4%BD%9C%E6%A5%AD%E8%A6%81%E6%B1%82) # 實做內容 ## queue.h 為了符合 [C Programming Lab](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf) 中所提到`q_size` 和 `q_insert_tail` 的時間複雜度須為 ==$\mathcal{O}(1)$== > Two of the functions: `q_insert_tail` and `q_size` will require some effort on your part to meet the required performance standards. Naive implementations would require O(n) steps for a queue with n elements. We require that your implementations operate in time O(1) 因此在 **struct queue_t** 新增了兩個 field **`list_ele_t *tail`**, **`int size`** ```cpp /* Queue structure */ typedef struct { list_ele_t *head, *tail; /* Linked list of elements */ int size; } queue_t; ``` 1. `tail` : 型別為 `list_ele_t *`, 用於紀錄 `queue` 最尾端的元素 2. `size` : 型別為 `int`, 用於紀錄 `queue` 上目前串接的元素數量 ## queue.c ### q_new 創建新的 `queue` 並初始化 : 1. 確認 `malloc` 是否成功 2. 確認成功後再將 `size` 設為 0 (空佇列 3. `head`, `tail` 皆指向 `NULL` ```c= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); /* Return NULL if malloc failed */ if (!q) { return NULL; } /* Initialize queue */ q->head = NULL; q->tail = NULL; q->size = 0; return q; } ``` ### q_free 釋放 `queue` 中的 space 1. 確認 `q` 是否為 `NULL` 2. 利用迴圈逐個釋放 element space (`list_ele_t` 和 `value`) 3. 釋放 queue structure ```c= void q_free(queue_t *q) { /* No effect if q is NULL */ if (!q) { return; } /* Free all elements space */ for (list_ele_t *cur = q->head, *prev = NULL; cur; ) { prev = cur; cur = cur->next; /* Free value space */ free(prev->value); /* Free list_ele structure */ free(prev); } /* Free queue structure */ free(q); } ``` ### q_insert_head 插入 element 到 `queue` 的 `head` 1. 確認 `q` 是否為 `NULL` 2. Allocate `list_ele_t` 的空間給 new element 並確認是否成功 3. Allocate `strlen(s) + 1` 給 new element 中的 `value` 並確認是否成功 4. 將 new element 的 `next` 指向 old head 5. new element 成為 `head` * 若 `queue` 為空, 則將 `tail` 和 `head` 都指向 new element 6. 利用 `strncpy` 將 `s` 複製到 `newh->value` 中 7. 將 `size` + 1 :::info 關於 (3) 為什麼要取 strlen(s) + 1 ? 在 C 語言中,字串被以連續記憶體的方式儲存下來 並在字串的最後存下中止符號 `'\0'` 代表字串的的結尾 因此假設 string 長度為 `k` 則需要 `k + 1` 個字元空間才能儲存 再看看 `strlen` 的描述 > The C library function size_t strlen(const char *str) computes the length of the string str up to, but not including the terminating null character. > From [tutorialspoint](https://www.tutorialspoint.com/c_standard_library/c_function_strlen.htm) 因此需要 allocate `strlen(s) + 1` 的空間 ::: ```c= bool q_insert_head(queue_t *q, char *s) { list_ele_t *newh; /* Return false if q is NULL */ if (!q) { return false; } newh = malloc(sizeof(list_ele_t)); /* Return false if malloc failed */ if (!newh) { return false; } /* Return false if malloc failed */ newh->value = malloc(strlen(s) + 1); if (!newh->value) { /* Free allocated space */ free(newh); return false; } newh->next = q->head; strncpy(newh->value, s, strlen(s) + 1); if (q->size == 0) { q->head = q->tail = newh; } else { q->head = newh; } (q->size)++; return true; } ``` ### q_insert_tail 插入 element 到 `queue` 的 `tail` 1. 確認 `q` 是否為 `NULL` 2. Allocate `list_ele_t` 的空間給 new element 並確認是否成功 3. Allocate `strlen(s) + 1` 給 new element 中的 `value` 並確認是否成功 4. 將 new element 的 `next` 指向 `NULL` 5. 利用 `strncpy` 將 `s` 複製到 `newh->value` 中 6. 將 old tail 的 `next` 指向 new element 使其成為 `tail` * 若 `queue` 為空, 則將 `tail` 和 `head` 都指向 new element 7. 將 `size` + 1 ```c= bool q_insert_tail(queue_t *q, char *s) { /* Return false if q is NULL */ if (!q) { return false; } list_ele_t *newt; newt = malloc(sizeof(list_ele_t)); /* Return false if malloc failed */ if (!newt) { return false; } newt->value = malloc(strlen(s) + 1); /* Return false if malloc failed */ if (!newt->value) { /* Free allocated space */ free(newt); return false; } newt->next = NULL; strncpy(newt->value, s, strlen(s) + 1); if (q->size == 0) { q->tail = q->head = newt; } else { q->tail->next = newt; q->tail = newt; } (q->size)++; return true; } ``` ### q_remove_head 移除 queue 的 `head`, 若 `sp` 不為 `NULL`, 則複製長度最大為 `bufsize` 的字串到 `sp` 中 1. 確認 `q` 是否為 `NULL` 或 empty 2. 若 `sp` 不為 `NULL`, 則將 `q->head->value` 複製到 `sp` 中, 並將 `sp` 的最後一個 `char` 設為 `'\0'` 3. 將 `head` 指向 `head->next` 改變 `queue` 的 `head` 4. 將 `size` - 1 5. 若移除 `head` 後 `queue` 為空, 則把 `tail` 也指向 `NULL` 6. 釋放 element space (`list_ele_t` 和 `value`) :::info `strncpy` 的描述 > The C library function char *strncpy(char *dest, const char *src, size_t n) copies up to n characters from the string pointed to, by src to dest. In a case where the length of src is less than that of n, the remainder of dest will be padded with null bytes. > From [tutorialspoint](https://www.tutorialspoint.com/c_standard_library/c_function_strncpy.htm) 當中提到若 `n` 大於 `src` 的字串長度時 其在 `dest` 上的差額會自動補上 null bytes 因此使用 strncpy 複製字串時可以直接給定最大長度 又因 `strncpy` 並不像 `strcpy` 會在複製後自動補上 `'\0'` 所以就必須預留最後一個 byte 給 `'\0'` 因此複製的最大長度為 `bufsize - 1` ::: ```c= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { /* Return false if q is NULL or empty */ if (!q || q->size == 0) { return false; } /* Copy string to sp if sp is non-NULL */ if (sp) { /* Copy the string that its length is smaller than bufsize */ strncpy(sp, q->head->value, bufsize - 1); sp[bufsize - 1] = '\0'; } list_ele_t *tmp = q->head; q->head = q->head->next; (q->size)--; /* Set tail to NULL if queue is empty */ q->tail = q->size == 0 ? q->head : q->tail; /* Free value space */ free(tmp->value); /* Free list_ele structure */ free(tmp); return true; } ``` ### q_size 回傳 `queue` 的 `size` 1. 若 `q` 為 `NULL` 則回傳 `0` 2. 回傳 `size` ```c= int q_size(queue_t *q) { /* Return 0 if q is NULL */ if (!q) { return 0; } return q->size; } ``` ### q_reverse 反轉 `queue` 的排序 1. 確認 `q` 是否為 `NULL` 或 empty 2. 每個 element 的 `next` 指向前一個 element 3. swap `queue` 中的 `head` 和 `tail` 4. 完成反轉 ```c= void q_reverse(queue_t *q) { /* No effect if q is NULL or empty */ if (!q || q->size == 0) { return; } for (list_ele_t *prev = NULL, *cur = q->head; cur; ) { list_ele_t *tmp = cur; cur = cur->next; tmp->next = prev; prev = tmp; } list_ele_t *tmp; tmp = q->head; q->head = q->tail; q->tail = tmp; } ``` ### q_sort 對 `queue` 做依據 [natural sort](https://github.com/sourcefrog/natsort) 排序,參考 [測驗 1]() 使用 `divide and conquer` 的方式做排序 ```c= list_ele_t *divide_and_conquer(list_ele_t *head) { if (!head || !head->next) { return head; } list_ele_t *left = head; list_ele_t *right = head->next; left->next = NULL; left = divide_and_conquer(left); right = divide_and_conquer(right); for (list_ele_t *merge = NULL; left || right; ) { if (!right || (left && strcmp(left->value, right->value) < 0)) { if (!merge) { head = merge = left; } else { merge->next = left; merge = merge->next; } left = left->next; } else { if (!merge) { head = merge = right; } else { merge->next = right; merge = merge->next; } right = right->next; } } return head; } /* * Sort elements of queue in ascending order * No effect if q is NULL or empty. In addition, if q has only one * element, do nothing. */ void q_sort(queue_t *q) { /* No effect if q is NULL or empty */ if (!q || q->size == 0) { return; } /* Do nothing if q has only one */ if (q->size == 1) { return; } q->head = divide_and_conquer(q->head); list_ele_t *tmp = q->head; while (tmp->next) { tmp = tmp->next; } q->tail = tmp; } ``` 除了測驗一的演算法外,值得注意的是做完排序後須重新指派 `head` 和 `tail`。 執行 `$ make test` 進行測試 : ``` +++ TESTING trace trace-15-perf: # Test performance of insert_tail, size, reverse, and sort --- trace-15-perf 0/6 +++ TESTING trace trace-16-perf: # Test performance of sort with random and descending orders # 10000: all correct sorting algorithms are expected pass # 50000: sorting algorithms with O(n^2) time complexity are expected failed # 100000: sorting algorithms with O(nlogn) time complexity are expected pass ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient --- trace-16-perf 0/6 +++ TESTING trace trace-17-complexity: # Test if q_insert_tail and q_size is constant time complexity Probably constant time Probably constant time --- trace-17-complexity 5/5 --- TOTAL 88/100 ``` 可以看到剩下 `trace-15-perf` 和 `trace-16-perf` 未通過。 **Time limit exceed** 其中 `trace-16-perf` 顯示 **Time limit exceed**,經思考過後發現 [測驗一]() 的演算法相當於將一個 `left` 插入已排序好的 `right` 中,概念類似 [insertion sort](https://en.wikipedia.org/wiki/Insertion_sort),時間複雜度為 ==$\mathcal{O}(N^2)$==,若將 `queue` 從中間切一半分成 `left` 和 `right` 再進行運算,概念類似 [merge sort](https://en.wikipedia.org/wiki/Merge_sort),時間複雜度則可從原本的 ==$\mathcal{O}(N^2)$== 降為 ==$\mathcal{O}(Nlog(N))$== > 利用 `left` 走一步 `right` 走兩步的技巧,當 `right` 或 `right->next` 為 `NULL` 時,`left` 就剛好在 `queue` 的中間位置 改良後 : ```c= list_ele_t *divide_and_conquer(list_ele_t *head) { if (!head || !head->next) { return head; } list_ele_t *left = head; list_ele_t *right = head->next; /* left : 1, right : 2 */ while (right && right->next) { left = left->next; right = right->next->next; } right = left->next; left->next = NULL; left = head; left = divide_and_conquer(left); right = divide_and_conquer(right); ... ``` 執行 `$ make test` : ![](https://i.imgur.com/s4chj4g.png) 通過測驗!! :heavy_check_mark: 不過我很好奇為什麼改完 `trace-16-perf` 後 `trace-15-perf` 也跟著修好了,於是我用原本的 code 利用 `Valgrind` 再測試一次 `trace-15-perf` 來分析記憶體的使用情形。 執行 `$ scripts/driver.py -p /tmp/qtest.pSix6W --valgrind -t 15` : ``` +++ TESTING trace trace-15-perf: # Test performance of insert_tail, size, reverse, and sort ==19349== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==19349== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==19349== Can't extend stack to 0x1ffe8010a8 during signal delivery for thread 1: ==19349== no stack segment ``` **Stack overflow** 使用 `Valgrind` 測試後, 發現裡面存在著 [stack overflow](https://zh.wikipedia.org/zh-tw/%E5%A0%86%E7%96%8A%E6%BA%A2%E4%BD%8D) 的問題,所以猜測是原本的演算法遞迴呼叫太多次導致 `stack` 無法容納如此多的 return 的 address,在改良成 [merge sort](https://en.wikipedia.org/wiki/Merge_sort) 後遞迴次數變少,不只時間複雜度下降, [stack overflow](https://zh.wikipedia.org/zh-tw/%E5%A0%86%E7%96%8A%E6%BA%A2%E4%BD%8D) 的問題也跟著解決 **Natural sort** - 3/1 更新 > 撰寫的過程中忘記要使用 [natural sort](https://github.com/sourcefrog/natsort) 來代替原本的 `strcmp`,因此補上進度 由於要使用 `strnatcmp` 因此先至 [natsort](https://github.com/sourcefrog/natsort) 下載 `strnatcmp.[ch]` 加入專案中,接著將 `strnatcmp.c` 匯編至 `Makefile` 中進行編譯並且連結 : ``` ... OBJS := qtest.o report.o console.o harness.o queue.o strnatcmp.o \ random.o dudect/constant.o dudect/fixture.o dudect/ttest.o ... ``` 將 `queue.c` 中 `strcmp` 換成 `strnatcmp` 並匯入 `strnatcmp.h`,發現執行 `make test` 時 trace-perf-15 失敗,於是參考 [merge sort iteration](https://npes87184.github.io/2015-09-12-linkedListSort/?fbclid=IwAR27D72ToEEA-0KnACXMF-jl9nTxw53ZVD16dnGkWiDIRTUUYhaNLVXTdC8) 對原本的程式碼稍做修改: ```c ... while (left && right) { if (strnatcmp(left->value, right->value) < 0) { if (!merge) { merge = head = left; } else { merge->next = left; merge = merge->next; } left = left->next; } else { if (!merge) { merge = head = right; } else { merge->next = right; merge = merge->next; } right = right->next; } } merge->next = left ? left : right ? right : NULL; ... ``` 即可跑過測資 - 測試 `sort` 是否成功 ``` cmd> ih h31 q = [h31 h4 h h h1 h11 h21 k t] cmd> sort cmd> sort ERROR: Not sorted in ascending order q = [h h h1 h4 h11 h21 h31 k t] ``` 出現了 ==ERROR: Not sorted in ascending order== 於是回頭 trace `qtest` 中的 `function do_sort` 發現 ```c /* FIXME: add an option to specify sorting order */ if (strcasecmp(e->value, e->next->value) > 0) { report(1, "ERROR: Not sorted in ascending order"); ok = false; break; } ``` ### Replace merge sort with pointer to pointer > 2021/1/31 更新 因為之前寫的 code 太醜,於是把整個 do_sort 重寫: **Divid_and_conquer** ```c list_ele_t *divide_and_conquer(list_ele_t *head) { if (!head || !head->next) { return head; } list_ele_t *left = head; list_ele_t *right = head->next; split(head, &left, &right); left = divide_and_conquer(left); right = divide_and_conquer(right); return merge(left, right); } ``` - 透過 function `split` 將 queue 從中間切開,這邊採用 Fast & Slow Pointer 來找出中間的值: ```c /* split from the middle of queue */ void split(list_ele_t *head, list_ele_t **q1, list_ele_t **q2) { list_ele_t *slow = head; list_ele_t *fast = head->next; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } *q1 = head; *q2 = slow->next; slow->next = NULL; return; } ``` **merge** ```c list_ele_t *merge(list_ele_t *q1, list_ele_t *q2) { list_ele_t *head = NULL; list_ele_t **tmp = &head; while (q1 && q2) { if (strnatcmp(q1->value, q2->value) < 0) { *tmp = q1; q1 = q1->next; tmp = &((*tmp)->next); } else { *tmp = q2; q2 = q2->next; tmp = &((*tmp)->next); } } /* add the last value which is the maximum value of the queue */ *tmp = q1 ? q1 : q2; return head; } ``` - 比較前面的凌亂寫法,這邊採用 pointer-to-pointer 的技巧,使整個程式碼簡潔許多,解釋一下 pointer to pointer 的概念: ![](https://i.imgur.com/9pUqft4.png) - `tmp` 是一個指向 `head` 的 pointer - 移動 node 時僅須改變 `tmp` 指向的位置也就是 `*tmp` 的值 ![](https://i.imgur.com/Zz4yx5V.png) - 這麼一來不只 head 的位址不會一直改變,整個程式碼也精簡很多 #### test 目前跑測資依舊是 trace-15-perf fail,用 valgrind 分析記憶體使用: ``` $ scripts/driver.py -p ./qtest -t 15 --valgrind --- Trace Points +++ TESTING trace trace-15-perf: # Test performance of insert_tail, size, reverse, and sort ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient ERROR: Insertion of dolphin failed (1 failures total) ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient ERROR: Insertion of gerbil failed (2 failures total) ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient ``` 由於 Time limit exceeded 是發生在採用 natural sort 後,猜測應該是 `strnatcmp` 帶來對效能的衝擊,使用 `perf` 測試 `strnatcmp` 與 `strcmp` 的差別: ``` $ perf record scripts/driver.py -p ./qtest -t 15 $ perf report ``` ![](https://i.imgur.com/EveQQhJ.png) - `strnatcmp` 直接佔了 43.13% 的 overhead ![](https://i.imgur.com/nBDYYDB.png) - 使用原本的 `strcmp` 後因為少了 `strnatcmp` 的 overhead,trace-15 也可以輕鬆通過 :::success 除了 `strnatcmp` 外,`split` 本身也佔了整體 20% ~ 29% 的 overhead,因此可能要從優化 split 下手。 ::: 利用 `perf` 找出 `split` bottleneck: ``` $ perf annotate ``` ![](https://i.imgur.com/7DeD3wP.png) - 可以看出大部分的 overhead 都是在 slow-fast pointer 中產生 :::warning TODO: 尋找優化方法 ::: # 透過 Massif 視覺化 simulation 的記憶體使用量 ``` $ valgrind --tool=massif ./qtest ``` 得到 `unknown option: --show-leak-kinds=all` 的結果,