# 2020q1 Homework3 (quiz3) contributed by < `kylekylehaha` > > [第三週測驗](https://hackmd.io/@sysprog/linux2020-quiz3) > [作業要求](https://hackmd.io/@sysprog/rJXs6hrHU) ###### tags: `linux2020` ## 測驗 1 **XOR Linked list** 根據維基百科的描述 [XOR linked list](https://en.wikipedia.org/wiki/XOR_linked_list) > An XOR linked list is a type of data structure used in computer programming. It takes advantage of the bitwise XOR operation to decrease storage requirements for doubly linked lists. 意思是說 `XOR linked list` 可以達到 `doubly linked list` 的效果:`可以在 O(1) 的時間複雜度下,取得前一項和後一項`,然而 `XOR linked list` 的空間雜度可以比 `doubly linked list` 低,因為只需要一個指標,而 `doubly linked list` 卻需要兩個指標 `prev` & `next`。 如何使用 `XOR linked list` 呢? 公式: `address of next node = address of perv node ^ pointer in the current node.` ,其中我們將 `address of next node` 記作 `addr(next) ` ; `address of perv node` 記作 `addr(prev)` 我們套入三種 case 來證明: * Head to next node: * 因為 head 的關係,故 addr(prev) 為 0 * 在 head 中,其element 為 `0 ^ addr(next)` = `addr(next)` ``` addr(next) = addr(prev) ^ pointer in the current node = 0 ^ (0 ^ addr(next)) = addr(next) ``` * Node to next node: ``` addr(next) = addr(prev) ^ pointer in the current node = addr(prev) ^ (addr(prev) ^ addr(next)) = 0 ^ addr(next) = addr(next) ``` * Tail to next node: * 由於 tail 即為最後一項,故其 next node = `NULL` * 在 tail 中,其 element 為 `addr(prev) ^ 0` = `addr(prev)` ``` addr(next) = addr(prev) ^ pointer in the cuurent node = addr(prev) ^ (addr(prev) ^ 0) = addr(prev) ^ addr(prev) = 0 (NULL) ``` 測驗一題目: ```cpp list *sort(list *start) { if (!start || !start->addr) return start; list *left = start, *right = start->addr; left->addr = NULL; right->addr = XOR(right->addr, left); left = sort(left); right = sort(right); for (list *merge = NULL; left || right;) { if (!right || (left && left->data < right->data)) { list *next = left->addr; if (next) next->addr = XOR(left, next->addr); if (!merge) { start = merge = left; merge->addr = NULL; } else { merge->addr = LL1; left->addr = LL2; merge = left; } left = next; } else { list *next = right->addr; if (next) next->addr = XOR(right, next->addr); if (!merge) { start = merge = right; merge->addr = NULL; } else { merge->addr = RR1; right->addr = RR2; merge = right; } right = next; } } return start; } ``` 可以發現,這題和[第一週題目](https://hackmd.io/@sysprog/linux2020-quiz1)相似,都是實做 `merge sort`。 `LL1` & `LL2` * 在第一小段,由於 `left` 為 head,因此 `left->addr` 即為它的下一個 node ```cpp ... list *next = left->addr; if (next) next->addr = XOR(left, next->addr); ... ``` * 我們已知 `merge` 為 merge list 的 tail,故我們要將 `left` 的 head 插在 `merge` 後面,並更新 `merge->addr`。在更新前, `merge->addr` = `addr(prev)` * 在更新後,原本的 `merge` 所指的記憶體位置,我們用 `merge'` 代替。此時 `merge'->addr` 應為 `addr(prev) ^ addr(next)` = `addr(prev) ^ addr(left)`,在之前我們已知 `addr(prev)` = `merge->addr` ; `left` 插在 `merge'` 後面。 * 故由此可知 `LL1` = `XOR(merge->addr, left)`。 * 由於 `left` 為 tail ,故其 `left->addr` 存的是上一個 node,也就是 `merge`。故 `LL2` = `merge` ```cpp if (!merge) { start = merge = left; merge->addr = NULL; } else { merge->addr = LL1; left->addr = LL2; merge = left; } left = next; ``` `RR1` & `RR2` 由於兩者對稱,因此我們可知 `RR1` = `XOR(merge->addr, right)`, `RR2 = merge` ### 延伸問題 #### 解釋上述 XOR linked list 排序實作的原理,並指出其中可改進之處 這邊的排序,基本上就是 [merge sort](https://alrightchiu.github.io/SecondRound/comparison-sort-merge-sorthe-bing-pai-xu-fa.html)。然而因為 `left` 只有一個節點,故 `merge sort` 效果不佳,會退化成 [insertion sort](http://alrightchiu.github.io/SecondRound/comparison-sort-insertion-sortcha-ru-pai-xu-fa.html),因此我們可以參考 [lab0](https://github.com/kylekylehaha/lab0-c/blob/master/queue.c) 的做法,將 `split` 加到 `sort`:從中間開始切。 #### 優化 XOR-merge sort * 根據 [Optimizing merge sort](https://en.wikipedia.org/wiki/Merge_sort#Optimizing_merge_sort),為了滿足 locality of reference,當子序列小於 S 時,便不再切下去,而是利用 in-place algo. (e.g. insertion sort or bubble sort) 去做排序。(其中 S 的大小為 CPU cache 的大小。) > For example, the tiled merge sort algorithm stops partitioning subarrays when subarrays of size S are reached, where S is the number of data items fitting into a CPU's cache * 這次實驗,我們用 `insertion_sort` 來當作 optimizing merge sort 的 in-place algo. * 因此,在這次實驗我們會製作 `print`, `insertion_sort` 和 `op_merge_sort` **print()** * 將 list 內的 `data` 印出來,方便 debug。 ```cpp void print(list *l) { if (!l) { printf("list is not exist now\n"); return; } printf("list: "); list *next = l->addr; while(l) { list *tmp; printf("%2d", l->data); if (next) { tmp = XOR(next->addr, l); } l = next; next = tmp; } printf("\n"); return; } ``` * 測試一下: ```cpp int main(){ list **start; for(int i = 0;i < SIZE;i++){ insert_node(start, i); } print(*start); return 0; } ``` * 輸出: ```cpp list: 9 8 7 6 5 4 3 2 1 0 ``` **insertion_sort** * 這邊我們會有兩個序列,分別是 `sorted` & 尚未排序的序列,每次都將尚未排序的序列取一個出來,和 `sorted` 一起丟入 `merge` 內做排序。 ```cpp list *merge(list *left, list *right) { list *start = NULL; for (list *merge = NULL; left || right;) { if (!right || (left && left->data < right->data)) { list *next = left->addr; if (next) next->addr = XOR(left, next->addr); if (!merge) { start = merge = left; merge->addr = NULL; } else { merge->addr = XOR(left, merge->addr); left->addr = merge; merge = left; } left = next; } else { list *next = right->addr; if (next) next->addr = XOR(right, next->addr); if (!merge) { start = merge = right; merge->addr = NULL; } else { merge->addr = XOR(right, merge->addr); right->addr = merge; merge = right; } right = next; } } return start; } ``` ```cpp list *insertion_sort(list *head) { if (!head || !head->addr) return head; list *sorted = head; list *next = sorted->addr; if (next) next->addr = XOR(sorted, next->addr); sorted->addr = NULL; for (list *cur = next; cur;) { next = cur->addr; if (next) next->addr = XOR(cur, next->addr); cur->addr = NULL; sorted = merge(sorted, cur); cur = next; } return sorted; } ``` * 測試一下: ```cpp int main(){ list **start; srand((unsigned)time(NULL)); for(int i = 0;i < SIZE;i++){ insert_node(start, rand()%100); } print(*start); *start = insertion_sort(*start); print(*start); return 0; } ``` * 輸出: ```cpp list: 31 36 2 43 68 82 53 13 9 38 list: 2 9 13 31 36 38 43 53 68 82 ``` **Optimizing merge sort** * 在 optimizing merge sort,當序列小於 `S` 時,我們使用 `insertion_sort` ; 大於 `s` 時則繼續遞迴 `op_merge_sort` * 基本上作法和一般的 merge sort 一樣,只差在需要判斷序列長度是否小於 threshold。 * 其中 `split` 用來平均切割序列,切割成兩個長度相同或是長度相差 1 的 子序列。 ```cpp list *op_merge_sort(list *start, int list_len, int thres) { if (!start || !start->addr) return start; if (list_len < thres) { return insertion_sort(start); } list *head = start; list *left, *right; int left_len = 0, right_len = 0; split(head, &left, &right, &left_len, &right_len, thres); left = op_merge_sort(left, left_len, thres); right = op_merge_sort(right, right_len, thres); return merge(left, right); } ``` ```cpp static void split(list *src, list **left, list **right, int *left_len, int *right_len, int thres) { list *fast = src, *prev_fast = NULL, *next_fast; list *slow = src, *prev_slow = NULL, *next_slow; int len = 0; while (fast && XOR(fast->addr, prev_fast)) { /* fast = fast->next->next */ next_fast = XOR(fast->addr, prev_fast); fast = XOR(fast, next_fast->addr); prev_fast = next_fast; /* slow = slow->next */ next_slow = XOR(slow->addr, prev_slow); prev_slow = slow; slow = next_slow; len++; } *left = src; if (!src) { *right = NULL; } else if (fast) { /* list is odd */ list *next = XOR(prev_slow, slow->addr); next->addr = XOR(slow, next->addr); *right = next; slow->addr = XOR(slow->addr, next); *left_len = len; *right_len = len + 1; } else { /* list is even */ prev_slow->addr = XOR(prev_slow->addr, slow); slow->addr = XOR(prev_slow, slow->addr); *right = slow; *left_len = *right_len = len; } return; } ``` * 測試一下 ```cpp ... list **start ; *start = NULL; for (int j = 0;j < LIST_LEN; j++) { insert_node(start, rand()%100); } printf("before sort\n"); print(*start); *start = op_merge_sort(*start, LIST_LEN, 6); print(*start); ... ``` * 輸出 ```cpp before sort list: 23 75 71 6 29 71 20 47 17 51 64 66 47 40 22 34 61 94 24 31 list: 6 17 20 22 23 24 29 31 34 40 47 47 51 61 64 66 71 71 75 94 ``` **利用實驗找出 threshold `S`** * 我們將 `LIST_LEN` 長度拉長,可以比較明顯看出 `op_merge_sort` 的效果。 ```cpp #define LIST_LEN 10000 ``` * 將程式固定在 CPU 0 上面,可以減少 context switch 的影響 ```cpp taskset 0x1 [execute] ``` * 我們將 `S` 的範圍從 0 ~ 1000 ,可以清楚發現有四個階段 ![](https://i.imgur.com/mz66jwI.png) * 實驗結果討論 * 我們可以利用 `lstopo` 命令來看出自己 cpu 的架構。 > 須先安裝套件: `$ sudo apt-get install hwloc` * ![](https://i.imgur.com/19itwsO.png) * 由此可知,cpu 內有四階 cache,分別對應上面實驗的四種階段。 * 因此根據實驗結果,可以得知當 `threshold` 小於 150 左右時,`op_merge_sort` 效果較佳。 **比較 `insertion_sort`, `merge_sort` 和 `op_merge_sort`** * 分別呼叫 `insertion_sort`, `merge_sort` 和 `op_merge_sort`,比較三者的時間。 ![](https://i.imgur.com/Gmgiiix.png) * 實驗結果討論 * 可以明顯看出 `op_merge_sort` 最佳,`insertion_sort` 最差,這和預期的結果符合。 * 由於是用 `rand()%100` ,也就是說在 random 的情況下,`insertion_sort` 不太會出現 worst case,也就是遞減的順序,因此其時間沒有差太多。 * [測驗一程式碼](https://github.com/kylekylehaha/self-practice/tree/master/c/xor_linked_list) #### 修改 lab0-c 裡頭的 harness.c,將原本內部使用的 doubly-linked list 換為 XOR linked list,觀察記憶體佔用率的變化,可設計頻繁的 test_malloc / test_free 呼叫交錯情境。 --- ## 測驗 2 基本上,這題就是 Linus Torvald 在 TED 演講所提到的「有品味的程式碼」,可以參照 [fcamel 的心得](https://medium.com/fcamels-notes/%E5%BE%9E%E5%88%AA%E9%99%A4-linked-list-node-%E7%9C%8B%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88%E7%9A%84%E5%93%81%E5%91%B3-b597cc5af785)。我們直接來看問題: ```cpp #include <stddef.h> struct node { int data; struct node *next; } *head; void insert(struct node *newt) { struct node *node = head, *prev = NULL; while (node != NULL && node->data < newt->data) { prev = node; node = node->next; } newt->next = node; if (prev == NULL) head = newt; else prev->next = newt; } ``` 可透過以下 pointer to pointer 改寫 insert 函式,功能等價但更簡潔,如下: ```cpp void insert(struct node *newt) { struct node **link = &head; while (*link && AA) BB; newt->next = *link; *link = newt; } ``` 兩者差別 * 上面的程式碼需要考慮特殊情形,也就是插入的即為 head,因此需要條件式來判斷。 * 下面的程式碼直接考慮 target 的位置,因此不須特別用條件逝去判斷特殊情形。 ==解題思路== `AA` * 由於 `link` 是 pointer to pointer,因此必須先 dereference 才會變成 pointer。故 `(*link)->data` 才能取到 data。須注意 `*` 與 `->` 的 [prioirty](https://en.cppreference.com/w/c/language/operator_precedence),因為 `->` priority 比較高,因此要加括號。 * 此題答案為 `(*link)->data < newt->data` `BB` * 當 `(*link)->data < newt->data` 時, `*link` 要移動到下一項,但因為 c 只有 `call by value` ,因此要改變指標內的值需要傳入位置。 * 故本題答案為 `link = &(*link)->next ` ### 延伸問題 #### 解釋程式運作原理、對執行時間的影響 (考慮到 branch predictor 行為),和指出其實作限制 * `insert_v1` 為第一個 insert。稱之為 **"ori insert"** * `insert_v2` 為第二個 insert,也就是 pointer to pointer。稱之為 **"tasted insert"** * `clear` 為清除 linked list ```cpp struct timespec start, end; clock_gettime(CLOCK_REALTIME, &start); for (int i = 0;i < LEN;i++) { insert_v2(create(i)); } clock_gettime(CLOCK_REALTIME, &end); printf("tasted insert time: %u ns\n", diff_in_ns(start, end)); clear(); /* test for insert_v2. i.e. tasted insert */ struct timespec t3, t4; clock_gettime(CLOCK_REALTIME, &t3); for (int i = 0;i < LEN;i++) { insert_v1(create(i)); } clock_gettime(CLOCK_REALTIME, &t4); printf("ori time: %u ns\n", diff_in_ns(t3, t4)); clear(); ``` * 輸出:和預期的相同, tasted insert 因為 branch 比較少,故執行時間較短。 ```cpp tasted insert time: 156140968 ns ori time: 225535635 ns ``` * 利用 [perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial) 來確認 branch 的次數 ```shell $ gcc -o ll linked_list.c $ sudo perf record -e branch-misses:u,branch-instructions:u ./ll $ sudo perf report ``` **insert_v1** ```cpp 288 branch-misses:u 714 branch-instructions:u ``` * u 代表 userspace **insert_v2 (tasted insert)** ```cpp 250 branch-misses:u 640 branch-instructins:u ``` * [測驗二程式碼](https://github.com/kylekylehaha/self-practice/blob/master/c/tasted_linked_list.c) #### 在 Linux 核心找出運用類似的手法的程式碼並解說 ## Reference * [XOR linked list](http://www.tastones.com/zh-tw/stackoverflow/data-structures/linked-list/xor_linked_list/) * [從刪除 linked-list node 看程式設計的品味](https://medium.com/fcamels-notes/%E5%BE%9E%E5%88%AA%E9%99%A4-linked-list-node-%E7%9C%8B%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88%E7%9A%84%E5%93%81%E5%91%B3-b597cc5af785)