owned this note
owned this note
Published
Linked with GitHub
# 2023q1 Homework1 (lab0)
contributed by < [`yanjiew1`](https://github.com/yanjiew1) >
> [GitHub](https://github.com/yanjiew1/lab0-c)
## 開發與實驗環境
```bash
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz
CPU family: 6
Model: 142
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 10
CPU max MHz: 3400.0000
CPU min MHz: 400.0000
BogoMIPS: 3600.00
```
## 進度記錄
以下複製自「作業要求」
- [x] 在 GitHub 上 fork [lab0-c](https://github.com/sysprog21/lab0-c)
- [x] 依據上述指示著手修改 `queue.[ch]` 和連帶的檔案,測試後用 Git 管理各項修改,要能滿足 `$ make test` 自動評分系統的所有項目。
- [x] 研讀 Linux 核心原始程式碼的 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 並嘗試引入到 `lab0-c` 專案,比較你自己實作的 merge sort 和 Linux 核心程式碼之間效能落差
* [x] 研讀 Linux 核心 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 原始程式碼的並引入自己的專案
* [x] 比較效能落差
- [ ] 確保 `qtest` 提供的 `web` 命令得以搭配上述佇列實作使用,目前一旦網頁伺服器啟動後,原本處理命令列操作的 `linenoise` 套件就無法使用,請提出改善機制
- [x] 在 `qtest` 提供新的命令 `shuffle`,允許藉由 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作,需要以統計的原理來分析你的實作,探究洗牌的「亂度」
- [x] 在 `qtest` 中執行 `option entropy 1` 並搭配 `ih RAND 10` 一類的命令,觀察亂數字串的分布,並運用「資訊與熵互補,資訊就是負熵」的觀念,設計一組新的命令或選項,得以在 `qtest` 切換不同的 PRNG 的實作 (可選定 [Xorshift](https://en.wikipedia.org/wiki/Xorshift)),搭配統計分析工具,比較亂數產生器 (如 Xorshift vs. 內建) 的品質
- [ ] 研讀論文〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉,解釋本程式的 "simulation" 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 [Student's t-distribution](https://en.wikipedia.org/wiki/Student%27s_t-distribution) 及程式實作的原理。
* [x] 注意:現有實作存在若干致命缺陷,請討論並提出解決方案
* [ ] 解釋 [Student's t-distribution](https://en.wikipedia.org/wiki/Student%27s_t-distribution)
* 在 [oreparaz/dudect](https://github.com/oreparaz/dudect) 的程式碼具備 percentile 的處理,但在 [lab0-c](https://github.com/sysprog21/lab0-c) 則無。
- [ ] 指出現有程式的缺陷或者可改進之處 (例如更全面地使用 Linux 核心風格的鏈結串列 API),嘗試實作並提交 pull request
- [ ] 除了修改程式,也要編輯「[作業區](https://hackmd.io/@sysprog/linux2023-homework1)」,增添開發紀錄和 GitHub 連結,除了提及你如何逐步達到自動評分程式的要求外,共筆也要涵蓋以下:
* [ ] 詳閱第 1 週所有教材及作業描述 [(一)](/@sysprog/linux2023-lab0-a), [(二)](/@sysprog/linux2023-lab0-b), [(三)](/@sysprog/linux2023-lab0-c), [(四)](/@sysprog/linux2023-lab0-d), [(五)](/@sysprog/linux2023-lab0-e),記錄你的啟發和疑惑
* [x] 開啟 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer),修正 `qtest` 執行過程中的錯誤
* [ ] 運用 Valgrind 排除 `qtest` 實作的記憶體錯誤,並透過 Massif 視覺化 "simulation" 過程中的記憶體使用量,需要設計對應的實驗
* [ ] 解釋 [select](http://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫在本程式的使用方式,並分析 [console.c](https://github.com/sysprog21/lab0-c/blob/master/console.c) 的實作,說明其中運用 CS:APP [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 的原理和考量點。可對照參考 [CS:APP 第 10 章重點提示](https://hackmd.io/@sysprog/H1TtmVTTz)
## 改進 `queue.c`
### `q_new`
透過 `malloc` 來配置記憶體,使用 `list.h` 裡面提供的 `INIT_LIST_HEAD` 來初始化 `struct list_head` 。
> commit [0f09bf6](https://github.com/yanjiew1/lab0-c/commit/0f09bf6ab87795c13f8aae2e6f773e48ce41eb0a)
```c
/* Create an empty queue */
struct list_head *q_new()
{
struct list_head *new = malloc(sizeof(struct list_head));
if (!new)
return NULL;
INIT_LIST_HEAD(new);
return new;
}
```
### `q_free`
使用 `list.h` 內提供的 `list_for_each_entry_safe` 來走訪每一個節點。使用的過程中,`safe` 會存放下一個節點,而 `it` 節點可以從 linked list 移除,不影響 `list_for_each_entry_safe` 運作。故在這裡,可以直接在走訪的過程中釋放每一個節點。最後再呼叫 `free` 釋放鏈結串列所配置的記憶體空間。。
```c
/* Free all storage used by queue */
void q_free(struct list_head *l)
{
if (!l)
return;
element_t *safe, *it;
list_for_each_entry_safe (it, safe, l, list)
q_release_element(it);
free(l);
}
```
#### 開發過程記錄
一開始程式碼如下:
```c
/* Free all storage used by queue */
void q_free(struct list_head *l)
{
struct list_head *safe;
struct list_head *it;
list_for_each_safe (it, safe, l) {
list_del(it);
free(it);
}
free(l);
}
```
隨後很快發現,應該要釋放 `element_t` 才對,因為 `struct list_head` 是包含在 `element_t` 裡面的一個成員。於是變更如下:
```diff
@@ -5,8 +5,10 @@ void q_free(struct list_head *l)
struct list_head *it;
list_for_each_safe (it, safe, l) {
+ element_t *elem = list_entry(it, element_t, list);
list_del(it);
- free(it);
+ free(elem->value);
+ free(elem);
}
free(l);
```
但因為釋放 `element_t` 很常被用到,所以後來我寫一個獨立函式 `q_free_elem` 來做。最後又發現 `queue.h` 有提供 `q_release_element` ,最後改成用 `q_release_element` ,修改如下:
```diff
@@ -1,14 +1,12 @@
/* Free all storage used by queue */
void q_free(struct list_head *l)
{
- struct list_head *safe;
- struct list_head *it;
+ element_t *safe;
+ element_t *it;
- list_for_each_safe (it, safe, l) {
- element_t *elem = list_entry(it, element_t, list);
- list_del(it);
- free(elem->value);
- free(elem);
+ list_for_each_entry_safe (it, safe, l, list) {
+ list_del(&it->list);
+ q_release_element(it);
}
free(l);
```
後來又仔細研究 `list_for_each_entry_safe` 巨集後,發現 `list_del` 可以不用做。此巨集的特性是我們可以把 `it` 直接釋放,只要不要動到 `safe` ,就可以安全操作。最後變更如下:
```diff
@@ -1,13 +1,12 @@
/* Free all storage used by queue */
void q_free(struct list_head *l)
{
- element_t *safe;
- element_t *it;
+ if (!l)
+ return;
- list_for_each_entry_safe (it, safe, l, list) {
- list_del(&it->list);
+ element_t *safe, *it;
+ list_for_each_entry_safe (it, safe, l, list)
q_release_element(it);
- }
free(l);
}
```
### `q_new_elem`
配合 `q_insert_head` 和 `q_insert_tail` 要新增元素,新增此函式,用來建立 `element_t` 。
使用 `malloc` 來配置記憶體空間。使用 `strdup` 來配置字串空間並拷貝字串。過程中如果配置失敗,則回傳 `NULL` 。
> commit [1e56bd7](https://github.com/yanjiew1/lab0-c/commit/1e56bd7b5c02c5dcc2710b7ba90f3ccd294814c9)
```c
/* Create a new element with the provided string */
static inline element_t *q_new_elem(char *s)
{
element_t *elem = malloc(sizeof(element_t));
if (!elem)
return NULL;
char *tmp = strdup(s);
if (!tmp) {
free(elem);
return NULL;
}
elem->value = tmp;
return elem;
}
```
### `q_insert_head` 和 `q_insert_tail`
一開始要檢查 `head` 是否為 `NULL` ,若為 `NULL` 則不進行任何操作。
使用前面建立的 `q_new_elem` 來建立 `element_t` 。並且透過 `list_add` 或 `list_add_tail` 來把節點串上去。 若過程中失敗則回傳 `false` 。
```c
/* Insert an element at head of queue */
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *elem = q_new_elem(s);
if (!elem)
return false;
list_add(&elem->list, head);
return true;
}
/* Insert an element at tail of queue */
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *elem = q_new_elem(s);
if (!elem)
return false;
list_add_tail(&elem->list, head);
return true;
}
```
### `q_copy_string`
在 `q_remove_head` 和 `q_remove_tail` 需要複製字串,且 buffer 的大小是有限制的。C 標準函式庫提供的 `strcpy` 無法滿足此需求,故在此實作另一個函式 `q_copy_string` 來完成字串拷貝。
```c
static inline void q_copy_string(char *dest, size_t size, const char *src)
{
size_t i;
for (i = 0; i < size - 1 && src[i] != '\0'; i++)
dest[i] = src[i];
dest[i] = '\0';
}
```
:::warning
此函式是否必要?其通用的程度如何?
:notes: jserv
> 謝謝老師,已改用 `strncpy` 來處理
> Commit [5b8c553](https://github.com/yanjiew1/lab0-c/commit/5b8c5537b832e02c045c65deba88578993999dad)
:::
### `q_remove_head` 和 `q_remove_tail`
這裡實作從佇列中移走開頭或尾端。
利用 `list_first_entry` 和 `list_last_entry` 來取得要移除的元素。使用 `list_del` 來移除元素。題目要求要把移除元素的字串值拷貝到 `sp` 這個 buffer ,並且 buffer 大小為 `bufsize` ,就透過前面實作的 `q_copy_string` 來做。
```c
/* Remove an element from head of queue */
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *elem = list_first_entry(head, element_t, list);
list_del(&elem->list);
if (sp && bufsize)
q_copy_string(sp, bufsize, elem->value);
return elem;
}
```
<s>`q_remove_tail` 程式碼相似,就不佔用版面了。</s>
:::warning
正因程式碼相似度高,你更該思索如何簡化程式碼,從而降低維護的程式碼。
:notes: jserv
> 已讓 `q_remove_tail` 和 `q_insert_tail` 重用 `q_remove_head` 和 `q_insert_head` 程式碼。謝謝老師
> Commit [bc97533](https://github.com/yanjiew1/lab0-c/commit/bc97533e3e2726eca6cf822d3e7550ca38792fca)
:::
### `q_size`
實作很簡單:直接使用 `list.h` 提供的 `list_for_each` 來走訪每一個節點。每走訪一個節點就遞增 `count` 變數,最後再回傳 `count` 變數的值即為所求。
```c
/* Return number of elements in queue */
int q_size(struct list_head *head)
{
if (!head)
return 0;
int count = 0;
struct list_head *it;
list_for_each (it, head)
count++;
return count;
}
```
### `q_delete_mid`
這裡充份利用雙向鏈結串列的特性,從頭尾開始走訪節點,直到二個指標碰面時,即取得中間的節點。最後再把中間節點所代表的元素刪除。
```c
/* Delete the middle node in queue */
bool q_delete_mid(struct list_head *head)
{
// https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
if (!head || list_empty(head))
return false;
struct list_head *left = head->next;
struct list_head *right = head->prev;
while (left != right && left->next != right) {
left = left->next;
right = right->prev;
}
list_del(right);
element_t *elem = list_entry(right, element_t, list);
q_release_element(elem);
return true;
}
```
### `q_delete_dup`
我的作法是先宣告 `cut` 變數,其內容固定存放「目前走訪元素往前看,第一個其元素值與目前元素不同的元素」。
此外另外宣告一個 ~~`trash`~~ `pending` 作為暫時放置待移除元素的地方,要移除的元素都會先放在這裡,最後再一起清掉。
:::warning
"trash" 命名不精準,可用 pending list 一類的詞彙。
:notes: jserv
> 已修改。 Commit [a725fcc](https://github.com/yanjiew1/lab0-c/commit/a725fcc2c8b58c9e8d661ff15c978e79552d4fda)
:::
在走訪元素的過程中, ~~`&safe->list != head && strcmp(safe->value, it->value) == 0`~~ `&safe->list != head && !strcmp(safe->value, it->value)` 會去判斷下一個元素跟目前元素的值是否相同。若相同時,就繼續走訪,直到遇到下個元素 `safe` 與目前元素 `it` 值不同時,再來進行接下來操作,如下:
當下個元素 `safe` 與目前元素 `it` 不同時,會先去檢查 `cut` 變數,是不是指向前一個元素。若是指向前一個元素,代表目前元素跟前一個元素不同,則不用處置,因為目前元素 `it` 跟前一個元素沒有重複;若 `cut` 不是指向前一個元素,則代表 `cut` 指向更之前的元素,且 $(cut, it]$ 元素其值均相同,故把 $(cut, it]$ 元素全部~~丟到 `trash`~~ 放到 `pending` 中。
迴圈最後 `cut = safe->list.prev;` 這敘述,因為目前元素 `it` 與下一個元素值 `safe` 不同 (若相同就不會執行到此,故更新 `cut` 為目前元素後,再進行下個迴圈。
迴圈結束 `q_delete_dup` 要返回前。要清除 ~~`trash`~~ `pending` 裡的~~垃圾~~元素。用 `list_for_each_entry_safe` 來走訪每一個 ~~`trash`~~ `pending` 中的元素,並用 `q_release_element` 來刪除它。
```c
/* Delete all nodes that have duplicate string */
bool q_delete_dup(struct list_head *head)
{
// https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
if (!head)
return false;
LIST_HEAD(pending);
element_t *it, *safe;
struct list_head *cut = head;
list_for_each_entry_safe (it, safe, head, list) {
if (&safe->list != head && !strcmp(safe->value, it->value))
continue;
/* Detect duplicated elements */
if (it->list.prev != cut) {
LIST_HEAD(tmp);
list_cut_position(&tmp, cut, &it->list);
list_splice(&tmp, &pending);
}
cut = safe->list.prev;
}
/* Process pending list */
list_for_each_entry_safe (it, safe, &pending, list)
q_release_element(it);
return true;
}
```
:::warning
將 `strcmp(safe->value, it->value) == 0` 改為 `!strcmp(safe->value, it->value)` 可縮減程式碼。
:notes: jserv
> 已修改 Commit [bd9253a](https://github.com/yanjiew1/lab0-c/commit/bd9253aa92b0673b3805ae815fd0dc72332e5f59)
:::
### `q_reverse`
這裡的實作很簡單,就是用 `list_for_each_safe` 走訪每一個節點。走訪過程中,用 `list_move` 把每一個節點移到開頭,這樣子順序就反過來了。 `list_for_each_safe` 允許對目前走訪的節點移動位置。因為是往前移到開頭,故不會因為節點移動而改變走訪次序或是重複走訪。
```c
/* Reverse elements in queue */
void q_reverse(struct list_head *head)
{
if (!head)
return;
struct list_head *it, *safe;
/* Iterate the list and move each item to the head */
list_for_each_safe (it, safe, head)
list_move(it, head);
}
```
#### 變更記錄
把 `list_for_each_safe` 大括號拿掉,按照 Coding Style ,這裡不用放大括號。
```diff
diff --git a/queue.c b/queue.c
diff --git a/queue.c b/queue.c
struct list_head *it, *safe;
/* Iterate the list and move each item to the head */
- list_for_each_safe (it, safe, head) {
+ list_for_each_safe (it, safe, head)
list_move(it, head);
- }
}
```
### `q_reverseK`
使用 `count` 來記錄已走訪幾個節點,一開始把 `count` 設為 `k` ,每走訪一個節點就把 `count` 減去 1 。當走訪 `k` 個節點時 (`--count` 變成 0),就會把 `cut` 記錄的切點到目前走訪的節點(不包含 `cut` 但包含 `it`,即 `(cut, it]`)切下,放到 `tmp` 上,再重用前面實作好的 `q_reverse` ,對 `tmp` 做反轉,再用 `list_splice` 把 `tmp` 接回來。
最後設定 `safe->prev` 為新的 `cut` 。這裡不能用 `it` 是因為 `it` 已在反轉的過程中移動位置了。
```c
/* Reverse the nodes of the list k at a time */
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
if (!head || list_empty(head))
return;
struct list_head *it, *safe, *cut;
int count = k;
cut = head;
list_for_each_safe (it, safe, head) {
if (--count)
continue;
LIST_HEAD(tmp);
count = k;
list_cut_position(&tmp, cut, it);
q_reverse(&tmp);
list_splice(&tmp, cut);
cut = safe->prev;
}
}
```
:::warning
追加 `list_empty()` 的檢查。
:notes: jserv
> 已修正
> Commit [b536d66](https://github.com/yanjiew1/lab0-c/commit/b536d66e3dbdab1503169324b5247b7b541bb2f7)
:::
### `q_swap`
`swap` 就是二個二個一組進行 `reverse` ,故直接重用 `q_reverseK` 的程式 。
```c
/* Swap every two adjacent nodes */
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
q_reverseK(head, 2);
}
```
### `q_merge_two`
因為 `q_sort` 和 `q_merge` 都會需要合併二個串列。故新增一個 `q_merge_two` 用來合併二個串列。參考 `alanjian85` 同學的實作。合併的方式是每次從輸入的二個串列中,取較小的,放到一個暫存串列中 `tmp` ,之後再把串列接回 `first` 。
```c
static int q_merge_two(struct list_head *first, struct list_head *second)
{
if (!first || !second)
return 0;
int count = 0;
LIST_HEAD(tmp);
while (!list_empty(first) && !list_empty(second)) {
element_t *f = list_first_entry(first, element_t, list);
element_t *s = list_first_entry(second, element_t, list);
if (strcmp(f->value, s->value) <= 0)
list_move_tail(&f->list, &tmp);
else
list_move_tail(&s->list, &tmp);
count++;
}
count += q_size(first) + q_size(second);
list_splice(&tmp, first);
list_splice_tail_init(second, first);
return count;
}
```
### `q_sort`
採用 merge sort 遞迴演算法。一開始運用雙向鏈結串列的特性,從頭尾開始往中間找到中間節點,找到後,把 (`head`, `mid`] 留在 `head` 並把 (`mid`, `head`) 切下來加到 `second` 中。再針對 `head` 、 `second` 分別遞迴呼叫 `q_sort` ,對子串列進行排序。最後再把排序好的 `head` 和 `second` 透過 `q_merge_two` 合併。
```c
/* Sort elements of queue in ascending order */
void q_sort(struct list_head *head)
{
/* Try to use merge sort*/
if (!head || list_empty(head) || list_is_singular(head))
return;
/* Find middle point */
struct list_head *mid, *left, *right;
left = right = head;
do {
left = left->next;
right = right->prev;
} while (left != right && left->next != right);
mid = left;
/* Divide into two part */
LIST_HEAD(second);
list_cut_position(&second, mid, head->prev);
/* Conquer */
q_sort(head);
q_sort(&second);
/* Merge */
q_merge_two(head, &second);
}
```
:::warning
若要改為迭代的實作,你預計怎麼做?
:notes: jserv
> 在還沒看到 Linux kernel 排序演算法前,我會傳統資料結構教科書內 bottom-up 方式實作。看到 Linux 排序演算法後,很明顯發現它比傳統教科書方式聰明。之後我會研究 Python 內的 Timsort 來實作。把 Timsort 實作出來後,新增一個 `sort` option ,來切換排序演算法。
:::
#### 更新記錄
原本串列合併的程式直接寫在 `q_sort` 裡,後來把它移到獨立的函式中。
> Commit [09de13c](https://github.com/yanjiew1/lab0-c/commit/09de13c7234b40731030f571198ffeab7a06a999)
### `q_descend`
我的想法就是從尾到頭掃過一次。在掃的過程中,會判斷下一個節點(`prev`)是否小於~~等於~~目前節點(`cur`),若是就把下一個節點(`prev`)刪除再重複迴圈,直到下一個節點是大於目前節點時,才會把 `cur` 移動到下一個節點,並且 `cnt` (計數器) 加 1 。
目前 `lab0-c` 內建的 `list.h` 標頭檔和 Linux 核心原始程式碼 [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 存在一些差距,後者的 `list_for_each_prev` 巨集不在前者出現。若有它則可以用它來實作。
```c
/* 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/
if (!head || list_empty(head))
return 0;
/**
* Traverse from the last entry and remove the element that is
* smaller or equal to its right. Also count the number of elements.
*/
int cnt = 1;
element_t *cur = list_last_entry(head, element_t, list);
while (cur->list.prev != head) {
element_t *prev = list_last_entry(&cur->list, element_t, list);
if (strcmp(prev->value, cur->value) < 0) {
list_del(&prev->list);
q_release_element(prev);
} else {
cnt++;
cur = prev;
}
}
return cnt;
}
```
:::warning
針對呼叫 `strcmp` 的那行敘述,若搭配使用 `break` 或 `continue` 關鍵字,可進一步縮減程式碼。
:notes: jserv
:::
### `q_merge`
採用 [Strategies for Stable Merge Sorting](https://arxiv.org/pdf/1801.04641.pdf) 這篇論文中的 2-merge 演算法來實作。用此演算法,使合併二個 queue 時,其二個 queue 元素數量差距不要太大。
其運作原理是這樣:
1. pending list 為一類似 stack 的結構,意即先加入它的 queue 會在後面才取出來。
2. 每次迴圈加入 1 個 queue 進入 pending list ,直到沒有未加入 pending list 的 queue。
若 pending list 個數大於 3 個時,判斷是否合併:
- 從 pending list 依序取出 `x`, `y`, `z` 。 若 `|y| < 2|x|` 則按下步驟進行合併,若否,則繼續進行 2. 迴圈
- 若 `|x| < |z|` 則將 `x` 和 `y` 合併,否則 `y` 和 `z` 合併
- 合併後需要再重新取出 `x` 、 `y` 、 `z` ,判斷是否符合需要合併的條件
4. 把 pending list 中未合併的 queue 進行合併。
```c
/* Merge all the queues into one sorted queue, which is in ascending order */
int q_merge(struct list_head *head)
{
// https://leetcode.com/problems/merge-k-sorted-lists/
if (!head || list_empty(head))
return 0;
if (list_is_singular(head))
return list_first_entry(head, queue_contex_t, chain)->size;
/* Use 2-merge algorithm in https://arxiv.org/pdf/1801.04641.pdf */
LIST_HEAD(pending);
LIST_HEAD(empty);
int n = 0;
while (!list_empty(head)) {
list_move(head->next, &pending);
n++;
while (n > 3) {
queue_contex_t *x, *y, *z;
x = list_first_entry(&pending, queue_contex_t, chain);
y = list_first_entry(&x->chain, queue_contex_t, chain);
z = list_first_entry(&x->chain, queue_contex_t, chain);
if (y->size >= z->size << 1)
break;
if (x->size < z->size) {
y->size = q_merge_two(y->q, x->q);
list_move(&x->chain, &empty);
n--;
} else {
z->size = q_merge_two(z->q, y->q);
list_move(&y->chain, &empty);
n--;
}
}
}
/* Merge remaining list */
while (n > 1) {
queue_contex_t *x, *y;
x = list_first_entry(&pending, queue_contex_t, chain);
y = list_first_entry(&x->chain, queue_contex_t, chain);
y->size = q_merge_two(y->q, x->q);
list_move(&x->chain, &empty);
n--;
}
/* Move the last queue and empty queue to head */
list_splice(&empty, head);
list_splice(&pending, head);
return list_first_entry(head, queue_contex_t, chain)->size;
}
```
#### 更新記錄
原本是這重用前面實作的 `q_sort` 。我把所有鏈結串列接在一起,然後呼叫 `q_sort` 排序好,放回第一個串列。後來變更實作,改成直接進行合併,並且合併時,透過一些策略降低二個 queue 要合併時的長度差異。
> Commit [f6568be](https://github.com/yanjiew1/lab0-c/commit/f6568bed251b77837fa0ab2b87ff2642adef718b)
### `q_ascend`
這是後來 `lab-0` 加上的函式,跟 `q_descend` 很像。故把 `q_descend` 的程式複製一份來用,把中間的 `<` 改為 `>` 即可。
```c
int q_ascend(struct list_head *head)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
if (!head || list_empty(head))
return 0;
/**
* Traverse from the last entry and remove the element that is
* smaller or equal to its right. Also count the number of elements.
*/
int cnt = 1;
element_t *cur = list_last_entry(head, element_t, list);
while (cur->list.prev != head) {
element_t *prev = list_last_entry(&cur->list, element_t, list);
if (strcmp(prev->value, cur->value) > 0) {
list_del(&prev->list);
q_release_element(prev);
} else {
cnt++;
cur = prev;
}
}
return cnt;
}
```
---
## Valgrind 與 Address Sanitizer 記憶體檢查
### Makefile 中關於 `make valgrind` 的內容分析
```
valgrind: valgrind_existence
# Explicitly disable sanitizer(s)
$(MAKE) clean SANITIZER=0 qtest
$(eval patched_file := $(shell mktemp /tmp/qtest.XXXXXX))
cp qtest $(patched_file)
chmod u+x $(patched_file)
sed -i "s/alarm/isnan/g" $(patched_file)
scripts/driver.py -p $(patched_file) --valgrind $(TCASE)
@echo
@echo "Test with specific case by running command:"
@echo "scripts/driver.py -p $(patched_file) --valgrind -t <tid>"
```
裡面看到很神奇的部份,就是在用 Valgrind 執行時,會把 `qtest` 執行檔中,所有 `alarm` 改為 `isnan` 。我的猜測是因為 Valgrind 會使程式執行速度變慢,導致一些操作會超過時間限制。我把這行拿掉 `sed -i "s/alarm/isnan/g" $(patched_file)` ,執行 `make valgrind` ,果然就出現超時訊息。查詢 C 標準函式庫可知,[isnan(3p)](https://man7.org/linux/man-pages/man3/isnan.3p.html) 是用來檢查傳入的浮點數是否為 NaN ,會選用這個函數來取代 `alarm` ,估計是因為它跟 `alarm` 一樣函式名稱都是 5 個字元,此外 `isnan` 沒有任何 side effects ,故剛好可以拿來用。為了測試,我嘗試把 `isnan` 替換成 `asinf` 看看能不能運作。實測的結果如預期,可以正常運作。
### 使用 Valgrind
執行 `make valgrind` ,產生了很多 Memory leak 的訊息。以下為節錄的訊息:
```
+++ TESTING trace trace-01-ops:
# Test of insert_head and remove_head
==30743== 32 bytes in 1 blocks are still reachable in loss record 1 of 2
==30743== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==30743== by 0x10CBE6: do_new (qtest.c:146)
==30743== by 0x10DE12: interpret_cmda (console.c:181)
==30743== by 0x10E3C7: interpret_cmd (console.c:201)
==30743== by 0x10E7C8: cmd_select (console.c:610)
==30743== by 0x10F0B4: run_console (console.c:705)
==30743== by 0x10D204: main (qtest.c:1228)
==30743==
```
我先用 `valgrind ./qtest` 的方式測試,發現只要在結束前沒有把所有的 queue 釋放,就會產生上述訊息。後來發現在 `q_quit` 裡面,在第一行是 `return true;` 使下面釋放記憶體的程式沒辦法執行。把這行拿掉後就正常了。
```diff
--- a/qtest.c
+++ b/qtest.c
@@ -1059,7 +1059,6 @@ static void q_init()
static bool q_quit(int argc, char *argv[])
{
- return true;
report(3, "Freeing queue");
if (current && current->size > BIG_LIST_SIZE)
set_cautious_mode(false);
```
但若是使用 `valgrind ./qtest < traces/trace-01-ops.cmd` 的方式來執行,仍然會出現下面訊息:
```
==33817== 130 bytes in 10 blocks are still reachable in loss record 1 of 3
==33817== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==33817== by 0x49FE60E: strdup (strdup.c:42)
==33817== by 0x1121B9: line_history_add (linenoise.c:1275)
==33817== by 0x113014: line_hostory_load (linenoise.c:1360)
==33817== by 0x10D332: main (qtest.c:1215)
==33817==
==33817== 130 bytes in 10 blocks are still reachable in loss record 2 of 3
==33817== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==33817== by 0x49FE60E: strdup (strdup.c:42)
==33817== by 0x1121B9: line_history_add (linenoise.c:1275)
==33817== by 0x10F10C: run_console (console.c:692)
==33817== by 0x10D2D7: main (qtest.c:1227)
==33817==
==33817== 160 bytes in 1 blocks are still reachable in loss record 3 of 3
==33817== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==33817== by 0x112205: line_history_add (linenoise.c:1263)
==33817== by 0x113014: line_hostory_load (linenoise.c:1360)
==33817== by 0x10D332: main (qtest.c:1215)
==33817==
```
從上述訊息可以看出是 linenoise 中處理 history 的部份出錯。分析了一下 linenoise.c 的程式碼,發現 `line_atexit` 只會在 `enable_raw_mode` 函式裡被註冊為 atexit function ,但是在 stdin 不為 tty 的情況下 `enable_raw_mode` 不會被呼叫到。故把註冊 `line_atexit` 的程式移到 `linenoise` function 就解決了。
```diff
--- a/linenoise.c
+++ b/linenoise.c
@@ -243,10 +243,6 @@ static int enable_raw_mode(int fd)
{
if (!isatty(STDIN_FILENO))
goto fatal;
- if (!atexit_registered) {
- atexit(line_atexit);
- atexit_registered = true;
- }
if (tcgetattr(fd, &orig_termios) == -1)
goto fatal;
@@ -1189,6 +1185,11 @@ char *linenoise(const char *prompt)
{
char buf[LINENOISE_MAX_LINE];
+ if (!atexit_registered) {
+ atexit(line_atexit);
+ atexit_registered = true;
+ }
+
if (!isatty(STDIN_FILENO)) {
/* Not a tty: read from file / pipe. In this mode we don't want any
* limit to the line size, so we call a function to handle that. */
```
至此,所有 `make valgrind` 會產生的記憶體問題都解決了。
### 使用 address sanitizer
重新編譯有開啟 address sanitizer 的版本。
```bash
make clean
make SANITIZER=1
```
之後執行 `make test` ,沒有出現任何記憶體相關錯誤訊息。至此檢查通過。
---
## 加入 option `descend`
`lab-0` 在 Commit [0fcefb0](https://github.com/sysprog21/lab0-c/commit/0fcefb0b3da3a4a33cad00e7e68248ab909e348a) 加入了 `descend` option ,故把上游版本 `sysprog21/lab0-c` 合併進來時,也需要一併實作 option `descend` 。
### 修改 `q_merge_two`
因為 `q_sort` 和 `q_merge` 都會用到 `q_merge_two` 來合併二個串列。故在 `q_merge_two` 實作合併時,由大到小排序。其中迴圈內在選擇要哪一個元素放到合併後的串列時,做了些更動,如下:
```diff
element_t *f = list_first_entry(first, element_t, list);
element_t *s = list_first_entry(second, element_t, list);
- if (strcmp(f->value, s->value) <= 0)
+ int cmp = strcmp(f->value, s->value);
+ if (descend)
+ cmp = -cmp;
+ if (cmp <= 0)
list_move_tail(&f->list, &tmp);
else
list_move_tail(&s->list, &tmp);
```
而 `q_merge_two` 函式宣告改為:
```c
static int q_merge_two(struct list_head *first,
struct list_head *second,
bool descend)
```
針對 `descend` 為 `true` 的清況,正負號反轉,即可實現大到小排序,且也維持 `q_sort` 和 `q_merge` 的穩定排序特性。
### 修改 `q_sort` 和 `q_merge`
而 `q_sort` 和 `q_merge` 則是修改成:呼叫 `q_merge_two` 時會多傳入 `descend` 參數,來決定是否要由大到小排序。
### 修改 `q_ksort`
`q_ksort` 是使用 Linux 核心的 `list_sort` 排序演算法。`list_sort` 會傳入比較函式 `cmp` 作為參數。故我實作一個比較函式 `q_cmp_descend` ,它會呼叫原本的比較函式 `q_cmp` ,但正負號相反,如下:
```c
static int q_cmp_descend(void *priv,
const struct list_head *a,
const struct list_head *b)
{
return -q_cmp(priv, a, b);
}
```
再把 `q_ksort` 改成判斷 `descend` 是否為 `true` ,再選擇適當的比較函式。
```c
void q_ksort(struct list_head *head, bool descend)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
list_cmp_func_t cmp = descend ? q_cmp_descend : q_cmp;
list_sort(NULL, head, cmp);
}
```
---
## Linux 核心 Linked List 排序研究
### 整合 Linux 核心的 `list_sort`
將 Linux 核心原始程式碼的 `list_sort.c` 和 `list_sort.h` 複製到 `lab0-c` 專案中,進行些許修改讓它可以成功在這個專案中編譯。主要是把 `u8` 改為 `uint8_t` (並加入 `stdint.h`) 、移除不必要的 `#include` 。之後修改 `Makefile` 加入 `list_sort.o` 後,確認用 `make` 命令可以正常編譯。
接下來是額外撰寫一份用 Linux kernel 排序的程式。因為 `queue.h` 不能更動,我決定把這個程式放在另一個檔案 `ksort.c` 並且有對應的標頭檔 `ksort.h` 。 此也在 `Makefile` 內加入 `ksort.o` 。
因為 Linux kernel 實作需傳入 `cmp` 比較函式作為比較大小用。我的實作如下:
```c
static int q_cmp(void *priv,
const struct list_head *a,
const struct list_head *b)
{
// cppcheck-suppress nullPointer
element_t *elem_a = list_entry(a, element_t, list);
// cppcheck-suppress nullPointer
element_t *elem_b = list_entry(b, element_t, list);
return strcmp(elem_a->value, elem_b->value);
}
```
我新增了一個 option `ksort` ,可以用來切換排序實作。在 `qtest.c` 按照原本定義 option 的格式新增下方程式在 `console_init` 中。此外也加入一個全域變數來記錄目前要使用的排序實作。
:::warning
未來或許會有多款排序的實作,設計選項時,應予以考慮,亦即你可用 `option sort 0` (預設), `option sort 1` (上述來自 Linux 核心原始程式碼的 `list_sort`), `option sort 2` (未來實作的排序程式碼) 來切換。
:notes: jserv
:::
```c
static int use_ksort = 0;
```
```c
add_param("ksort", &use_ksort,
"Whether to use Linux kernel sorting algorithm", NULL);
```
### 效能分析
在開始分析效能前。看到 Linux 核心的 `list_sort` 是以 bottom-up 方式實作 merge sort 並且合併方式是用對 cache 較友善的方式。即 $3 \times 2^k$ 就合併,大概可以猜測 Linux 的排序實作效能會比較好。我用以下的命令序列來分析排序效能:
```
new
option ksort 1/0 <= 跟據要測試的排序實作進行切換
ih RAND 2000000
shuffle
time sort <= 執行 10 次取平均, 中間以 shuffle 間隔
```
為了避免出現 Time out 的問題。開始測試前會先 patch ./qtest,即 `sed -i "s/alarm/isnan/g"`。
測試的結果如下表:
| 我的排序 | Linux Kernel 排序 |
| -------- | ----------------- |
| 2.8899 | 2.1537 |
可以看到我的 top-down 排序演算法花的時間是 Linux 核心 `list_sort` 實作的 1.341 倍。即比 Linux 排序演算法慢 34% 左右。
### Linux Kernel Linked List 排序演算法探索
Linux 核心是使用 bottom-up 的 merge sort ,但是它合併的方式跟傳統教科書不一樣。
:::info
**TODO** 補完此節
:::
---
## `web` 命令/網頁伺服器改善
:::info
TODO
:::
---
## 亂數研究及 shuffle 實作
### 程式實作
在作業說明裡面,每次抽到的數字後,都要從最前面開始往後數,這樣子會讓時間複雜度最壞情況至 $O(n^2)$ ,故我採取不同的策略。
我先建立一個存放所有節點的指標陣列,如下所示:
```c
struct list_head **entries = malloc(sizeof(struct list_head *) * size);
```
:::warning
可變更為 `malloc(sizeof(*entries) * size)` 以縮減程式碼。
:notes: jserv
> 已修改。 Commit [92d98c6](https://github.com/yanjiew1/lab0-c/commit/92d98c6902dcc681297d2485141a51508100f6a3)
:::
把所有節點的位址放入陣列中:
```c
int i = 0;
list_for_each (node, head)
entries[i++] = node;
```
取得隨機數先直接使用 C 語言標準函式庫提供的 `rand` 函數取得隨機數。因為 `rand` 的範圍是 `[0, RAND_MAX]` , `RAND_MAX` 不一定可以被候選數個數整除。故使用一個迴圈,丟棄任何會大於 `RAND_MAX - (RAND_MAX % k)` 的數值,使得取得的隨機數最大值可被候選數個數整除。讓每一個候選數被選中的機率是一樣的。以下是相關程式碼片段。這裡要取得 $[0, i]$ 之間的數。
```c
int n;
do {
n = rand();
} while (n >= RAND_MAX - (RAND_MAX % (i + 1)));
n %= i + 1;
```
:::warning
對照看 `random.[ch]` 程式碼。
:notes: jserv
:::
:::info
這樣的實作缺點是需要使用 `malloc` 來配置額外空間。無法做到空間複雜度 $O(1)$ 。目前還沒想到能在時間複雜度為 $O(n)$ 情況下,空間複雜度為 $O(1)$ 的方式。
:::
### Fisher–Yates shuffle 亂度分析
分析方式為建立一個 queue , 並分別新增 a, b, c, d 至 queue 中。執行 shuffle 1000000 次,並統計每種排列方式出現的次數。
直接使用作業說明提供的程式來分析。把作業說明的 Python 程式複製到 `script/shuffle.py` ,並修改改用 a, b, c, d 來進行 shuffle 測試。
| 排列 | 出現次數 | 期待次數 | ${(O_i - E_i)^2 \over E_i}$ |
|------|----------|----------|-----------|
| abcd | 41451 | 41666.67 | 1.1162907 |
| abdc | 41827 | 41666.67 | 0.6169627 |
| acbd | 41722 | 41666.67 | 0.0734827 |
| acdb | 41524 | 41666.67 | 0.4884907 |
| adbc | 41707 | 41666.67 | 0.0390427 |
| adcb | 41439 | 41666.67 | 1.2439707 |
| bacd | 41937 | 41666.67 | 1.7539227 |
| badc | 41628 | 41666.67 | 0.0358827 |
| bcad | 42129 | 41666.67 | 5.1300507 |
| bcda | 41591 | 41666.67 | 0.1374107 |
| bdac | 41626 | 41666.67 | 0.0396907 |
| bdca | 41709 | 41666.67 | 0.0430107 |
| cabd | 41418 | 41666.67 | 1.4840427 |
| cadb | 41500 | 41666.67 | 0.6666667 |
| cbad | 41712 | 41666.67 | 0.0493227 |
| cbda | 41794 | 41666.67 | 0.3891307 |
| cdab | 41439 | 41666.67 | 1.2439707 |
| cdba | 42090 | 41666.67 | 4.3010667 |
| dabc | 41767 | 41666.67 | 0.2416027 |
| dacb | 41190 | 41666.67 | 5.4530667 |
| dbac | 41823 | 41666.67 | 0.5865627 |
| dbca | 41679 | 41666.67 | 0.0036507 |
| dcab | 41625 | 41666.67 | 0.0416667 |
| dcba | 41673 | 41666.67 | 0.0009627 |
| Total| | | 25.17992 |
使用[此線上工具](https://www.socscistatistics.com/pvalues/chidistribution.aspx)計算 P 值,計算出來為 **0.34** 左右,故無法拒絕 Fisher–Yates shuffle 不是 Uniform Distribution。
### Entropy 觀察
跟據作業要求,執行以下命令來觀察 entropy。
```
option entropy 1
new
it RAND 10
```
```!
l = [haiowq(29.69%) wncthutns(32.81%) yhzrx(26.56%) dfjqntag(35.94%) whgushb(29.69%) ciahh(21.88%) oidgffg(26.56%) pmeqkxou(35.94%) xffww(17.19%) nrgavix(32.81%)]
```
大致上可以發現,在相同字串長度下,不重複的字母數愈多,其 entropy 愈高。例如: `ciahh(21.88%)`, `xffww(17.19%)` ,也就是代表其亂度愈高。
從 entropy 公式可以發現,若字串中每一個字母其出現機率愈一致(即字串愈亂),且一字串所包含的不同字母數愈多 (即每個字母出現的機率一致),其 entropy 愈高,符合作業說明所說的。
不過到這裡,我還是沒有完全理解 entropy 的意義,目前的理解是:它用來表達一個訊息內容有多亂。之後還要再研讀。
### 加入 Xoshiro256++ 亂數產生器並比較系統提供之亂數產生器
`qtest` 亂數產生器存放在 `random.c` ,可以看到此亂數產生器是直接使用 Linux 核心提供的亂數產生器。
我跟據作業提供的 Wikipedia Xorshift [參考連結](https://en.wikipedia.org/wiki/Xorshift),並參考了相關的變形實作。我決定引進 Xoshiro256++ 來作為另一套亂數產生器。因為原本的 Xorshift 跟據維基百科說明,在統計上可能有些瑕疵。而其變種 Xoshiro256++ 在[作者官網](https://prng.di.unimi.it/)介紹中感覺看不出有瑕疵。
> Xorshift 是 [Linear-feedback shift register](https://en.wikipedia.org/wiki/Linear-feedback_shift_register) (LFSR) 的子集合
作者在他的網站上[提供了 Xoshiro256++ 的程式碼](https://prng.di.unimi.it/xoshiro256plusplus.c),故我就直接使用作者網站提供的程式碼,再做點修改。此外作者建議使用 SplitMix64 作為初始化 Xoshiro256++ 的內部狀態,同樣也在他的網站上[提供程式碼](https://prng.di.unimi.it/splitmix64.c),我也直接把它拿來用。
把上述程式結合,再做點修改後,Xoshiro256++ 亂數產生器程式放在 [xoshiro.c 檔案](https://github.com/yanjiew1/lab0-c/blob/master/xoshiro.c)內。
另外再建立一個 [`qrandom.c` 檔案](https://github.com/yanjiew1/lab0-c/blob/master/qrandom.c),這裡是亂數產生器的單一入口,會再透過全域變數 `qrandom_impl` 來決定要使用的亂數產生器。而 `qrandom_impl` 可用 `option random` 來控制。目前設定是 `qrandom_impl == 0` 時使用系統提供的亂數產生器, `qrandom_impl != 1` 時就用 Xoshiro256++。
此外,我在 `qtest.c` 內實作 `-r` 選項,可讓 `qtest` 直接使用內部的亂數產生器,產生隨機 raw bytes 並輸出到 stdout 。`-r 0` 為使用內建的亂數產生器,`-r 1` 為使用 xoshiro256++ ,搭配作業說明 `ent` 工具來評估亂數產生器的品質。
在 `main` 函式處理命令列選項的 switch-case 地方加入:
```c
case 'r':
char *endptr;
errno = 0;
qrandom_impl = strtol(optarg, &endptr, 10);
if (errno != 0 || endptr == optarg) {
fprintf(stderr, "Invalid random number generator\n");
exit(EXIT_FAILURE);
}
generate_randombytes();
break;
```
另外也新增一個函式 `generate_randombytes` ,來把亂數輸出到 stdout 。
```c
#define RANDOM_BYTES 1048576
static void generate_randombytes(void)
{
uint8_t *buf = malloc(RANDOM_BYTES);
if (!buf)
exit(1);
do {
qrandombytes(buf, RANDOM_BYTES);
} while (fwrite(buf, RANDOM_BYTES, 1, stdout));
exit(0);
}
```
#### `Xoshiro256++` 亂數品質測試
執行下面命令來測試亂數品質,可得到這個報告
```shell
./qtest -r 1 | head -c 10M | ent
Entropy = 7.999998 bits per byte
Optimum compression would reduce the size
of this 104857600 byte file by 0 percent.
Chi square distribution for 104857600 samples is 264.69, and randomly
would exceed this value 32.52 percent of the times.
Arithmetic mean value of data bytes is 127.4997 (127.5 = random).
Monte Carlo value for Pi is 3.142157713 (error 0.02 percent).
Serial correlation coefficient is 0.000097 (totally uncorrelated = 0.0).
```
卡方檢定的結果 p 值為 32.52% ,離 5% 遙遠,故此亂數產生器產生的亂數可能是 Uniform Distribution 的。
此外 entropy 值為 7.99998 bits per byte, 可說是相當高。
#### 系統內建亂數測試
```bash
$ ./qtest -r 0 | head -c 10M | ent
Entropy = 7.999983 bits per byte.
Optimum compression would reduce the size
of this 10485760 byte file by 0 percent.
Chi square distribution for 10485760 samples is 248.23, and randomly
would exceed this value 60.75 percent of the times.
Arithmetic mean value of data bytes is 127.4847 (127.5 = random).
Monte Carlo value for Pi is 3.141642434 (error 0.00 percent).
Serial correlation coefficient is -0.000522 (totally uncorrelated = 0.0).
```
卡方檢定的結果 p 值為 60.75% ,離 5% 遙遠,故此亂數產生器產生的亂數可能是 Uniform Distribution 的。
此外 entropy 值為 7.999983 bits per byte, 也是相當高。
故二種亂數產生器均能產生品質不錯的亂數。
原本想要用 `dieharder` 測試,但跑了很久都沒有結束,所以後來就先提供 `ent` 報告結果。
:::warning
你若對這主題感興趣,可對照閱讀 [Uniting the Linux random-number devices](https://lwn.net/Articles/884875/),亂數產生器的品質,是近期 Linux 核心的重要開發方針之一。
:notes: jserv
:::
---
## 研讀論文〈Dude, is my code constant time?〉
> [論文](https://eprint.iacr.org/2016/1123.pdf)
### `dudect` 統計原理
`dudect` 會以二種不同類型的資料作為輸入,對待測的程式進行多次時間量測,並分別觀察二種資料作為輸入時其執行時間的分布,進行比較。這二種不同類型的資料分別是:
- 固定資料
- 隨機資料
若固定輸入資料與隨機輸入資料的時間分布有顯著差異,則可以證明待測的程式其執行時間不是常數時間。反之則其執行時間可能是常數時間 (即無法拒絕其為常數時間)
執行 `dudect` 分為三步驟:
1. 量測執行時間:對二種不同類型的資料進行多次量測。論文建議不要進行一類量測再進行另外一類,避免數值受環境干擾。建議的方式是:每次隨機任一類資料,進行執行時間量測。
2. 量測後的資料處理:
- Cropping 因為執行時,作業系統或硬體中斷都可能打斷正在執行中的程式,這會讓量測不準確。此外資料會偏向較長執行時間一方。故可以透過 Cropping 手法來裁剪執行時間較長的部份。
- Higher-order preprocessing :尚未理解這部份
4. 進行統計分析:透過統計方法拒絕虛無假說「二種輸入類型執行時間分佈一致」。若成功拒絕此虛無假說,則可說明程式執行不是常數時間。論文中提到二種檢測方式,其中 `dudect` 是用 Welch’s t-test
- [Welch’s t-test](https://en.wikipedia.org/wiki/Welch%27s_t-test)
- Non-parametric tests
[這份文件](https://hackmd.io/@RinHizakura/S1PfuxJkv)有其他人寫的論文說明也可供參考
:::info
**需要再研讀** 目前還是不懂 Welch’s t-test 和 Student's t-test 的原理,還需要再研究。
:::
### 目前 `lab0-c` 內 `dudect` 缺陷
我發現 `lab0-c` 裡 `dudect` 有許多嚴重錯誤會導致偏差
- 跟據作業說明:「在 oreparaz/dudect 的程式碼具備 percentile 的處理,但在 lab0-c 則無。」經過檢查 `lab0-c` 的程式碼,發現的確沒有 percentile 相關程式。在原始的 oreparaz/dudect 會先計算 percentile 並把極端值除去。故在 `lab-0` 裡沒有正確實作這部份。
- `constants.c` 裡的 `prepare_inputs` 和 `measure` 設計錯誤。 `prepare_inputs` 裡的字串產生程式沒有辦法產生長度一致的字串,因為 randombytes 可能會讓字串中間有 `'\0'` ,此外 `prepare_inputs` 沒有跟據「固定資料」產生固定字串。
- `prepare_inputs` 雖然一開始產生隨機資料放在 `input_data` 中,但這份資料沒有在 `measure` 使用
`dudect` 是直接用 `rdtsc` 指令來計算花費週期數,但計算的結果會包含程式執行到一半可能會被中斷去執行,因為系統上不是只有待測程式在執行。
### 對 `lab-0` 中 `dudect` 的改進
改進方法如下:
- 嘗試直接把 [oreparaz/dudect](https://github.com/oreparaz/dudect) 引入 lab0-c 中,解決沒有 percentile 相關程式的問題。且 [oreparaz/dudect](https://github.com/oreparaz/dudect) 內的統計相關程式比較完整。
- 重寫準備輸入資料和待測程式碼(也就是 dudect 會呼叫量測時間的程式),改善上節提到的缺陷。
1. 我直接把 [`dudect.h`](https://github.com/oreparaz/dudect/blob/master/src/dudect.h) 引入我的程式,但把它拆成 `dudect.c` 和 `dudect.h` 。
2. 把 `dudect.c` 內的 `randombytes` 和 `randombit` 的函式移除, 因此在 `random.c` 就有提供相同的函式了。
3. `dudect` 原本是設計一個 binary 只能有一種測試,因為它會直接呼叫 `prepare_inputs` 和 `do_one_computation` 這二個由使用者實作的函式,但因為 `lab0-c` 有四個待測物,故我把 `dudect_config_t` 內加入 `prepare` 和 `compute` 二個存放 function pointer 欄位,然後把原本呼叫 `prepare_inputs` 和 `do_one_computation` 改成呼叫 `dudect_config_t` 內的 function pointer 。
```c
typedef struct __dudect_config dudect_config_t;
typedef void (*dudect_prepare_func_t)(void *priv,
dudect_config_t *conf,
uint8_t *input_data,
uint8_t *classes);
typedef uint8_t (*dudect_compute_func_t)(void *priv,
size_t size,
uint8_t *data);
struct __dudect_config {
size_t chunk_size;
size_t number_measurements;
void *priv;
dudect_prepare_func_t prepare;
dudect_compute_func_t compute;
};
```
4. 增加 `DUDECT_NOT_ENOUGHT_MEASUREMENTS` 這個狀態。並修改 `report` 在還沒收集足夠資料時,回傳 `DUDECT_NOT_ENOUGHT_MEASUREMENTS` 。而 `dudect_main` 函式中,預設也是在還沒測試時回 `DUDECT_NOT_ENOUGHT_MEASUREMENTS`
```c
typedef enum {
DUDECT_LEAKAGE_FOUND = 0,
DUDECT_NO_LEAKAGE_EVIDENCE_YET,
DUDECT_NOT_ENOUGHT_MEASUREMENTS,
} dudect_state_t;
```
5. 實作 [`measure.c`](https://github.com/yanjiew1/lab0-c/blob/master/dudect/measure.c) 和 [`measure.h`](https://github.com/yanjiew1/lab0-c/blob/master/dudect/measure.h) ,放置待測程式及 `dudect_config_t` 的相關設定。並且也實作 `is_##op_const` 巨集用來產生 `is_xxx_const` 函數供 `qtest.c` 內的程式呼叫。此外把 `CHUNK_SIZE` 提升到 640 使測試更準確。因為版面有限,就不把程式碼貼出來,可點連結觀看。
6. 為了測試看看 dudect 是不是能正常偵測時間差異,我故意把固定資料那組測資的字串長度改成 0,確實 dudect 就能發現非常數時間。確認功能正常後就回復原本設定。
完成以上步驟後,`make test` 的結果正確回報為執行時間為常數時間。
:::warning
請提交 pull request。
:notes: jserv
:::
:::info
**可再進一步實驗**
1. 嘗試用 `perf_event_open` 搭配 `config = PERF_COUNT_HW_CPU_CYCLES` 量測時間。看看能不能更能排除待測程式執行時被中斷的執行時間。
2. 嘗試對原本的 `labc-0` 中的 `dudect` 程式做修正,而不是直接引入上游最新版和重寫測試程式。
:::
---
## 貢獻與程式改進記錄
### 修正 GitHub Action 問題
我檢視我的 repository 的 GitHub Action 有無正常運作時,發現 GitHub Action 在 `Run webfactory/ssh-agent@v0.7.0` 這個步驟時就失敗了。
於是我就看了一下原始的 `sysprog21/lab0-c` repository 。裡面的 GitHub Action 可以運作成功。發現原來 `Run webfactory/ssh-agent@v0.7.0` 是用來載入 ssh private key 讓原始 `sysprog21/lab0-c` repository 在沒有 `queue.c` 的解答情況下,能從另一個 private repository 拷貝 `queue.c` 來使用,使原始的 `sysprog21/lab0-c` repository 的 GitHub 在沒解答情況下也可以測試。
但是在 fork 之後的 repository ,不會有 ssh private key 。導致在 `Run webfactory/ssh-agent@v0.7.0` 步驟就失敗,以致後面的 `make check` 、 `make test` 等步驟都不會執行。
我看了 GitHub 的文件後,發現可以把 GitHub Action 某個步驟標示為即使失敗仍然能繼續執行。故我在 `.github/workflows/main.yml` 裡進行小修正,針對 `webfactory/ssh-agent@v0.7.0` 這個步驟加入 `continue-on-error: true` 來讓它失敗時,也可以往下執行其他 GitHub Action ,使得即便沒有 ssh private key , GitHub Action 也能有作用。
我為此建了一個 [pull request](https://github.com/sysprog21/lab0-c/pull/112) 來提交我的改動,目前已經被 merge 了。
```diff
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 56c7d9e..6ed0b93 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -8,6 +8,7 @@ jobs:
steps:
- uses: actions/checkout@v3.3.0
- uses: webfactory/ssh-agent@v0.7.0
+ continue-on-error: true
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: install-dependencies
```
### 其他貢獻
- [#121 Remove `return true;` in `q_quit` function](https://github.com/sysprog21/lab0-c/pull/121)
原本的程式 `q_quit` 一開始就 `return true;` 導致後面記憶體釋放的程式沒有執行,以致於觸發 valgrind memory leak。此 pull request 改善此問題
- [#122 Register `line_atexit` in `linenoise` function](https://github.com/sysprog21/lab0-c/pull/122)
`line_atexit` 內會呼叫 `free_history` 釋放記憶體。但在 stdin 非 tty 時, `line_atexit` 不會被註冊,以致於使用檔案輸入指令時,沒辦法釋放 `linenoise` 記憶體,導致 `linenoise` 出現 memory leak 。此 pull request 修正此問題。
- [#127 `Merge do_i(h|t)` into a single function](https://github.com/sysprog21/lab0-c/pull/127)
- [#130 Fix warning message in do_descend](https://github.com/sysprog21/lab0-c/pull/130)
:::info
**TODO** 放 GitHub Pull requests
:::
---
## 其他記錄
### cppcheck 問題
要 commit 時發現在 `report.c` 中的 `static char *msg_name_text[N_MSG]` 變數宣告會觸發 cppcheck 錯誤。原本打算把此行改為 `static char * const msg_name_text[N_MSG]` ,但後來發現 GitHub 有更新新的程式碼加入 `// cppcheck-suppress constVariable` 修正此問題,因此我直接在 GitHub 網頁介面上操作 sync fork 取得更新的程式至自己 fork 出來的 repository 。
因為自己只寫了一點點程式,故直接捨棄本地端的 commit,用 GitHub 上新的程式覆寫本地的 repository 。
:::warning
可善用 `git rebase`
:notes: jserv
> 謝謝老師,我會嘗試練習 `git rebase`
:::