# 2020q3 Homework1 (lab0) contributed by < [WeiCheng14159](https://github.com/WeiCheng14159) > ###### tags: `sysprog2020` ## Outline [TOC] ## 環境 ```shell $ uname -a Linux 5.4.0-47-generic #51~18.04.1-Ubuntu SMP Sat Sep 5 14:35:50 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux $ gcc --version gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0 ``` ## 作業要求 在 ``queue.[c/h]`` 中完成以下實作 * `q_new`: 建立新的「空」佇列; * `q_free`: 釋放佇列所佔用的記憶體; * `q_insert_head`: 在佇列開頭 (head) 加入 (insert) 給定的新元素 (以 LIFO 準則); * `q_insert_tail`: 在佇列尾端 (tail) 加入 (insert) 給定的新元素 (以 FIFO 準則); * `q_remove_head`: 在佇列開頭 (head) 移除 (remove) 給定的元素。 * `q_size`: 計算佇列中的元素數量。 * `q_reverse`: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列元素,換言之,它只能重排已經存在的元素; * `q_sort`: 以==遞增==順序來排序鏈結串列的元素 ## 實作流程 ### queue.h * 在 `queue.h` 中增加的 `list_ele_t *tail`, `unsigned int size;` 使得 `q_size()` , `q_insert_tail()` 能在 $O(1)$ 時間內完成 * 加上 `unsigned int size` 的同時也限制了queue的容量 * queue 所需的記憶體空間增加為 `2 x sizeof(list_ele_t*) + sizeof(unsigned int)` ```cpp /* Queue structure */ typedef struct { list_ele_t *head; /* Linked list of elements */ list_ele_t *tail; /* Linked list of elements */ int size; /* size of the queue*/ } queue_t; ``` ### queue.c * **q_new** * 操作 `*q` 之前,先確認 `malloc` 是否成功 * `q->size` 初始值為0 ```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** * 利用 `tmp` 指標路過每一個節點,並釋放記憶體 ```cpp void q_free(queue_t *q) { if (!q) return; while (q->head) { list_ele_t *tmp = q->head; q->head = q->head->next; free(tmp->value); free(tmp); } free(q); } ``` * **q_insert_head** * 若 `q` 為 `NULL` 則不能插入任何節點 * `strlen()` 函數計算 `c-style string` 的長度,注意此長度不包含最後的 `\0` * 若任意一個 `malloc` 無法取得記憶體,則應釋放已經持有之記憶體 * 參照 CERN [Common vulnerabilities guide for C programmers](https://security.web.cern.ch/recommendations/en/codetools/c.shtml) 的建議使用 `strlcpy` 取代 `strcpy` 以避免 buffer overflow 的問題 ```cpp bool q_insert_head(queue_t *q, char *s) { if (!q || q->size == INT_MAX) return false; // strlen calculates the lenght of C string //(without including the terminating null character itself). int strLen = strlen(s) + 1; list_ele_t *newHead = (list_ele_t *) malloc(sizeof(list_ele_t)); char *val = (char *) malloc(sizeof(char) * strLen); // either one of malloc fail to allocate memory if (!newHead || !val) { free(newHead); free(val); return false; } strlcpy(val, s, strLen); newHead->value = val; newHead->next = q->head; q->head = newHead; if (q->size == 0) { // empty queue q->tail = newHead; } q->size++; return true; } ``` * **q_insert_tail** * 同上 ```cpp bool q_insert_tail(queue_t *q, char *s) { if (!q || q->size == INT_MAX) return false; int strLen = strlen(s) + 1; list_ele_t *newTail = (list_ele_t *) malloc(sizeof(list_ele_t)); char *val = (char *) malloc(sizeof(char) * strLen); if (!newTail || !val) { free(newTail); free(val); return false; } strlcpy(val, s, strLen); newTail->value = val; newTail->next = NULL; if (q->size == 0) { // empty queue q->head = newTail; } else { q->tail->next = newTail; } q->tail = newTail; q->size++; return true; } ``` * **q_remove_head** * 需要計算 `strlcpy` 實際要拷貝多大的記憶體 ```cpp bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (!q || !sp || !q->head) return false; list_ele_t *rmElem = q->head; int strLen = strlen(rmElem->value) + 1; size_t realBufSize = bufsize > strLen ? strLen : bufsize; strlcpy(sp, rmElem->value, realBufSize); q->head = q->head->next; q->size--; free(rmElem->value); free(rmElem); return true; } ``` * **q_size** * 在 $O(1)$ 時間回傳queue的大小 ```cpp int q_size(queue_t *q) { if (!q) return 0; else return q->size; } ``` * **q_reverse** * 標準的 `linked list` 反轉 ```cpp void q_reverse(queue_t *q) { if (!q || !q->head) return; list_ele_t *tmp = q->head; list_ele_t *prev = NULL; while (q->head != NULL) { list_ele_t *next = q->head->next; q->head->next = prev; prev = q->head; q->head = next; } q->head = q->tail; q->tail = tmp; } ``` * **q_sort** * 參考課程提供的連結 [Linked LIst Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) * 使用 Merge sort 因為時間複雜度最佳 * `list_cmp` 決定兩個list的順序 ```cpp inline bool list_cmp(list_ele_t *l1, list_ele_t *l2) { // assume *l1 and *l2 aren't NULL return (strcmp(l1->value, l2->value) >= 0) ? false : true; } ``` * `merge` 依據兩個list的內容合併為一個list (舊的 recursive 版本) ```cpp list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { if (!l2) return l1; if (!l1) return l2; if (list_cmp(l1, l2)) { // compare 2 lists l1->next = merge(l1->next, l2); return l1; } else { l2->next = merge(l1, l2->next); return l2; } } ``` * `merge` [更] 兩個list的內容合併為一個list(新的 iterative 版本) ```cpp list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { if (!l2) return l1; if (!l1) return l2; list_ele_t *tmp, *head; if (list_cmp(l1, l2)) { tmp = l1; l1 = l1->next; } else { tmp = l2; l2 = l2->next; } head = tmp; while (l1 && l2) { if (list_cmp(l1, l2)) { // compare 2 lists tmp->next = l1; l1 = l1->next; } else { tmp->next = l2; l2 = l2->next; } tmp = tmp->next; } if (!l1) tmp->next = l2; if (!l2) tmp->next = l1; return head; } ``` * `merge_sort` 回傳排序好的指針 ```cpp 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); // merge sorted list l1 and l2 return merge(l1, l2); } ``` * 需要額外處理tail pointer ```cpp void q_sort(queue_t *q) { if (!q || !q->head || !q->head->next) return; q->head = merge_sort(q->head); list_ele_t *tmp = q->head; while (tmp->next) { tmp = tmp->next; } q->tail = tmp; } ``` ## Debug * 遇到的問題: `make test` 後第15個test case有問題 * `+++ TESTING trace trace-15-perf: # Test performance of insert_tail, size, reverse, and sort` * 處理流程 * 進入 `./traces` 資料夾找到 `trace-15-perf.cmd` 的測試指令如下 ``` # Test performance of insert_tail, size, reverse, and sort option fail 0 option malloc 0 new ih dolphin 1000000 it gerbil 1000000 size 1000 reverse sort size 1000 ``` * `make clean` 後重新編譯,利用 `make SANITIZER=1` 開啟記憶體檢測再執行測試 ``` ==9115==ERROR: AddressSanitizer: stack-overflow on address 0x7ffd01123ff8 (pc 0x7f908757fe6f bp 0x7ffd01124860 sp 0x7ffd01123fc0 T0) ``` * 結果得知 `queue.c` 的第 206 行有 stack-overflow 的問題,估計是遞迴函數 `list_ele_t *merge(list_ele_t *l1, list_ele_t *l2)` 被遞迴呼叫所造成 * 參考 [Merge two list iteratively](https://www.geeksforgeeks.org/merge-two-sorted-lists-place/) 實做 iteratively merge two list 的方法,結果如下: * 如果允許使用額外的記憶體,可以使用一個 `dummy node` 使程式碼更簡潔,但本題要求 `in-place sorting` 故不適用 * 不使用 `dummy node` 則程式較冗長 ```cpp list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { if (!l2) return l1; if (!l1) return l2; list_ele_t *tmp, *head; if (list_cmp(l1, l2)) { tmp = l1; l1 = l1->next; } else { tmp = l2; l2 = l2->next; } head = tmp; while (l1 && l2) { if (list_cmp(l1, l2)) { // compare 2 lists tmp->next = l1; l1 = l1->next; } else { tmp->next = l2; l2 = l2->next; } tmp = tmp->next; } if (!l1) tmp->next = l2; if (!l2) tmp->next = l1; return head; } ``` * `make test` 全數通過且 `iterative` 版本的 `merge` 函式已更新 ## Valgrind 實驗 * 實驗:利用 `Massif` 視覺化指令檔 `trace-perf.cmd` 的記憶體使用量 * 其中 `traces/trace-perf.cmd` 的指令如下 ``` # 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 option fail 0 option malloc 0 new ih ABC 10000 sort reverse sort free new ih ABC 50000 sort reverse sort free new ih ABC 100000 sort reverse sort free ``` * 環境設定:依照官方建議安裝 [massif-visualizer](https://github.com/KDE/massif-visualizer) `$ sudo apt install massif-visualizer ` * 執行測試:`valgrind --tool=massif ./qtest < traces/trace-perf.cmd` * 記憶體佔用情形 ![](https://i.imgur.com/W17hdMS.png) * 共有三個記憶體佔用峰值,分別是 986.5 KiB, 4.8 MiB, 9.5 MiB * 分別對應到10000/50000/100000個 `list_ele_t`,其中每一個 element 的內容都是 `ABC` * 分析記憶體佔用如下: * `queue_t` 佔用兩個 `pointer` 跟一個 `int` 共20 bytes * `list_ele_t` 佔用一個 `char*` 跟一個 `list_ele_t*` 共16 bytes * 每個`ABC` c-style string 佔用4 bytes,共有10000/50000/100000個 * 10000個 `list_ele_t` 會佔用20+(16+4)*10000 bytes ~= 200KiB * 50000個 `list_ele_t` 會佔用20+(16+4)*50000 bytes ~= 1MiB * 100000個 `list_ele_t` 會佔用20+(16+4)*100000 bytes ~= 2MiB * 估計值與實際值有不小的差距,還在找原因 ## branch 最佳化實驗 * 起因:參考[GNU/Linux 開發工具共筆](https://hackmd.io/@sysprog/gnu-linux-dev/)的perf效能分析工具,想要了解這個程式的提昇空間 * 使用 `traces/trace-perf.cmd` 測試 queue 的排序效能,指令檔如下 ``` option fail 0 option malloc 0 new ih RAND 100000 sort free ``` * 利用 `perf record` 紀錄 `branch-misses` 和 `branch-instructions` 的發生次數 * `$ perf record -e branch-misses:u,branch-instructions:u ./qtest < traces/trace-perf.cmd` * 結果: * `branch-misses` 前五名 | | branch-misses | |--------|-----------------------------| | 50.04% | merge | | 14.44% | __strcmp_sse2_unaligned | | 8.34% | q_insert_head | | 4.22% | __random | | 2.97% | _IO_default_xsputn | * `branch-instructions`前五名 | | branch-instructions | |--------|-----------------------------| | 12.81% | merge | | 11.38% | fill_rand_string | | 9.49% | __random | | 8.86% | __strcmp_sse2_unaligned | | 6.47% | __random_r | * 分析:發現 linked list 的操作 branch 的機會本來就很多不像 loop 可以 unroll 減少 branch misses 故找不到最佳化切入點作罷。想了解 `__strcmp_sse2_unaligned` 函式有沒有最佳化的空間(因為看到 `unaligned` 感覺可以做更好)但是看不懂 `x86 assembly` 故作罷。 ## 論文研讀 `Dude, is my code constant time?` 這篇論文中藉由偵測執行時間,來獲取程式的時間複雜度資訊。其中運用處理器洩漏出來的資訊,反向推斷程式執行的狀態之研究領域被稱為 `side-channel domain` 。近期較出名的相關研究有 `Meltdown` `Spectrum` 等漏洞,是利用處理器硬體架構的特性作為攻擊的手段,是為 `side-channel attack` 。 本篇論文的貢獻是,將上述之特性運用到程式時間複雜度之量測上。具體之作法是給程式兩個不同類型的輸入資料(i.e. fix-vs-random 測試),並觀察給定不同的輸入資料,所造成的執行時間分佈在統計上是否相同。以下是詳細的步驟: * 步驟一:量測時間 * 運用 `fix-vs-random` 產生兩種測試資料,顧名思義,第一種資料皆是常數,第二種資料是亂數 * 藉由處理器內部的暫存器得知執行時間,確切的程式碼可在 `dudect/cpucycles.h` 找到 ```c=1 #include <stdint.h> // http://www.intel.com/content/www/us/en/embedded/training/ia-32-ia-64-benchmark-code-execution-paper.html static inline int64_t cpucycles(void) { #if defined(__i386__) || defined(__x86_64__) unsigned int hi, lo; __asm__ volatile("rdtsc\n\t" : "=a"(lo), "=d"(hi)); return ((int64_t) lo) | (((int64_t) hi) << 32); #else #error Unsupported Architecture #endif } ``` * 根據連結 [How to Benchmark Code Execution Times on Intel® IA-32 and IA-64 Instruction Set Architectures](https://www.intel.com/content/www/us/en/embedded/training/ia-32-ia-64-benchmark-code-execution-paper.html) 發現在 x86 或 x86_64 架構中,程式使用 `rdtsc` 指令讀取執行的 cycle 數並不準確 * 問題描述如下:Intel CPU 每一個核心有個 timestamp counter 來計算執行的 cycle 數目,並可使用 `RDTSCP (Read Time-Stamp Counter and Processor ID IA assembly instruction)` 和 `RDTSC` 指令來存取執行的 cycle 數目 * 乍看之下,如按照以下步驟,我們應該可以得到準確的執行時間 * `disable preemption on our CPU` * `disable hard interrupts on our CPU` * 讀取 `timestamp counter` * 執行待測的程式 * 再次讀取 `timestamp counter` * `enable preemption on our CPU` * `enable hard interrupts on our CPU` * 將兩個 `cycle count` 相減得到佔用 `cycle` 數 * Intel 指出這樣的計算方式並不能夠得到正確的執行時間,因為現今多數的處理器都支援亂序執行 (OOOE) 來實現 `latency hiding` 故不能保證上述的指令在 CPU 中是按照順序執行的,解決方法是在讀取 `RDTSC` 暫存器之前加入序列化指令 (`serializing instruction`) 告訴 CPU 接下來的指令必須嚴格按照順序執行。另外,還有 `Register Overwriting` 的問題(此處不贅述) * Intel 建議正確的時間測量程式應該如此撰寫: ```c=1 asm volatile ("CPUID\n\t" "RDTSC\n\t" "mov %%edx, %0\n\t" "mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low):: "%rax", "%rbx", "%rcx", "%rdx"); /***********************************/ /*call the function to measure here*/ /***********************************/ asm volatile("RDTSCP\n\t" "mov %%edx, %0\n\t" "mov %%eax, %1\n\t" "CPUID\n\t": "=r" (cycles_high1), "=r" (cycles_low1):: "%rax", "%rbx", "%rcx", "%rdx"); ``` * 其中 `CPUID` 是序列化指令,`RDTSCP` 是讀取 `timestamp counter` 的指令 * Intel 建議的時間測量方法跟本篇論文的測量方法略有不同,本篇論文沒有考慮 OOOE 對時間測量的影響,需要實際測試一下 Intel 建議的方法對時間複雜度的判定會不會有影響(i.e. 在高負載的 CPU 上執行會不會影響 `dudect` 程式判定時間複雜度的結果) * 此外,根據以下 `dudect/constant.c` 中的程式碼,`cpucycles()` 函式讀取 `timestamp register` 之前並沒有按照 Intel 的建議關閉 `preemption` 和 `hard interrupt` ,作業系統層級的時間影響還要再多做測試 ```c=55 void measure(int64_t *before_ticks, int64_t *after_ticks, uint8_t *input_data, int mode) { assert(mode == test_insert_tail || mode == test_size); if (mode == test_insert_tail) { for (size_t i = drop_size; i < number_measurements - drop_size; i++) { char *s = get_random_string(); dut_new(); dut_insert_head( get_random_string(), *(uint16_t *) (input_data + i * chunk_size) % 10000); before_ticks[i] = cpucycles(); dut_insert_tail(s, 1); after_ticks[i] = cpucycles(); dut_free(); } } else { for (size_t i = drop_size; i < number_measurements - drop_size; i++) { dut_new(); dut_insert_head( get_random_string(), *(uint16_t *) (input_data + i * chunk_size) % 10000); before_ticks[i] = cpucycles(); dut_size(1); after_ticks[i] = cpucycles(); dut_free(); } } } ``` * 步驟二:資料後處理 * 執行時間的判定會被外部干擾所影響(i.e. `context switch` )故需要捨棄不合理的資料,捨棄的標準需要手動設定 `(p-th percentile)` * 步驟三:統計上的判定 * `Hypothesis Testing` 簡介: * `Null hypothesis (H0)` 是我們想要測定的假說 * `Alternative hypothesis (H1)` 是 `H0` 的反面 * 結果的判定: * `Reject Null Hypothesis` 代表我們在統計上有足夠的證據,證明 `H0` 是錯的,故 `H1` 是對的 * `Fail to reject Null Hypothesis` 代表在統計上==沒有==足夠的證據,證明 `H0` 是錯的 * 在本論文中 `Welch’s t-test` 的運用如下: * 定義: * $\mu_1$:`class 1` 真正的執行時間的平均值(未知) * $\mu_2$:`class 2` 真正的執行時間的平均值(未知) * $\sigma_1$:`class 1` 採樣的執行時間的標準差(已知) * $\sigma_2$:`class 2` 採樣的執行時間的標準差(已知) * $n_1$:`class 1` 的樣本數(已知) * $n_2$:`class 2` 的樣本數(已知) * $\bar x_1$:`class 1` 採樣的的執行時間的平均值(已知) * $\bar x_2$:`class 2` 採樣的的執行時間的平均值(已知) * $H_0$:$\mu_1 - \mu_2 = 0$ 假設`class 1` 和`class 2` 的平均執行時間相等,亦為 ==『兩個 `class` 的執行時間分佈是相同的』== * $H_1$:$\mu_1 - \mu_2 \neq 0$ 假設`class 1` 和`class 2` 的平均執行時間並不相等,亦為 ==『兩個 `class` 的執行時間分佈是不同的』== * 計算檢定統計量 $t = \frac{\bar x_1 - \bar x_2}{\sqrt{\frac{\sigma_1^2}{n_1}+ \frac{\sigma_2^2}{n_2}}}$ * 若 $|t| \gt z_{\frac{\alpha}{2}}$ 則 `reject` $H_0$ 進而得到 $\mu_1 \neq \mu_2$ ,代表以不同 `class` 為輸入的執行時間的平均在統計上是不同的 * 若 $|t| \leq z_{\frac{\alpha}{2}}$ 則 無法 `reject` $H_0$ 推得 $\mu_1$ ==可能== 等於 $\mu_2$ ,在統計上無法判定 * $z_{\frac{\alpha}{2}}$ 之值在統計上要查表,論文中卻使用 `#define t_threshold_moderate 10` 來計算不知為何 * 檢定統計量的判定可以在 `static bool report(void)` 中找到 * 例如:`class 1` 為常數資料,`class 2` 為亂數資料。將兩種資料餵給程式,並計算多筆執行時間,帶入上述公式計算檢定統計量,如果可以 `reject` $H_0$ 則此程式的時間複雜度在統計上並不如其所宣稱的好,因為亂數輸入的時間複雜度和常數輸入的時間複雜度不相同,原因可能是亂數輸入觸發了程式中的某些執行路徑,導致時間複雜度增加