# 2020q1 Homework1 (lab0) contributed by < `colinyoyo26` > ## 實驗環境 - 作業系統: Ubuntu Linux 16.04.6 - 編譯器版本: gcc 7.4.0 :::warning 你的開發環境能升級到 Ubuntu Linux 18.04 或更新的版本嗎?後續的實驗需要 Linux v4.15+ :notes: jserv ::: > 好的,正在升級中 ## 實作過程 ### queue.h #### `queue_t` 資料結構的部份增加了 `tail`和 `size`分別為了在 O(1) 時間 insertion 還有 return size ```cpp typedef struct { list_ele_t *head; list_ele_t *tail; size_t size; } queue_t; ``` ### queue.c #### `q_insert_head` 比起去年,今年多了安全性的檢查,所以把 `strcpy` 替換成了 `strncpy` ```cpp bool q_insert_head(queue_t *q, char *s) { list_ele_t *newh; char *str; if (!q) return false; size_t len = strlen(s); newh = malloc(sizeof(list_ele_t)); str = malloc(len * sizeof(char) + 1); if (!newh || !str) { free(str); free(newh); return false; } strncpy(str, s, len); str[len] = '\0'; newh->value = str; newh->next = q->head; q->head = newh; if (!q->tail) q->tail = q->head; q->size++; return true; } ``` #### `q_insert_tail` 這部份和 `q_insert_head` 差不多 ```cpp bool q_insert_tail(queue_t *q, char *s) { list_ele_t *newh; char *str; if (!q || !s) return false; size_t len = strlen(s); newh = malloc(sizeof(list_ele_t)); str = malloc(len * sizeof(char) + 1); if (!newh || !str) { free(str); free(newh); return false; } strncpy(str, s, len); str[len] = '\0'; newh->value = str; if (!q->head) { q->head = newh; q->tail = newh; } q->tail->next = newh; newh->next = NULL; q->tail = newh; q->size++; return true; } ``` #### `q_remove_head` ```cpp bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (!q || !q->head) return false; if (bufsize > 0 && sp) { strncpy(sp, q->head->value, bufsize - 1); sp[bufsize - 1] = '\0'; } list_ele_t *tem = q->head; q->head = q->head->next; free(tem->value); free(tem); q->size--; return true; } ``` #### `q_size` 直接回傳多加入的 `size` member ```cpp int q_size(queue_t *q) { return !q ? 0 : q->size; } ``` #### `q_reverse` ```cpp void q_reverse(queue_t *q) { if (!q || q->size < 2) return; list_ele_t *cur = q->head, *prev = NULL, *tem; q->tail = q->head; while (cur) { tem = cur; cur = cur->next; tem->next = prev; prev = tem; } q->head = prev; } ``` #### `q_free` 直接 reuse `q_remove_head` 的部份避免重複的 code 為 `free` 和 `q_remove_head` 都會檢查 NULL 所以沒有特別檢查 ```cpp void q_free(queue_t *q) { while (q_remove_head(q, NULL, 0)) ; free(q); } ``` #### `q_sort` 先提及 natural sort 的部份,在 lab0-c 目錄加入 `strnatcmp.h` 和 `strnatcmp.c` 然後在 `queue.h` include `strnatcmp.h` 並用 macro 包裝函式讓 code 比較易讀一點 ```cpp #include "strnatcmp.h" ...... #define LESS_THAN(a, b) strnatcmp(a, b) < 0 ``` `Makefile` 的 `OBJS` 也要加入 `strnatcmp.o`用於靜態連結 ``` OBJS := qtest.o report.o console.o harness.o queue.o strnatcmp.o\ random.o dudect/constant.o dudect/fixture.o dudect/ttest.o ``` `qtest.c` 和 `q_sort` 比較的地方改成 ```cpp LESS_THAN(e->next->value, e->value)` ``` 剛改完 `make test` 因為呼叫 function call 成本的關係會有超時的情況,在 `strnatcmp.c` 內用 `inline` 宣告`strnatcmp0` 就解決了 我採用 merge-sort 實作 `q_sort` , 利用 `q->size` 找到分段點,分成 `left` `right` 分別排序然後在 `q_merge` merge 起來, `LESS_THAN` 用於比較字典排序 - [ ] `q_sort` ```cpp void q_sort(queue_t *q) { if (!q || q->size < 2) return; queue_t left, right; left.size = (q->size >> 1) + (q->size & 1); right.size = q->size >> 1; list_ele_t *cur = left.head = q->head; right.tail = q->tail; for (size_t i = 0; i < left.size - 1; i++) cur = cur->next; left.tail = cur; right.head = cur->next; left.tail->next = right.tail->next = NULL; q->head = q->tail = NULL; q_sort(&left); q_sort(&right); q_merge(&left, &right, q); } ``` - [ ] `q_merge` ```cpp static void q_merge(queue_t *left, queue_t *right, queue_t *q) { q->size = left->size + right->size; list_ele_t *l = left->head, *r = right->head; list_ele_t *tem = NULL; q->head = LESS_THAN(left->head->value, right->head->value) ? left->head : right->head; q->tail = q->head; for (size_t i = 0; i < q->size; i++) { if (!r || (l && LESS_THAN(l->value, r->value))) { tem = l; l = l->next; } else { tem = r; r = r->next; } q->tail->next = tem; q->tail = tem; } tem->next = NULL; } ``` ## Dude, is my code constant time? 利用 leakage detection test 測試程式是否是 constant time ,優點是在這個方法下不需要硬體模型,分為以下三個步驟進行 #### Repeatedly measure exeution time - 分別用兩類(class) input data ,論文中提到的是 fix-vs-random , fixed class 是為了 corner-case #### Apply post-processing - Cropping: 去掉某些極端值 (可能是 OS interrupt 造成......) - High-order preprocessing: (不太懂) #### Apply statistical test - null hypothesis: "the two timing distributions are equal" - if statistical test reject the null hypothesis, then we know it's not run in constant time - [Welch's t-test](https://en.wikipedia.org/wiki/Welch's_t-test): lab0 使用的測試,以下較完整介紹 ### Welch’s t-test 先簡介 Student’s t-distribution 就是抽樣下的常態分佈,抽樣越多就會越接近標準常態分佈(因為越接近母體),圖中的 one-tailed test 和 two-tailed test 和 alternative hypothesis 有關,我們只需要看兩組數據是否不一樣,所以用 two-tailed test > alternativw hypothesis: "the two timing distributions are not equal" > ![](https://i.imgur.com/jeWKqt1.gif) 接著是 Welch’s t-test - 先計算出兩個 class (class 0, class 1) 的平均值和變異數 - Welch's t-test defines the statistic t by the following formula: - $t = \frac{\bar{X_0} - \bar{X_1}}{\sqrt{\frac{Var_0}{N_0} + \frac{Var_1}{N_1}}}$ - $t$ 越接近 0 代表兩組數據的差異越小 - lab0 實作上只有到這裡,然後直接檢查 $t$ 是否大於 `t_threshold_moderate` - approximate df (degree of freedom) using the [Welch–Satterthwaite equation](https://en.wikipedia.org/wiki/Welch%E2%80%93Satterthwaite_equation) - 由 df 我們可以得到相應的 t-distribution 如上圖的鐘型曲線 - 圖上的藍白交界處分別為 +-t - 把藍色部份積分起來可以得到 p value 若 p 小於特定值 (通常 0.05 這部份沒有深入理解) 我們可以說兩組數據有統計上的顯著差異 (statistically significant difference) ## Dudect in lab0 知道理論後,接著是 lab0 的實作 ### `ttest.h` - [ ] `t_ctx` ```cpp typedef struct { double mean[2]; double m2[2]; double n[2]; } t_ctx; ``` - `mean[i]` 為 class i 的平均數 - `m2[i]` 為 class i 的 $M_2$ (which is $Var \cdot (N - 1)$) - `n[i]` 為 class i 的 sample 數 ($N$) ### `fixture.c` 這裡可以看到 `is_size_const` 和 `is_insert_tail_const` 用於測試, `q_size` `q_insert_tail` 是不是 constant time ,以下藉由 `is_insert_tail_const` 說明程式實作原理。 - [ ] `bool is_insert_tail_const(void)` :::info `t` is pointer to `t_ctx` `q` is pointer to `queue_t` ::: - `init_once()` 用於初始化 `*t` 和 `q` (指向 NULL) - 接著會 call `doit(0)` 先測量執行時間,再做統計分析 (t-test) ```cpp bool is_insert_tail_const(void) { bool result = false; t = malloc(sizeof(t_ctx)); for (int cnt = 0; cnt < test_tries; ++cnt) { printf("Testing insert_tail...(%d/%d)\n\n", cnt, test_tries); init_once(); for (int i = 0; i < enough_measurements / (number_measurements - drop_size * 2) + 1; ++i) result = doit(0); printf("\033[A\033[2K\033[A\033[2K"); if (result == true) break; } free(t); return result; } ``` 接著看到 `doit` function - [ ] `static bool doit(int mode)` - `mode` 用於分辨 `is_size_const` 和 `is_insert_tail_const` - `prepare_inputs(input_data, classes)` 會用亂數產生 `input_data` 的值 - 每個成員大小為 `sizeof(uint8_t) * chunk_size` - 用 `classes` 分辨他是 class 0 或 class 1 (也是隨機),被指定到 class 0 的會執行以下 code 把前 2 bytes 會被設為 0 - `*(uint16_t *) (input_data + i * chunk_size) = 0x00` - `measure(before_ticks, after_ticks, input_data, mode)` 先對新建的 queue 中執行數次 `q_insert_head` 接著再測量一次 `q_insert_tail` 的執行時間 (紀錄在 `before_ticks` 和 `after_ticks`) - random string 的插入次數為 `*(uint16_t *) (input_data + i * chunk_size)` 所以 class 0 不會事先插入任何元素 - `differentiate(exec_times, before_ticks, after_ticks)` 由 `before_ticks` 和 `after_ticks` 相減得到執行時間,紀錄在 `exec_times` - `update_statistics` 裡面會 call `t_push` 計算出兩個 class 分別的平均值 (mean) 和 $M_2$ ( $M_2 = Var \cdot (N - 1)$ ) - 計算平均值得公式,把 `delta` 展開可以得到 $\bar{x_i} = \bar{x_{i-1}} + \frac{x_i - \bar{x_{i-1}}}{N} = \frac{(N - 1) \cdot \bar{x_{i-1}} + x_i}{N}$ - $(N - 1) \cdot \bar{x_{i-1}}$ 是前 $N - 1$ 項的總和 - code 中的 `x` 對應到 $x_i$ (第 $i$ 個 sample (execution time)) - 上述的 $\bar{x_{i-1}}$ 對應到 code 中的 `ctx->mean[class]` (更新前) - $N$ 對應到 `ctx->n[class]` (`ctx->n[class]++` 之後) - 接著 $M_2$ 的公式,根據 [Welford's online algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance) $M_{2,i} = M_{2,{i-1}} + (x_i - \bar{x_{i-1}}) \cdot (x_i - \bar{x_{i}})$ - 把 `delta` 展開後符合公式 ```cpp void t_push(t_ctx *ctx, double x, uint8_t class) { assert(class == 0 || class == 1); ctx->n[class]++; double delta = x - ctx->mean[class]; ctx->mean[class] = ctx->mean[class] + delta / ctx->n[class]; ctx->m2[class] = ctx->m2[class] + delta * (x - ctx->mean[class]); } ``` - `report()` 內會在呼叫 `t_compute` 進行 [Welch's t-test](https://en.wikipedia.org/wiki/Welch's_t-test) - $t = \frac{\bar{X_0} - \bar{X_1}}{\sqrt{\frac{Var_0}{N_0} + \frac{Var_1}{N_1}}}$ - $\bar{X_i}$ 為 class i 的平均值 - $Var_i$ 為 class i 的變異數 - $t$ 對應到 code 中的 `t_value` ```cpp double t_compute(t_ctx *ctx) { double var[2] = {0.0, 0.0}; var[0] = ctx->m2[0] / (ctx->n[0] - 1); var[1] = ctx->m2[1] / (ctx->n[1] - 1); double num = (ctx->mean[0] - ctx->mean[1]); double den = sqrt(var[0] / ctx->n[0] + var[1] / ctx->n[1]); double t_value = num / den; return t_value; } ``` - 最後檢查 $|t|$ 是否大於 `t_threshold_moderate` - if yes, reject the null hypothesis ```cpp static bool doit(int mode) { int64_t *before_ticks = calloc(number_measurements + 1, sizeof(int64_t)); int64_t *after_ticks = calloc(number_measurements + 1, sizeof(int64_t)); int64_t *exec_times = calloc(number_measurements, sizeof(int64_t)); uint8_t *classes = calloc(number_measurements, sizeof(uint8_t)); uint8_t *input_data = calloc(number_measurements * chunk_size, sizeof(uint8_t)); if (!before_ticks || !after_ticks || !exec_times || !classes || !input_data) { die(); } prepare_inputs(input_data, classes); measure(before_ticks, after_ticks, input_data, mode); differentiate(exec_times, before_ticks, after_ticks); update_statistics(exec_times, classes); bool ret = report(); free(before_ticks); free(after_ticks); free(exec_times); free(classes); free(input_data); return ret; } ``` ## `select` system call & `console.c` ### `select` 簡單來說 `select` 被用來同時監控多個 file descriptors 並 block 直到有 file descriptors 可以執行 I/O operation 或是 timeout ,資料來自 Linux Programmer's Manual 因為篇幅關係就不貼上來 以下是 `select` 的宣告 ```cpp int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); ``` - `nfds` is the highest-numbered file descriptor in any of the three sets, plus 1. - `timeout` argument specifies the interval that select() should block waiting for a file descriptor to become ready. The call will block until either: - a file descriptor becomes ready - the call is interrupted by a signal handler - the timeout expires > If timeout is NULL (no timeout), select() can block indefinitely. `fd_set` 以及用來操作 `fd_set` 的四個 macro - `fd_set` a fixed size buffer - Executing `FD_CLR()` or `FD_SET()` with a value of fd that is negative or is equal to or larger than `FD_SETSIZE` will result in undefined behavior. - `FD_ZERO()` clear a set - `FD_SET()` add a given file descriptor from a set - `FD_CLR()` remove a given file descriptor from a set - `FD_ISSET()` tests to see if a file descriptor is part of the set - this is useful after select() returns. return value - 一般情況回傳 #file descriptor - timeout return 0 - error return -1 ### `console.c` 以下分析 `console.c` 實作,先簡單介紹 RIO (Robust I/O),他有以下特點 - 自動處理 short count :::info 這個地方課本沒有寫得很具體,是指 `rio_read` 遇到 ``'\n'`` 還會繼續讀,不會像 `read` 直接回傳 short count 嗎 ::: - application-level buffe (對應到下面資料結構的 buf member) - buffer 空時才會 call `read` 重新填滿 - 優點以 `console.c` 的 `readline()`為例,在檢查 `\n` 時不用對每一個 byte 都 call `read` - [ ] `struct RIO_ELE` ```cpp typedef struct RIO_ELE rio_t, *rio_ptr; struct RIO_ELE { int fd; /* File descriptor */ int cnt; /* Unread bytes in internal buffer */ char *bufptr; /* Next unread byte in internal buffer */ char buf[RIO_BUFSIZE]; /* Internal buffer */ rio_ptr prev; /* Next element in stack */ }; ``` - [ ] `int cmd_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)` 這部份函式分成三段來看,call `cmd_select` 時會先執行以下迴圈,把 `buf_stack` 內的 command 先執行完 - `block_flag` 初始為 false 且值沒有被更改所以一直都為 flase - 有宣告 `static` 且 include 的檔案中沒有修改到 - `read_ready()`檢查 `buf_stack` 是否有完整 command line - `readline()` 對應到 CSAPP 就是 `rio_readlineb` - `interpret_cmd(cmdline)` 直譯 `cmdline` - 裡面會 call `parse_args` 對 `cmpline` 進行預處理 (把空白換成 `'\0'`) - 預處理完後會 call `interpret_cmda` 比對 `cmd_list` (command 的 metadata 被用 linked list 串起來) 找出對應的 command ```cpp while (!block_flag && read_ready()) { cmdline = readline(); interpret_cmd(cmdline); prompt_flag = true; } ``` 下面這段的目的是把 `buf_stack->fd` 加到 `readfds` 這個 `fd_set` 內,並把 `nfds` 設成最大的 file descriptor + 1 ,最後會回傳參數三個 set 包含的 file descriptor 總數 - `if (!readfds)` 若參數 `readfds` 傳入 `NULL` 則把 `readfds` 指向 local 空的 set ```cpp if (!block_flag) { /* Process any commands in input buffer */ if (!readfds) readfds = &local_readset; /* Add input fd to readset for select */ infd = buf_stack->fd; FD_SET(infd, readfds); if (infd == STDIN_FILENO && prompt_flag) { printf("%s", prompt); fflush(stdout); prompt_flag = true; } if (infd >= nfds) nfds = infd + 1; } if (nfds == 0) return 0; ``` 接下來是最後的部份 `select` 等到可以進行 I/O operation (讀取 `buf_stack->fd`),並執行寫入的 command - 第一個 `if` 用於檢查 `select` timeout 或 error ,接著把剛剛加入的 file descriptor 從 set 移除 ```cpp int result = select(nfds, readfds, writefds, exceptfds, timeout); if (result <= 0) return result; infd = buf_stack->fd; if (readfds && FD_ISSET(infd, readfds)) { /* Commandline input available */ FD_CLR(infd, readfds); result--; cmdline = readline(); if (cmdline) interpret_cmd(cmdline); } return result; ``` ## 程式現有缺陷 目前朝 `readline` 函式的方向想,for loop 中的第一個 `if` 最多進去一次,剩下的部份等於是一次搬一個 byte 並檢查,如果 command 很長會成為效能瓶頸,於是想到用 `memchr` 和 `memcpy` 試著優化 (不過還沒實驗驗證) ```cpp for (cnt = 0; cnt < RIO_BUFSIZE - 2; cnt++) { if (buf_stack->cnt <= 0) { /* Need to read from input file */ buf_stack->cnt = read(buf_stack->fd, buf_stack->buf, RIO_BUFSIZE); buf_stack->bufptr = buf_stack->buf; if (buf_stack->cnt <= 0) { /* Encountered EOF */ pop_file(); if (cnt > 0) { /* Last line of file did not terminate with newline. */ /* Terminate line & return it */ *lptr++ = '\n'; *lptr++ = '\0'; if (echo) { report_noreturn(1, prompt); report_noreturn(1, linebuf); } return linebuf; } return NULL; } } /* Have text in buffer */ c = *buf_stack->bufptr++; *lptr++ = c; buf_stack->cnt--; if (c == '\n') break; } ``` 我試著改成以下,用 `memchr` 和 `memcpy` 叫有效率的處理資料,且這邊 while loop 最多跑兩次 - [Github](https://github.com/colinyoyo26/2020q1-lab0-c/blob/test2/console.c) ```cpp while (!end && cnt < RIO_BUFSIZE - 2) { if (buf_stack->cnt <= 0) { /* Need to read from input file */ buf_stack->cnt = read(buf_stack->fd, buf_stack->buf, RIO_BUFSIZE); buf_stack->bufptr = buf_stack->buf; if (buf_stack->cnt <= 0) { /* Encountered EOF */ pop_file(); if (cnt > 0) { /* Last line of file did not terminate with newline. */ /* Terminate line & return it */ *lptr++ = '\n'; *lptr++ = '\0'; if (echo) { report_noreturn(1, prompt); report_noreturn(1, linebuf); } return linebuf; } return NULL; } } size_t len = buf_stack->cnt < RIO_BUFSIZE - 2 - cnt ? buf_stack->cnt : RIO_BUFSIZE - 2 - cnt; end = (char *) memchr(buf_stack->bufptr, '\n', len); size_t offset = end ? (intptr_t) end - (intptr_t) buf_stack->bufptr + 1 : len; memcpy(lptr, buf_stack->bufptr, offset); buf_stack->cnt -= offset; buf_stack->bufptr += offset; cnt += offset; lptr += offset; } ``` :::danger 上面的超連結是 dead link,你有對應的測試計畫嗎? :notes: jserv ::: > 已更正連結,目前只有想到比較 `make test` 時花在 `readline` 的總共時間,晚點動手實驗看看 先用 perf 觀察,但沒有發現到熱點 `$ sudo perf record -e cycles:u make test && sudo perf report` 但還是做了簡單的實驗,把 `readline` 改名成 `read_line` 然後在用一個 inline function `readline` 包裝它,把每次呼叫這個函式的時間加到 `total_time` 最後程式結束時會在 `run_console` 最後印出 `total_time` 的值 先清空 cache ,然後 `make test` ``` $ sudo sh -c "/usr/bin/echo 3 > /proc/sys/vm/drop_caches" $ make test ``` 試了幾次發現效能反而比較差一點,先推測試因為 command 都很短,所以呼叫 `memchr` 的成本反更大 ```cpp double total_time = 0; static inline char *readline() { double time = gettime(); char *buf = read_line(); total_time += gettime() - time; return buf; } ``` `read_ready` 也用一樣的邏輯修改 ```cpp static bool read_ready() { if (!buf_stack || !memchr(buf_stack->bufptr, '\n', buf_stack->cnt)) return false; return true; } ``` ## Reference * [Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf) * video: [Student's t-test](https://www.youtube.com/watch?v=pTmLQvMM-1M) * [Welford's online algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance) * [統計學:大家都喜歡問的系列-p值是什麼](https://medium.com/@chih.sheng.huang821/%E7%B5%B1%E8%A8%88%E5%AD%B8-%E5%A4%A7%E5%AE%B6%E9%83%BD%E5%96%9C%E6%AD%A1%E5%95%8F%E7%9A%84%E7%B3%BB%E5%88%97-p%E5%80%BC%E6%98%AF%E4%BB%80%E9%BA%BC-2c03dbe8fddf) * [CS:APP 第 10 章重點提示](https://hackmd.io/@sysprog/H1TtmVTTz)