# 2024q1 Homework1 (lab0)
contributed by < `mesohandsome` >
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 39 bits physical, 48 bits virtual
CPU(s): 12
On-line CPU(s) list: 0-11
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 151
Model name: 12th Gen Intel(R) Core(TM) i5-12400
Stepping: 2
CPU MHz: 2500.000
CPU max MHz: 4400.0000
CPU min MHz: 800.0000
BogoMIPS: 4992.00
Virtualization: VT-x
L1d cache: 288 KiB
L1i cache: 192 KiB
L2 cache: 7.5 MiB
L3 cache: 18 MiB
NUMA node0 CPU(s): 0-11
```
## 針對指定佇列操作的實作
### `q_new`
可能會有 allocation failed。
剛開始還沒發現 `list.h` 中已經有定義好許多巨集可以使用,其中有定義 `INIT_LIST_HEAD` 可以直接使用。
```c
struct list_head *head =
(struct list_head *) malloc(sizeof(struct list_head));
if (!head)
return NULL;
INIT_LIST_HEAD(head);
```
### `q_free()`
原本使用 while 迴圈去走訪並 free 每一個節點,後續發現也有定義好的巨集可以使用。
開始要清除節點時,我並不知道 struct 內有哪些資料需要清除,看了 queue.h 發現 `element_t` 與 `list_head` 有關聯,所以也要把它清除。
```diff
list_for_each_safe (iterator, safe, l) {
element_t *entry = list_entry(iterator, element_t, list);
list_del(iterator);
- free(entry->value);
- free(entry);
+ q_release_element(entry);
}
```
### `q_insert_head()`
考量到會有 allocation failed 的狀況,需要再判斷並清除已經配置過的空間。
```diff
element_t *new_element = (element_t *) malloc(sizeof(element_t));
+ if (!new_element)
+ return false;
new_element->value = strdup(s);
+ if (!new_element->value) {
+ free(new_element->value);
+ free(new_element);
+ return false;
+ }
list_add(&new_element->list, head);
```
### `q_insert_tail()`
和 q_insert_head() 相差不多,在寫這個 function 時有發現定義好的 q_release_element 可以使用。
```diff
new_element->value = strdup(s);
if (!new_element->value) {
- free(new_element->value);
- free(new_element);
+ q_release_element(new_element);
return false;
}
```
### `q_remove_head()`
原本使用 `memcpy()` 複製字串,後來改用 `strncpy()`。
`strncpy()` 會保證字串都是以 `\0` 結尾,如果字串長度小於 `bufsize - 1`,也會自動添加 `\0`
```diff
if (sp && bufsize > 0) {
- memcpy(sp, entry->value, bufsize - 1);
+ strncpy(sp, entry->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
```
### `q_remove_tail()`
和 `q_remove_head()` 除了 `list_last_entry()` 外,其他都相同。
```diff
- element_t *entry = list_first_entry(head, element_t, list);
+ element_t *entry = list_last_entry(head, element_t, list);
```
### `q_size()`
使用 `list.h` 中的 `list_for_each` 走訪過每一個節點計算長度
```c
list_for_each (iterator, head) {
size++;
}
```
### `q_delete_mid()`
原本對 `q_delete_mid()` 的實作,需要先透過 `q_size()` 走訪一次取得長度,再找到中間點。
```c
int size = q_size(head) / 2;
struct list_head *iterator, *safe;
list_for_each_safe (iterator, safe, head) {
if (size-- == 0) {
element_t *entry = list_entry(iterator, element_t, list);
list_del(&entry->list);
q_release_element(entry);
break;
}
}
```
後來使用快慢指標<s>實做</s> 實作,不用事先取得 list 的長度就直接找到中間點並刪除。
```c
while (fast != head && fast->next != head) {
fast = fast->next->next;
slow = slow->next;
}
element_t *entry = list_entry(slow, element_t, list);
list_del(&entry->list);
q_release_element(entry);
```
### `q_delelte_dup()`
每次都判斷下一個節點的字串是否與當前相同,若相同就將當前節點刪除。
刪除到最後,如果之前自己有和其他節點重複,也必須將自己刪除,因此使用了一個 boolean 值來紀錄。
```c
if (safe != head &&
strcmp(current_entry->value, next_entry->value) == 0) {
delete_flag = true;
list_del(¤t_entry->list);
q_release_element(current_entry);
} else {
if (delete_flag) {
delete_flag = false;
list_del(¤t_entry->list);
q_release_element(current_entry);
}
}
```
### `q_swap()`
找出需要被交換的兩個節點,將他們指向的 next 和 prev 交換。
發現與這兩個節點前後的 next 和 prev 也需要更新。
```diff
while (first != head && second != head) {
first->next = second->next;
+ second->next->prev = first;
second->next = first;
second->prev = first->prev;
+ first->prev->next = second;
first->prev = second;
first = first->next;
second = first->next;
}
```
### `q_reverse()`
從頭開始將 next 和 prev 交換,再往後一個個更新。
```c
do {
temp = current->next;
current->next = current->prev;
current->prev = temp;
current = temp;
} while (current != head);
```
:::warning
TODO: 善用 List API,撰寫更精簡的程式碼。
:::
使用 `list_for_each_safe()` 將目前遇到的節點都向 head 移動,從而翻轉整個 list。
```diff
- do {
- temp = current->next;
- current->next = current->prev;
- current->prev = temp;
- current = temp;
- } while (current != head);
+ list_for_each_safe (current, safe, head)
+ list_move(current, head);
```
### `q_reverseK()`
中間與 `q_reverse()` 大致相同,多了一個變數計算目前換了幾次。
每次換完就再將當前位置的指標更新。
```diff
do {
do {
...
} while (current != head && remain > 0);
+ current->prev->prev = prev;
+ prev->next = current->prev;
+ start->next = current;
+ prev = start;
+ start = current;
} while (size > k);
```
### `q_ascend()`
從尾端開始往前檢查,將起始節點的 value 設定為目前的最小值。
向前存取每一個節點,如果值較大就刪除。
~~因為 function 敘述上面寫的是 remove,所以只把節點 unlink,並未把 element_t 清除~~
發現如果沒有 release 的話,在 make test 有一部分沒辦法通過,所以 queue.h 中的 function 敘述不是 remove 而是要改成 delete,或是要修改 test 本身?
```diff
while (ptr != head && ptr->prev != head) {
element_t *entry = list_entry(ptr->prev, element_t, list);
if (strcmp(entry->value, current_min) < 0) {
current_min = entry->value;
ptr = ptr->prev;
} else {
list_del(&entry->list);
+ q_release_element(entry);
}
}
```
:::warning
確認後,提交 GitHub issue / pull request。
:::
### `q_descend()`
與 `q_ascend()` 除了比較函式外都相同。
```diff
- if (strcmp(entry->value, current_min) <= 0)
+ if (strcmp(entry->value, current_max) >= 0)
```
:::danger
避免非必要的項目列舉 (即 `* `),以清晰明確的漢語書寫。
:::
:::warning
TODO: 提高程式碼的可重用程度。
:::
`q_ascend()` 和 `q_descend()` 只有比較函式不同,所以額外寫了一個函式並使用一個 bool 參數去控制目前要使用哪一個比較。
```c
if (descend ? strcmp(entry->value, current_value) >= 0
: strcmp(entry->value, current_value) <= 0)
```
### `q_sort()`
使用快慢指標找到中間節點,將鏈結串列拆成左右二側。
再修改 [Merge k Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists/description/) 中有使用過 merge 兩個 list 的方式合併拆出的 list。
```c
list_cut_position(&left, head, slow);
list_splice_init(head, &right);
q_sort(&left, descend);
q_sort(&right, descend);
```
### `q_merge()`
將 chain 中的所有 list 都先合併到第一個 list,再使用前面完成的 `q_sort()` 做排序。
```c
list_for_each_safe (node, safe, head) {
if (node == head->next)
continue;
queue_contex_t *current_entry = list_entry(node, queue_contex_t, chain);
size += current_entry->size;
list_splice_init(current_entry->q, entry->q);
}
q_sort(entry->q);
```
---
## 研讀論文〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉並改進 lab0-c 的 dudect
### student's t-distribution
t-distribution 和常態分佈的分佈狀況相似,圖上表示都是對稱而中間隆起的形狀。
![image](https://hackmd.io/_uploads/B1cdyNyT6.png)
> 紅線表示 t-distribution 藍線表示常態分佈。
但 t-distribution 的兩側會比常態分佈來的更厚,代表會有更多數據分佈在兩端,所以 t-distribution 適合用在小量數據的分析,因為數據不多的情況下,很有可能會出現極端值影響整體的分佈狀況。
### Welch's t-test
論文中使用這個方式檢驗,計算中會需要一個 p-value,為下圖大於 t 的機率密度值
![image](https://hackmd.io/_uploads/rJV9o6GTT.png)
不同於常態分佈需要大量資料才會有正常的機率分佈,透過 t-distribution 可以處理資料量小的情況,讓計算過程中可以取得合適的 p-value 將極端值切除,因此論文中採用 t-distribution 是合適的。
:::warning
探討用於本處是否合適。
:::
### 改進 lab0-c/dudect
dudect 會將超出一定範圍的資料切除/忽略,在 [dudect Source code](https://github.com/oreparaz/dudect/blob/master/src/dudect.h) 中的 Percentiles 在做這件事。
但目前 test 中缺少了這部份的檢查,將缺少的部份補上後即可通過 trace-17-complexity 的檢測。
```diff
bool ret = measure(before_ticks, after_ticks, input_data, mode);
+ bool first_time = percentiles[DUDECT_NUMBER_PERCENTILES - 1] == 0;
+ update_statistics(exec_times, classes);
+ if (first_time) {
+ prepare_percentiles(exec_times, percentiles);
+ } else {
+ differentiate(exec_times, before_ticks, after_ticks);
+ update_statistics(exec_times, classes);
+ ret &= report();
+ }
```
## 新增 shuffle 命令
### `qtest` 新增命令
在 `qtest.c` 的 `console_init()` 中加入新的命令。
```diff
+ ADD_COMMAND(shuffle, "Shuffle the queue", "");
```
參考原先就有的測試函式,添加一個 `do_shuffle()` 函式並將呼叫的函式修改為新增的 `q_shuffle()`。
```diff
if (exception_setup(true))
- q_reverseK(current->q, k);
+ q_shuffle(current->q);
```
### `q_shuffle()`
參考 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm) 演算法,實作 `q_shuffle()`。
```c
while (size && current != head) {
int index = rand() % (size--);
struct list_head *target = head->next;
while (index--)
target = target->next;
if (current == target)
continue;
if (current != target) {
struct list_head *temp = target->prev;
list_move(target, current);
list_move(current, temp);
current = target->prev;
} else {
current = current->prev;
}
}
```
```text
Expectation: 41666
Observation: {'1234': 41841, '1243': 41335, '1324': 41735, '1342': 41830, '1423': 41653, '1432': 41390, '2134': 41641, '2143': 41523, '2314': 41365, '2341': 41591, '2413': 41730, '2431': 41493, '3124': 42083, '3142': 41394, '3214': 41922, '3241': 41859, '3412': 41771, '3421': 41642, '4123': 41635, '4132': 41643, '4213': 41989, '4231': 41660, '4312': 41699, '4321': 41576}
chi square sum: 21.043920702731242
```
![Shuffle_result](https://hackmd.io/_uploads/SyHndFBAp.png)
## 引入 lib/list_sort.c 並比較 q_sort
### 新增 list_sort 命令
將 list_sort.c 中的 `cmp` 都先更換成之前 `q_descend` 中有使用到的 `strcmp`。
在程式中加入 `likely()` 及 `unlikely()` 兩個 macro。
```c
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
```
並用和新增 `shuffle` 命令時一樣的方式新增一個 `list_sort` 命令。
```text
help | Show summary
ih str [n] | Insert string str at head of queue n times. Generate random string(s) if str equals RAND. (default: n == 1)
it str [n] | Insert string str at tail of queue n times. Generate random string(s) if str equals RAND. (default: n == 1)
list_sort | Sort queue with lib/list_sort
log file | Copy output to file
```
### 比較 q_sort
建立 shell script 在不同的資料量時各測量 5 次,將數據取平均做觀察。
```shell
for i in {1..5}; do
echo -e "new\n it RAND ${NUMBER}\n time sort" | ./qtest | grep "Delta"
done
```
![Figure_2](https://hackmd.io/_uploads/rJ9tioqC6.png)
從結果上可以看出,`list_sort` 比起自己實做的 `q_sort` 更快,尤其是在資料量增加時,差距更加顯著。
## 研讀 lib/list_sort.c
如果是通常的 merge sort,在出現 2 個同為 $2^k$ 大小的 list 時,就會將 2 個合併,但 list_sort 中會等到同時出現大小為 `2 : 1` 的 list 才會合併,讓長度較長的 list 不會很快就產生,以此降低 [cache thrashing](https://en.wikipedia.org/wiki/Thrashing_(computer_science))
```
* This mergesort is as eager as possible while always performing at least
* 2:1 balanced merges. Given two pending sublists of size 2^k, they are
* merged to a size-2^(k+1) list as soon as we have 2^k following elements.
*
* Thus, it will avoid cache thrashing as long as 3*2^k elements can
* fit into the cache. Not quite as good as a fully-eager bottom-up
* mergesort, but it does use 0.2*n fewer comparisons, so is faster in
* the common case that everything fits into L1.
```