--- tags: linux, queue, linked list, c_programming --- # 2020q1 Homework1 (lab0) contributed by < [chses9440611](https://github.com/chses9440611/lab0-c) > ## Implemenation ### queue.h 在 queue_t 增加 size 和 tail 欄位,用來完成在 $O(1)$ 時間內完成 q_insert_tail 和 q_size ```cpp typedef struct ELE { char *value; struct ELE *next; } list_ele_t; typedef struct { list_ele_t *head, *tail; // Linked list of elements int size; } queue_t; ``` ### q_new 當 malloc queue 失敗時要回傳 `NULL`,而一開始讓 `q->head` 和 `q->tail` 指向 `NULL` ```cpp queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (!q) return NULL; q->head = q->tail = NULL; q->size = 0; return q; } ``` ### q_free ```cpp void q_free(queue_t *q) { // when q is not NULL, we do free(q), free node from head if (q != NULL) { while (q->size > 0) { list_ele_t *tmp = q->head; q->head = q->head->next; free(tmp->value); free(tmp); q->size--; } free(q); } } ``` ### q_insert_head 這邊要注意的是當新的節點的 value 在 malloc 失敗時要記得將新節點的空間一併釋放。 第二個要注意的是當要插入第一個節點時,要讓 `q->tail` 和 `q->head` 指向同一個節點。 ```cpp bool q_insert_head(queue_t *q, char *s) { list_ele_t *newh; int length; if (!q) return false; newh = malloc(sizeof(list_ele_t)); if (!newh) { printf("Fail to malloc a node\n"); return false; } // get the string length of s, not including '\0' length = strlen(s); newh->value = malloc(length + 1); // if fail to malloc value, also free the node newh if (!newh->value) { printf("Fail to malloc value\n"); free(newh); return false; } strncpy(newh->value, s, length); newh->value[length] = '\0'; /* * if q is an empty queue, first insert * will let tail and head point to same node */ newh->next = q->head; q->head = newh; if (q->size == 0) q->tail = q->head; (q->size)++; return true; } ``` ### q_insert_tail 要注意的地方跟 q_insert_head 一樣。 ```cpp bool q_insert_tail(queue_t *q, char *s) { /* Remember: It should operate in O(1) time */ if (!q) return false; list_ele_t *newt; newt = malloc(sizeof(list_ele_t)); if (!newt) { return false; } int length = strlen(s); newt->value = malloc(length + 1); if (!newt->value) { free(newt); return false; } strncpy(newt->value, s, length); newt->value[length] = '\0'; // avoid newt->next NULL deference newt->next = NULL; if (q->size == 0) { q->head = q->tail = newt; } else { q->tail = q->tail->next = newt; } (q->size)++; return true; } ``` ### q_remove_head 當 q 為 `NULL` 或是 q->size 為0,以及 sp 指向 NULL 時回傳 `NULL` 要注意要移除的節點的字串長度要跟 bufsize 比較大小,字串長度不可以超過 `bufsize - 1` ```cpp bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (!q || q->size == 0 || !sp) return false; list_ele_t *old = q->head; int length = strlen(old->value); if (bufsize < length + 1) length = bufsize - 1; // for sp is pointed to an allocated space, no need to malloc again. // sp = malloc(length + 1); // if(!sp) { // printf("sp malloc fail\n"); // return false; // } strncpy(sp, old->value, length); sp[length] = '\0'; q->head = q->head->next; free(old->value); free(old); if (q->size == 1) q->tail = q->head; (q->size)--; return true; } ``` ### q_size 由於在 queue_t 中增加了 size 欄位,使得可以在常數時間內完成 ```cpp int q_size(queue_t *q) { if (!q) return 0; return q->size; } ``` :::warning 可改寫為以下: ```cpp int q_size(queue_t *q) { return q ? q->size : 0; } ``` 簡潔又清晰 :notes: jserv ::: :::success 經過改寫後: ```cpp int q_size(queue_t *q) { return q ? q->size : 0; } ``` ::: ### q_reverse 先讓 `q->tail = q->head` 可以避免之後再用迴圈來尋找 `q->tail` 的 overhead。 ```cpp void q_reverse(queue_t *q) { if (q && q->size > 1) { list_ele_t *left, *right; q->tail = q->head; left = q->head->next; while (left != NULL) { right = left->next; left->next = q->head; q->head = left; left = right; } q->tail->next = NULL; } } ``` ### q_sort 若用 bottom-up 的方式,會需要 $O(n)$ 的空間,在 trace-15 會檢測時會超出程式記憶體的容量。若是純用遞迴也會造成 Segmentation fault。於是最後我採取的是用 Top-down 遞迴來均分串鏈,用迴圈來合併分併兩個串鏈,最後在用一個 for 迴圈來尋找 `q->tail`。則遞迴式為 $\Big\{\begin{array}{11} T(n) = 2T(\frac{n}{2}) + O(n) & \mbox{n > 1} \\ T(1) = O(1) & \mbox{n = 1}\end{array}$ 藉由 [Mater theorem](https://en.wikipedia.org/wiki/Master_theorem_\(analysis_of_algorithms\)) 或是計算可得 $T(n) = O(n\log n)$ ```cpp void q_sort(queue_t *q) { if (!q || q->size < 2) return; q->head = merge_sort(q->head); for (q->tail = q->head; q->tail->next != NULL; q->tail = q->tail->next); } ``` ### auxiliary function of q_sort :::warning `mergesort` 和 `merge` 這兩個函式應該在宣告標註 `static`,這樣就不會有符號的 visibility 被公開的狀況,有助於編譯器施加最佳化。 :notes: jserv ::: :::info 在使用時,要注意 static function 的宣告和實作要放在同一個檔案 否則會有error: foo_function declared ‘static’ but never defined ::: #### declare in queue.c ```cpp static list_ele_t *merge_sort(list_ele_t *head); static list_ele_t *merge(list_ele_t *l1, list_ele_t *l2); ``` #### mergesort ```cpp static list_ele_t *merge_sort(list_ele_t *head) { if (!head || !head->next) return head; list_ele_t *fast = head->next; list_ele_t *slow = head; // split list while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } fast = slow->next; slow->next = NULL; // sort each List list_ele_t *l1 = merge_sort(head); list_ele_t *l2 = merge_sort(fast); return merge(l1, l2); } ``` #### merge 在合併時,先尋找第一個節點,並讓 `head` 指向他,之後再用迴圈的方式來尋找。 ```cpp static list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { int cmpResult = strcmp(l1->value, l2->value); list_ele_t *head, *tail; if (cmpResult <= 0) { tail = head = l1; l1 = l1->next; } else { tail = head = l2; l2 = l2->next; } while (l1 != NULL && l2 != NULL) { cmpResult = strcmp(l1->value, l2->value); if (cmpResult <= 0) { tail->next = l1; l1 = l1->next; } else { tail->next = l2; l2 = l2->next; } tail = tail->next; } if (l1 == NULL) { tail->next = l2; } else { tail->next = l1; } return head; } ``` ### q_sort (Bottom-up way) 但這個函式在節點數來到百萬級時造成程式的 stack 已滿而無法運作 ```cpp void q_sort(queue_t *q) { if (!q || q->size <= 1) return; int size = q->size; list_ele_t *ptr[size]; // linked list merge sort bottom-up way for (int width = 1; width < size; width *= 2) { list_ele_t* target; for(int k = 0; k < size; k++) { ptr[k] = q->head; q->head = q->head->next; } q->tail = ptr[size-1]->next = NULL; for (int i = 0; i < size; i += 2 * width) { if(size < i + width) { // the remain left < size, no need to compare left and rig q->tail->next = ptr[i]; q->tail = ptr[size-1]; continue; } // the total component to insert int leftBound = i + width, rightBound = (size < i + 2 * width)?size : i + 2 * width; for (int left = i, right = leftBound; left < leftBound || right < rightBound;) { if (left == leftBound) { // right remain q->tail->next = ptr[right]; right = rightBound; q->tail = ptr[right-1]; } else if (right == rightBound) { // left remain q->tail->next = ptr[left]; left = leftBound; q->tail = ptr[left-1]; } else { int result = strcmp(ptr[left]->value, ptr[right]->value); if (result <= 0) { // Same string or smaller left, insert left target = ptr[left++]; // ptr[j] = tmp[left], j++, left += 1 } else { // right is smaller target = ptr[right++]; } if(!q->tail) q->head = target; else { q->tail->next = target; } q->tail = target; } } } } q->tail->next = NULL; } ``` ## Make test result ```shell make test scripts/driver.py -c --- Trace Points +++ TESTING trace trace-01-ops: # Test of insert_head and remove_head --- trace-01-ops 6/6 +++ TESTING trace trace-02-ops: # Test of insert_head, insert_tail, and remove_head --- trace-02-ops 6/6 +++ TESTING trace trace-03-ops: # Test of insert_head, insert_tail, reverse, and remove_head --- trace-03-ops 6/6 +++ TESTING trace trace-04-ops: # Test of insert_head, insert_tail, size, and sort --- trace-04-ops 6/6 +++ TESTING trace trace-05-ops: # Test of insert_head, insert_tail, remove_head, reverse, size, and sort --- trace-05-ops 5/5 +++ TESTING trace trace-06-string: # Test of truncated strings --- trace-06-string 6/6 +++ TESTING trace trace-07-robust: # Test operations on NULL queue --- trace-07-robust 6/6 +++ TESTING trace trace-08-robust: # Test operations on empty queue --- trace-08-robust 6/6 +++ TESTING trace trace-09-robust: # Test remove_head with NULL argument --- trace-09-robust 6/6 +++ TESTING trace trace-10-malloc: # Test of malloc failure on new --- trace-10-malloc 6/6 +++ TESTING trace trace-11-malloc: # Test of malloc failure on insert_head Fail to malloc a node Fail to malloc value Fail to malloc a node Fail to malloc a node Fail to malloc a node Fail to malloc a node Fail to malloc a node Fail to malloc a node --- trace-11-malloc 6/6 +++ TESTING trace trace-12-malloc: # Test of malloc failure on insert_tail --- trace-12-malloc 6/6 +++ TESTING trace trace-13-perf: # Test performance of insert_tail --- trace-13-perf 6/6 +++ TESTING trace trace-14-perf: # Test performance of size --- trace-14-perf 6/6 +++ TESTING trace trace-15-perf: # Test performance of insert_tail, size, reverse, and sort --- trace-15-perf 6/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 --- trace-16-perf 6/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 100/100 ``` ## Natural sort - [ ] 什麼是 natural sort - [ ] 如何修改比較程式 - [ ] 如何在 simulation 修改來反映出 natural sort 的使用 ## Valgrind 排除記憶體錯誤, Massif 視覺化 - [ ] 什麼是 Valgrind 怎麼使用 - [ ] 什麼是 Massif,如何使用 - [ ] 要設計怎樣的實驗 - [ ] 如何實作實驗 ### Dude, is my code constant time? - [ ] ## TODO: - [ ] 修改排序所用的比較程式,變更為 natural sort,在 "simulaition" 也做對應的修改,得以反映出 natural sort 的使用。 * 紀錄如何逐步達到自動評分的要求,需要涵蓋 - [ ] 運用 Valgrind 排除 qtest 實作的記憶體錯誤,並透過 Massif視覺 "simulation" 過程中的記憶體使用量,需要設計對應的實驗。 - [ ] 研讀論文 [Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf),解釋本程式的 "simulation" 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 [Student's t-distribution](https://en.wikipedia.org/wiki/Student%27s_t-distribution) 及程式實作的原理; - [ ] 解釋 [select](http://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫在本程式的使用方式,並分析 [console.c](https://github.com/sysprog21/lab0-c/blob/master/console.c) 的實作,說明其中運用 CS:APP [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 的原理和考量點。可對照參考 [CS:APP 第 10 章重點提示](https://hackmd.io/@sysprog/H1TtmVTTz) :arrow_forward: 為避免「舉燭」,請比照 `qtest` 實作,撰寫獨立的終端機命令解譯器 (可建立新的 GitHub repository) * 挑戰題 - [ ] 參照 [antirez/linenoise](https://github.com/antirez/linenoise),強化 `qtest` 命令列功能,使其支援單行或多行編輯、歷史命令處理、命令自動補完 (例如輸入 `h` 之後按下 Tab 按鍵,自動接續補上 `elp`,成為完整的 `help` 命令)。除了整合程式碼外,應當說明 [antirez/linenoise](https://github.com/antirez/linenoise) 的運作原理,注意到 [termios](http://man7.org/linux/man-pages/man3/termios.3.html) 的運用 - [ ] 思考如何預設開啟 AddressSanitizer,卻又能滿足效能測試所需 (可能需要兩種以上的 build target) - [ ] 指出現有程式的缺陷 (提示: 和 [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 有關),嘗試強化並提交 pull request