# 2024q1 Homework1 (lab0)
contributed by < `MiohitoKiri5474` >
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 36 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
Vendor ID: GenuineIntel
Model name: 06/7e
CPU family: 6
Model: 126
Thread(s) per core: 1
Core(s) per socket: 4
Socket(s): 1
Stepping: 5
BogoMIPS: 3993.10
```
## neoVim Config
習慣用 neoVim 當作 editor,有原生支援 language server protocol,提供語法補完和程式碼檢查的功能。
之前為了寫 C++ 而做了一些設定,不過這次打開 `queue.c` 之後發現有無法套用 C++17 的錯誤。
發現是我之前為了寫 C++ 時可以直接套用 C++17 的語法,所以在 `~/.clangd` 裡面新增了相關的 argument,只要 clangd 啟動時就會套用。
研究了要如何按照不同檔案格式套用不同的標準後,改成以下樣式。
```json
CompileFlags:
# Flags for C files
'.*\.c$':
- "-std=c11" # or your preferred C standard
# Flags for C++ files
'.*\.cpp$':
```
同時 neoVim 也支援 formatter,於是在 config 中新增了相關設定,在存檔時自動使用 clang-format formatting。
```lua
return {
"stevearc/conform.nvim",
optional = true,
opts = {
formatters_by_ft = {
["c"] = { "clang_format" },
},
},
keys = {
{
"<leader>ft",
function()
require("conform").format()
end,
},
},
}
```
## `queue.c`
> 目前得分 100。
### `q_new`
用 `link_head` 宣告一個鏈結串列,並用 `INIT_LIST_HEAD` 初始化後回傳。
```c
/* Create an empty queue */
struct list_head *q_new()
{
struct list_head *res =
(struct list_head *) malloc(sizeof(struct list_head));
INIT_LIST_HEAD(res);
return res;
}
```
### `q_free`
原本是用一個遞迴函數來處理,但因為不容易維護且容易造成 segmentation fault,最後改用 `list_for_each_safe` 取得所有節點,並在其中使用 `list_entry`, `contain_of` 取得其被包含的結構體,並用 `list_del` 刪除這個節點後再將 structure 資料釋放。
原先在測試時一直遇到有記憶體沒有被釋放的問題,最後發現是忘記把 `element_t` 中的字串釋放掉。
同時接下來預期會在許多地方需要釋放 `element_t` 的 object,因此撰寫巨集 `free_element` 來處理。
```c
/* Free element_t object */
#define free_element(obj) \
free(obj->value); \
free(obj)
/* Free all storage used by queue */
void q_free(struct list_head *l)
{
if (!l || q_empty ( l )) {
free ( l );
return;
}
struct list_head *tmp, *next;
list_for_each_safe (tmp, next, l) {
element_t *entry = list_entry(tmp, element_t, list);
free_element(entry);
}
free(l);
}
```
### `q_insert_head`, `q_insert_tail`
兩個函數作用比較類似,也只有差一點點,所以放在同一個標題下。
也因為部分函數內容是重複的,所以額外寫了一個 `element_new` 函數用來建立一個新的 `element_t` object。
```c
/* Create a new element */
element_t *element_new(char *s)
{
element_t *res;
res = (element_t *) malloc(sizeof(element_t));
if (res == NULL) {
free(res);
return NULL;
}
res->value = strdup(s);
return res;
}
```
本來 `strdup` 是先 `malloc` 位置之後再用 `strcpy` 過去,不過這樣寫更為簡潔,於是換成現在的寫法。
而在 `q_insert_head`/`q_insert_tail` 中便是建立完 element 後將他們加入鏈結串列中。
```c
/* Insert an element at head of queue */
bool q_insert_head(struct list_head *head, char *s)
{
element_t *element = element_new(s);
if (!element) {
free(element);
return false;
}
list_add(&element->list, head);
return true;
}
/* Insert an element at tail of queue */
bool q_insert_tail(struct list_head *head, char *s)
{
element_t *element = element_new(s);
if (!element) {
free(element);
return false;
}
list_add_tail(&element->list, head);
return true;
}
```
### `q_remove_head`, `q_remove_tail`
同樣也是為了方便接下來需要移除節點的時候不用重複寫一次,所以這邊將移除節點寫成一個函數 `remove_element`。
```c
/* Delete element */
element_t *remove_element(struct list_head *head,
char *sp,
size_t bufsize,
struct list_head *target)
{
element_t *res = list_entry(target, element_t, list);
if (sp && res->value) {
strncpy ( sp, res -> value, bufsize );
sp[bufsize - 1] = 0;
}
list_del ( target );
return res;
}
```
然後在 `q_remove_head`/`q_remove_tail` 分別呼叫對應的位置。
```c
/* Remove an element from head of queue */
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
return remove_element(head, sp, bufsize, head->next);
}
/* Remove an element from tail of queue */
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
return remove_element(head, sp, bufsize, head->prev);
}
```
### `q_delete_mid`
藉由兩個前進速度不同的指標,找到中間的節點。
等到前進速度較快的指標到末端的時候,另一個指標會停在中間。
```c
while (fast != head && fast->next != head) {
fast = fast->next->next;
target = target->next;
}
list_del(target);
element_t *entry = list_entry(target, element_t, list);
free(entry->value);
free(entry);
```
### `q_delete_dup`
本來是用 while-loop 去寫,如果檢查到相同的就刪除,然後下一個節點過去。
不過因為直接使用 `list_del`,會造成原本的 `cur->nxet` 跑掉,因此後來改成用 `list_for_each_safe` 檢查整個鏈結串列,並檢查如果目前的節點資料與下一個節點資料相同,便將目前節點刪除。
後來在測試的時候才發誤解題意了,是要將全部相同的元素都拔掉。
```c
bool last = false;
list_for_each_safe (cur, safe, head) {
element_t *entry = list_entry(cur, element_t, list);
if (last || (cur->next != head &&
!strcmp(entry->value,
list_entry(cur->next, element_t, list)->value))) {
last = true;
list_del(cur);
free_element(entry);
} else
last = false;
}
```
寫完過了幾天的測試中,才發現之前的測試不夠完全,沒有找到 bug。
這個 bug 會發生在如果重複的元素不是排在最後面的時候(例如:["abc", "abc", "xyz"]),會將第一組出現重複的元素後面的所有元素都刪掉,因為只要在上面的程式碼第 15 行的條件一但達成後(也就是兩個字串完全相同時),`last` 就再也不可能被改為 false 了,無論後面的比較結果如何這一條條件永遠會達成。
於是把 function 改成以下的版本,在 for-loop 的最後會將 last 的值與 match 結果同步。
```diff
list_for_each_safe (cur, safe, head) {
element_t *entry = list_entry(cur, element_t, list);
- if (last || (cur->next != head &&
- !strcmp(entry->value,
- list_entry(cur->next, element_t, list)->value))) {
+ bool match = cur->next != head &&
+ !strcmp(entry->value,
+ list_entry(cur->next, element_t, list)->value);
+ if (last || match) {
list_del(cur);
free_element(entry);
- } else
- last = false;
+ }
+ last = match;
}
```
### `q_swap`
本來是要將節點倆倆取出,並將他們依序加回鏈結串列的前端,但在很後面的時候發現會有錯誤,所以改成直接呼際 $k = 2$ 的 `q_reverseK`。
```c
/* Swap every two adjacent nodes */
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
if (!head || list_empty(head) || q_size(head) < 2)
return;
// the previous version had a bug, using q_reverseK instead
q_reverseK(head, 2);
}
```
### `q_reverse`
:::danger
避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景。
:::
用 `list_for_each_safe` <s>遍歷</s> 所有 linked list 中的節點,並將所有節點依序放到開頭。
```c
/* Reverse elements in queue */
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *cur, *safe;
list_for_each_safe (cur, safe, head)
list_move(cur, head);
}
```
### `q_reverseK`
用 `list_cut_position` 從 linked lsit 中取出前 k 個節點,藉由重複利用 `q_reverse` 反轉區間之後將這個區間接回去,並取出接下來 k 個節點重複此操作。
```c
do {
// check the rest of list have k nodes or more
bool flag = true;
tmp = cur->next;
for (int i = 0; i < k; i++) {
if (!tmp || tmp == head) {
flag = false;
break;
}
tmp = tmp->next;
}
// if the rest of list don't have enough nodes
if (!flag)
break;
// reverse the segment and splice it back to right position
LIST_HEAD(slice);
list_cut_position(&slice, cur, tmp->prev);
q_reverse(&slice);
list_splice_init(&slice, cur);
// move forward k nodes
for (int i = 0; i < k; i++)
cur = cur->next;
} while (cur != head);
```
### `q_sort`
手寫 merge sort,原先是要按照以前習慣的作法,將鏈結串列分成兩邊後分別排序、用一個 tmp array 暫存資料後再複製回原始位置。
當時這樣寫是想要降低複雜度,在合併兩個串列時不用花單次 $O(N)$ 的新增元素時間,每次操作均為 $O(1)$,並且在完成合併後再花 $O(N)$ 的時間複製,會較快一些。
不過既然都用鏈結串列了,單次操作本來就需要 $O(N)$,因此改為插入元素的方式。
會將原先的 `head` 切出前半放在 `left` 中,剩下留在 `head` 中。
而原先是用 `list_for_each_safe` <s>遍歷</s> `head`,並確認是否能把 `left` 最前端的元素加入這個位置,不過這樣會讓 `list_for_each_safe` 的指標順序產生混亂造成 segmentation fault,後來改成<s>遍歷</s> `left`,並用另一個指標控制目前在 `head` 中的位置,並檢查是否能將目前的元素放進這個位置中。
```c
LIST_HEAD(left);
struct list_head *mid = head->next, *fast = head->next, *safe, *cur, *tmp;
while (fast != head &&
fast->next != head) { // get the mid position by using the same way
// in q_delete_mid
fast = fast->next->next;
mid = mid->next;
}
// split the list into two seq
list_cut_position(&left, head, mid->prev);
// sort each side
q_sort(&left, descend);
q_sort(head, descend);
tmp = head->next;
list_for_each_safe (cur, safe, &left) { // merge lists
while (tmp != head) { // find a place for current element_t object
char *cur_value = list_entry(cur, element_t, list)->value;
char *tmp_value = list_entry(tmp, element_t, list)->value;
int res = strcmp(cur_value, tmp_value);
if ((!descend && res <= 0) || (descend && res >= 0)) {
// the element is allowed be placed in this position
list_del(cur);
list_add(cur, tmp->prev);
break; // current element is placed, break the loop
}
// if not, keep moving
tmp = tmp->next;
}
if (tmp == head) {
list_del(cur);
list_add_tail(cur, head);
}
}
```
:::danger
你如何確保目前的測試已涵蓋排序演算法最差的狀況呢?
:::
### `q_ascend`/`q_descend`
為了從串列的尾端操作,所以先將鏈結串列先倒置,等處完後再將順序倒回來。
在 `q_ascend` 中,從尾端開始比較每個節點的值,如果比目前節點大則會將節點刪除,反之將目前的值紀錄下來。
而在 `q_descend` 中則是改成紀錄最大值,並且刪除較小的節點。
另外在過程中發現有多個區塊的記憶體沒有被釋放,後來發現是在更新紀錄的值之前沒有先原先的記憶體位置釋放掉,多 `free` 一次便沒有這個問題了。
```c
/* Remove every node which has a node with a strictly less value anywhere to
* the right side of it */
int q_ascend(struct list_head *head)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
if (!head || list_empty(head))
return 0;
struct list_head *cur, *safe;
char *mi = NULL;
q_reverse(head); // the process will begin from the end, reverse first
list_for_each_safe (cur, safe, head) {
element_t *cur_entry = list_entry(cur, element_t, list);
if (cur != head->next && strcmp(cur_entry->value, mi) > 0) {
// skip at first element
list_del(cur);
free_element(cur_entry);
} else {
free(mi); // importent! remind free the string before next strdup
mi = strdup(cur_entry->value);
}
cur_entry = NULL;
}
q_reverse(head); // reverse back to original order
free(mi);
return q_size(head);
}
/* Remove every node which has a node with a strictly greater value anywhere to
* the right side of it */
int q_descend(struct list_head *head)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
// same as q_ascend, but in different order
if (!head || list_empty(head))
return 0;
struct list_head *cur, *safe;
char *ma = NULL;
q_reverse(head);
list_for_each_safe (cur, safe, head) {
element_t *cur_entry = list_entry(cur, element_t, list);
if (cur != head->next && strcmp(cur_entry->value, ma) < 0) {
list_del(cur);
free_element(cur_entry);
} else {
free(ma);
ma = strdup(cur_entry->value);
}
cur_entry = NULL;
}
q_reverse(head);
free(ma);
return q_size(head);
}
```
## 論文 〈Dude, is my code constant time?〉
上面的程式碼完成後,push 上 Github,大概過了兩分鐘後收到了一封 email。
![Screenshot 2024-02-28 at 15.54.14](https://hackmd.io/_uploads/B1ZaUvn3T.png)
奇怪,local host 測都過了,打開 GitHub Actions 之後發現。
![Screenshot 2024-02-28 at 16.02.11](https://hackmd.io/_uploads/Hyc9uP33a.png)
於是跑回去看作業敘述。
> 研讀論文〈Dude, is my code constant time?〉,解釋本程式的 "simulation" 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 Student's t-distribution 及程式實作的原理。
> * 注意:現有實作存在若干致命缺陷,請討論並提出解決方案
> * 在 oreparaz/dudect 的程式碼具備 percentile 的處理,但在 lab0-c 則無。
應該是跟這方面有關,於是去研究了一下論文。
論文中提到有些程式碼看起來是 constant time,但實際上並不是,會造成一些資安上的漏洞。
總之,在大略的研究之後,發現在 [orparaz/dudect](https://github.com/oreparaz/dudect) 的原始碼中,有以下的 code(429 行處):
```c
bool first_time = ctx->percentiles[DUDECT_NUMBER_PERCENTILES - 1] == 0;
dudect_state_t ret = DUDECT_NO_LEAKAGE_EVIDENCE_YET;
if (first_time) {
// throw away the first batch of measurements.
// this helps warming things up.
prepare_percentiles(ctx);
} else {
update_statistics(ctx);
ret = report(ctx);
}
```
尋找了一下 `prepare_percentiles` 的定義後,有以下結果:
```c
/*
set different thresholds for cropping measurements.
the exponential tendency is meant to approximately match
the measurements distribution, but there's not more science
than that.
*/
static void prepare_percentiles(dudect_ctx_t *ctx) {
qsort(ctx->exec_times, ctx->config->number_measurements, sizeof(int64_t), (int (*)(const void *, const void *))cmp);
for (size_t i = 0; i < DUDECT_NUMBER_PERCENTILES; i++) {
ctx->percentiles[i] = percentile(
ctx->exec_times, 1 - (pow(0.5, 10 * (double)(i + 1) / DUDECT_NUMBER_PERCENTILES)),
ctx->config->number_measurements);
}
}
```
大意是將極值切除,只留下 $1-0.5^{10 \cdot \frac{1 + i}{N}}$ 以下的部分。
接著回去翻閱程式碼,從 `qtest.c` 中找到是 `is_insert_tail_const` 和 `is_insert_head_const` 這部分在判斷,於是從上面 include 的幾個 local header 逐個尋找,在第一個 header `deduct/fixture.c` 中就找到了,定義在最下面:
```c
#define DUT_FUNC_IMPL(op) \
bool is_##op##_const(void) \
{ \
return test_const(#op, DUT(op)); \
}
```
> 很優雅的 define,筆記一下原來 define 可以這樣寫。
:::warning
即是 [X macros](https://en.wikipedia.org/wiki/X_macro) 技巧。
:::
繼續尋找 `test_const` 的定義,再往上跳到 `doit` 函數,這應該就是對應論文中的 `dudect_main`,於是我將其中的 `prepare_statistics` 實作進來。
```c
static int cmp(const int64_t *a, const int64_t *b)
{
return (int) (*a - *b);
}
static int64_t percentile(int64_t *arr, double which, size_t sz)
{
size_t res = (size_t) sz * which;
assert(res < sz);
return res;
}
static void pre_percentiles(int64_t *exec_time)
{
qsort(exec_time, N_MEASURES, sizeof(int64_t),
(int (*)(const void *, const void *)) cmp);
for (size_t i = 0; i < N_MEASURES; i++) {
exec_time[i] = percentile(
exec_time, 1 - (pow(0.5, 10) * (double) (i + 1) / N_MEASURES),
N_MEASURES);
}
}
```
同時按照論文中的寫法,將 `update_statistics` 中的 for 迴圈改從 10 開始:
```diff
static void update_statistics(const int64_t *exec_times, uint8_t *classes)
{
- for (size_t i = 0; i < N_MEASURES; i++) {
+ for (size_t i = 10; i < N_MEASURES; i++) {
```
最後將這些改動 push 上 GitHub 重新跑一次 CI/CD,最後得到以下結果。
![Screenshot 2024-02-28 at 16.26.49](https://hackmd.io/_uploads/HyCDAw2nT.png)
太好了是卡比!
:::warning
對照論文,確認你的修改反映出其論述,之後可提交 pull request。
:::
## `q_shuffle`
使用 Fisher–Yates shuffle 來實作,但順序反過來從前端開始處理、並和往後的做交換,從前端實作會好寫一些,反正最終都是要將串列打亂,從頭或從尾開始實作並不影響結果。
會逐個處理,並將目前<s>物件</s> ?? 和後面的做交換,`tms` 為後面幾個元素,如果 `tms` 為1 代表和下一個元素交換、為 2 代表和下兩個元素交換依此類推(`tms` 為 0 時不交換)。
```c
/* Shuffle the list by using Fisher-Yates shuffle Algorithm */
void q_shuffle(struct list_head *head)
{
if (!head || list_empty(head))
return;
int len = q_size(head);
if (len == 1) // don't need to process
return;
struct list_head *cur, *safe, *target;
list_for_each_safe (cur, safe, head) {
len--;
if (len == 0) // last element
break;
target = cur;
int tms = rand() % len;
if (tms == 0) // don't need to swap
continue;
for (int i = 0; i < tms; i++)
target = target->next;
swap(target, cur);
}
puts("end of q_shuffle");
}
```
:::warning
補上數學分析。
:::