# 2020q1 Homework3 (quiz3) contributed by < `jeeeff` > ## 解釋上述 XOR linked list 排序實作的原理,並指出其中可改進之處 ### XOR 為鍊結與一般指標的差別 一般指標只需要用 node.link 就可以直接指向下個節點,但 XOR 的 list 需要用 `next = XOR(prev, curr->addr);` 這種 XOR 的特性往前後移動 ```cpp while (curr) { /* ACTION */ next = XOR(prev, curr->addr); prev = curr; curr = next; } ``` * 在這次作業裡多次用到上面的 while 迴圈來移動 ### sort ```clike= list *sort(list *start) // 傳 head 進副程式 sort,並回傳 head { if (!start || !start->addr) // 若 list 為空或只有一項,直接回傳 head return start; list *left = start, *right = start->addr; left->addr = NULL; right->addr = XOR(right->addr, left); // 切割 list ,left 僅一項,其餘在 right ,所以 merge sort 退化為 insertion sort left = sort(left); // right = sort(right); // divide (Partition) for (list *merge = NULL; left || right;) { // conquer and then merge if (!right || (left && left->data < right->data)) { // 子序列(right)尚未為空 // 或左項(left->data)小於右項(right->data) // merge 為最終要輸出的主序列(已排序) list *next = left->addr; // next 是暫存變數,負責插入的動作 if (next) // 若 left 不為 list 的尾巴,用 XOR 紀錄 addr next->addr = XOR(left, next->addr); if (!merge) { // 這是剛開始 Sort 的情況,第一項的 addr 指向 NULL start = merge = left; // start 為 merge 的 head merge->addr = NULL; } else { merge->addr = XOR(merge->addr, left); // 用 XOR 紀錄 addr 指向的位置 left->addr = merge; // left 這項為目前的尾巴, addr 只要 XOR(merge, NULL) 就好 merge = left; // merge 項右移一格到尾巴(left) } left = next; // left 子序列往後移一格,繼續做下去 } else { // 子序列為空或左項(left->data)不小於右項(right->data) list *next = right->addr; if (next) next->addr = XOR(right, next->addr); if (!merge) { // 這也是剛開始 Sort 的情況,第一項的 addr 指向 NULL start = merge = right; merge->addr = NULL; } else { merge->addr = XOR(merge->addr, right); right->addr = merge; merge = right; } right = next; // right 子序列往後移一格,繼續做下去 } } return start; } ``` ### 可改進之處 * Insertion Sort 的時間複雜度平均是 O(n^2^),如果在 Partition 的時候使 left 子序列盡量和 right 子序列一樣長, * 預設如上,實際做過一次,發現速度上慢了許多,並沒有降到 Merge sort 的 O(nlogn),先貼 code 再貼圖 ```clike= list *merge_sort(list *start, int cut) { if (!start || !start->addr) return start; list *left = start, *right = start->addr, *left_end = start; int mid = cut / 2; for (int i = 0; i < mid - 1; i++) { list *tmp; tmp = right; right = XOR(right->addr, left_end); left_end = tmp; } left_end->addr = XOR(right, left_end->addr); right->addr = XOR(right->addr, left_end); left = merge_sort(left, mid); right = merge_sort(right, cut - mid); left = merge_sort(left, mid); right = merge_sort(right, cut - mid); ... ``` ![](https://i.imgur.com/BqAo22L.png) * 可見這方式是失敗的 :::danger 用語該精確,什麼「失敗」? :notes: jserv ::: ### main * 附上目前 main 主程式 ```cpp #define count 100 // for average #define round 500 // for size int main() { int i, j, input; long long ts[round], tm[round]; struct timespec start, stop; FILE *fp; if(!(fp = fopen("time.txt","w"))) { printf("Open file error\n"); return -1; } for(j = 0; j < round; j++) { ts[j] = 0; tm[j] = 0; } for(j = 0; j < round; j++) { for(i = 0; i < count; i++) { struct __list *a = malloc(sizeof(struct __list)); struct __list *b = malloc(sizeof(struct __list)); a = NULL; b = NULL; for(int k = 0; k < j; k++) { input = rand(); insert_node(&a, input); insert_node(&b, input); } clock_gettime(CLOCK_MONOTONIC, &start); a = sort(a); clock_gettime(CLOCK_MONOTONIC, &stop); ts[j] += stop.tv_nsec - start.tv_nsec; if (ts[j] < 0 && j > 0) // ts[j] = ts[j - 1]; delete_list(a); printf("\n%dnd sorting-->\n",j); // print_list(b); clock_gettime(CLOCK_MONOTONIC, &start); b = merge_sort(b, j + 1); clock_gettime(CLOCK_MONOTONIC, &stop); // printf("%dn\t sorted\n", i + 1); tm[j] += stop.tv_nsec - start.tv_nsec; // print_list(b); if (tm[j] < 0 && j > 0) // tm[j] = tm[j - 1]; delete_list(b); } ts[j] /= count; tm[j] /= count; fprintf(fp,"%d\t%lld\t%lld\n", j+1, ts[j], tm[j] ); // printf("%d\t%ld\n", j+1, ts); } fclose(fp); } ``` :::danger 附帶一提,這裡是做 100 次取平均,而且不知道為什麼輸出時間可能為負數,若不加上補救方式(如下),最終圖會有許多負的極端值 ::: * 補救方式: ```clike= if (ts[j] < 0 && j > 0) ts[j] = ts[j - 1]; ... if (tm[j] < 0 && j > 0) tm[j] = tm[j - 1]; ``` * 許多負的極端值的圖: ![](https://i.imgur.com/qsRx95T.png) :::danger 1. 不要只用平均值,用統計手法,去除 outlier。 2. 留意 timer 的精準度 :notes: jserv ::: * 另一個改進策略是題目提到的,讓子序列大小$S$恰好為 Cahce 能容納的大小,減少 Paging 次數 ## Optimizing merge sort * 先搜尋 CPU 的 Cache size,使用指令 ``` $ pico /proc/cpuinfo ``` * 找到的硬體資訊如下 ``` processor : 0 cache size : 3072 KB ... processor : 1 cache size : 3072 KB ... processor : 2 cache size : 3072 KB ... processor : 3 cache size : 3072 KB ``` * 然後修改 merge_sort 成 big_s_sort,蠻快的 ```cpp list *big_s_sort(list *start, int cut, int s) { if (!start || !start->addr) return start; list *left = start, *right = start->addr, *left_end = start; int mid = cut / 2; for (int i = 0; i < mid - 1; i++) { list *tmp; tmp = right; right = XOR(right->addr, left_end); left_end = tmp; } left_end->addr = XOR(right, left_end->addr); right->addr = XOR(right->addr, left_end); if(mid > s) { left = big_s_sort(left, mid, s); right = big_s_sort(right, cut - mid, s); } else { 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 = XOR(merge->addr,left); 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(merge->addr,right); right->addr = merge; merge = right; } right = next; } } return start; } ``` * 先設定$S$為 5 測試三個演算法的速度,明顯快出許多 ![](https://i.imgur.com/5lUse92.png) * 接下來就是單純測試 $S$ 與 list length 的關係 ** 先固定 list 長度為 1~10000,S 為 100 ![](https://i.imgur.com/od3ixuU.png) ** 先固定 list 長度為 1~10000,S 為 1000 ![](https://i.imgur.com/RqTCXLj.png) ** 先固定 list 長度為 1~10000,S 為 5000 ![](https://i.imgur.com/0w043K0.png) ## 修改 lab0-c 裡頭的 harness.c