# 2024q1 Homework1 (lab0)
contributed by < [youjiaw](https://github.com/youjiaw/lab0-c) >
:::danger
留意各式細節,唯有重視小處並步步為營,方可挑戰原始程式碼規模超越三千七百萬行的 Linux 核心。
> 好的
:::
### Reviewed by `yu-hsiennn`
```diff
// Add remaining elements
- if (!list_empty(head1))
- list_splice_tail_init(head1, &merged);
- if (!list_empty(head2))
- list_splice_tail_init(head2, &merged);
+ (!list_empty(head1)) ? list_splice_tail_init(head1, &merged) : list_splice_tail_init(head2, &merged);
```
改寫程式碼 ( q_sort ),使其更為精簡。
:::warning
感謝提醒,已修改。
:::
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 46 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 20
On-line CPU(s) list: 0-19
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i9-10900X CPU @ 3.70GHz
CPU family: 6
Model: 85
Thread(s) per core: 2
Core(s) per socket: 10
Socket(s): 1
Stepping: 7
CPU max MHz: 4700.0000
CPU min MHz: 1200.0000
BogoMIPS: 7399.70
Virtualization features:
Virtualization: VT-x
Caches (sum of all):
L1d: 320 KiB (10 instances)
L1i: 320 KiB (10 instances)
L2: 10 MiB (10 instances)
L3: 19.3 MiB (1 instance)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-19
```
## 指定的佇列操作
<s>實做 queue</s>
:::danger
對照 Dictionary.com 對 [implement](https://www.dictionary.com/browse/implement) 一詞的解釋:
* to fulfill; perform; carry out:
* to put into effect according to or by means of a definite plan or procedure.
* Computers. to realize or instantiate (an element in a program), often under certain conditions as specified by the software involved.
* to fill out or supplement
「實作」會是符合上述 "implement" 語意的譯詞,取「起」以「創作」的意思,而「製造」本質上是固定輸入並通常產生固定輸出的行為。
$\to$ [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)
:::
### `q_new`
配置記憶體,並用 `INIT_LIST_HEAD` 初始化。
### `q_free`
一開始實作時,沒有確認 `head` 是否有成功配置到記憶體,在 `make test` 的 "Test of malloc failure on new" 會出現錯誤。
```diff
void q_free(struct list_head *head)
{
+ if (!head)
+ return;
element_t *entry, *safe;
list_for_each_entry_safe (entry, safe, head, list)
q_release_element(entry);
free(head);
}
```
### `q_insert_head`, `q_insert_tail`
原本未檢查記憶體配置情況,在 `make test` 的 "Test of malloc failure on insert_head, insert_tail" 出現錯誤。
```diff
element_t *new = malloc(sizeof(element_t));
if (!new)
return false;
new->value = strdup(s);
+ if (!new->value) {
+ free(new);
+ return false;
+ }
```
### `q_remove_head`, `q_remove_tail`
使用 API 的 `list_entry` 取得開頭的 `element` ,將值複製到 `sp` 並從佇列移除。
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
struct list_head *node = head->next;
element_t *element = list_entry(node, element_t, list);
if (sp) {
strncpy(sp, element->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(node);
return element;
}
```
`q_remove_tail` 的做法相同,但改取尾端的 `element`。
```diff
- struct list_head *node = head->next;
+ struct list_head *node = head->prev;
```
### `q_size`
使用[教材](https://hackmd.io/@sysprog/linux2024-lab0-a#%E7%89%9B%E5%88%80%E5%B0%8F%E8%A9%A6)提供的做法。
:::danger
列出程式碼的目的是討論和檢討,倘若你沒有如此的舉措,就不用列出。
> 好的,已修正。
:::
### `q_delete_mid`
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
> 好的,謝謝老師提醒。
:::
我原本的做法如下,除了沒有考慮到 `q_size` 逐一走訪每個節點的成本,使用 indirect pointer 來實作此函式的優缺點也需討論。
```c
int mid_node = q_size(head) / 2;
struct list_head **indir = &head;
for (int i = 0; i <= mid_node; i++)
indir = &((*indir)->next);
element_t *element = list_entry(*indir, element_t, list);
list_del(*indir);
q_release_element(element);
```
在參考 [chun61205](https://hackmd.io/H2fpkPChSkuRw88H4bt_XQ?view#q_delete_mid) 以及 [yanjiew1](https://hackmd.io/@komark06/SyCUIEYpj#q_delete_mid-%E5%AF%A6%E4%BD%9C) 的建議後,決定使用兩個指標,從頭尾往中間找,以此減少記憶體的存取次數。
> commit [85a19ba](https://github.com/youjiaw/lab0-c/commit/85a19babc45540b0532aa275ceadf1b746fe0f2c)
```c
struct list_head *left = head->next, *right = head->prev;
while (!(left == right) && !(right->next == left)) {
left = left->next;
right = right->prev;
}
list_del(left);
q_release_element(list_entry(left, element_t, list));
```
### `q_delete_dup`
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
由於是傳入已排序的佇列,因此,只需檢查相鄰節點的值是否相同。
使用 `list_for_each_entry_safe` 逐一走訪每個節點,若 `current` 與 `next` 的值相同,就刪除 `current`,而 `flag` 的作用為刪除最後一個重複的節點。
```c
bool q_delete_dup(struct list_head *head)
{
if (!head || list_empty(head))
return false;
element_t *current, *next;
bool flag = false;
list_for_each_entry_safe (current, next, head, list) {
if (current->list.next == head)
break;
if (strcmp(current->value, next->value) == 0) {
list_del(¤t->list);
q_release_element(current);
flag = true;
} else if (flag) {
list_del(¤t->list);
q_release_element(current);
flag = false;
}
}
return true;
}
```
目前發現 `make test` 有時後會出現以下錯誤:
ERROR: Duplicate strings are in queue or distinct strings are not in queue
在經過 `./qtest` 測試不同測資後,發現如果最後一個節點有重複的話,會直接被跳過,因此加入檢查程式碼。
> commit [bb834f2](https://github.com/youjiaw/lab0-c/commit/bb834f2fb39f86fd469119c091e7ec9c925ae037)
```diff
- if (current->list.next == head)
+ if (current->list.next == head) {
+ if (flag) {
+ list_del(¤t->list);
+ q_release_element(current);
+ }
break;
+ }
```
### `q_swap`
:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。
:::
使用 `list_for_each` 逐一走訪每個節點。
使用 `list_move` 將 `next` 移到 `prev` 後方,等同於將 `current` 與 `next` 交換位置。
```c
struct list_head *current;
list_for_each (current, head) {
if (current->next == head)
break;
list_move(current->next, current->prev);
}
```
### `q_reverse`
用指標 `first` 指向開頭節點,並不斷將 `next` 移動至 `head` 後方。
```c
struct list_head *first = head->next;
while (first->next != head)
list_move(first->next, head);
```
### `q_reverseK`
參考 [koty6069](https://hackmd.io/UJQfmB61SkuVMoKR7dcYVA?view#q_reverseK) 的做法,以每 k 個節點為一組執行 `q_reverse`。
```c
struct list_head *current, *safe, *ptr = head;
int cnt = 0;
list_for_each_safe (current, safe, head) {
if (++cnt == k) {
LIST_HEAD(tmp);
list_cut_position(&tmp, ptr->next, current);
q_reverse(&tmp);
list_splice(&tmp, ptr);
ptr = safe->prev;
cnt = 0;
}
}
```
### `q_sort`
分為 `q_sort` 及 `list_merge`。
1. `q_sort`
* 使用快慢指標及 `list_cut_position` 將佇列對半切。
* 不斷遞迴,直到所有節點被分割成單一節點,再進行 `list_merge`。
```c
struct list_head *fast = head->next, *slow = head;
while (fast != head && fast->next != head) {
slow = slow->next;
fast = fast->next->next;
}
struct list_head second_half;
list_cut_position(&second_half, head, slow);
q_sort(head, descend);
q_sort(&second_half, descend);
list_merge(head, &second_half, descend);
```
2. `list_merge`
* 比較兩個佇列的開頭節點值,依照結果和排序方式將指定節點移動到 `merged` (用來存放排序過的節點)。
* 當其中一個佇列空了,就將另一個移動到 `merged` 尾端。
```c
static void list_merge(struct list_head *head1,
struct list_head *head2,
bool descend)
{
struct list_head merged;
INIT_LIST_HEAD(&merged);
while (!list_empty(head1) && !list_empty(head2)) {
int compare_result =
strcmp(list_entry(head1->next, element_t, list)->value,
list_entry(head2->next, element_t, list)->value);
if ((descend && compare_result > 0) ||
(!descend && compare_result < 0)) {
struct list_head *tmp = head1->next;
list_move_tail(tmp, &merged);
} else {
struct list_head *tmp = head2->next;
list_move_tail(tmp, &merged);
}
}
// Add remaining elements
if (!list_empty(head1))
list_splice_tail_init(head1, &merged);
if (!list_empty(head2))
list_splice_tail_init(head2, &merged);
INIT_LIST_HEAD(head1);
list_splice(&merged, head1);
}
```
:::danger
你如何確認目前的測試程式已涵蓋排序演算法的最差狀況?
:::
### `q_ascend`
由尾端向 `head` 走訪,並比較當前與前一個節點的 `value`:
1. 若 `prev` 大於 `current`,就刪除 `prev`。
```graphviz
digraph q_ascend {
rankdir=LR
node [shape=circle, fontsize=12];
3 [fillcolor="#f9a7a7", style=filled]
Head [shape=box, fontsize=12]
Head -> 5 -> 2 -> 13 -> 3 -> 8 [arrowhead=normal, arrowtail=normal ,dir=both];
current [fontsize=15, shape=plain, fontcolor=red]
current -> 8 [style=dashed, color=red];
{ rank=same; current;8 };
}
```
2. 其餘情況,則 `current` 指向 `prev`。
```graphviz
digraph q_ascend {
rankdir=LR;
node [shape=circle, fontsize=12];
Head [shape=box, fontsize=12]
Head -> 5 -> 2 -> 13 -> 8[arrowhead=normal, arrowtail=normal ,dir=both];
current [fontsize=15, shape=plain, fontcolor=red]
current -> 13 [color=red];
{rank=same; current;13};
}
```
```c
struct list_head *current = head->prev;
while (current->prev != head) {
struct list_head *prev = current->prev;
if (strcmp(list_entry(current, element_t, list)->value,
list_entry(prev, element_t, list)->value) <= 0) {
list_del_init(prev);
q_release_element(list_entry(prev, element_t, list));
} else
current = current->prev;
}
```
<!-- ![1](https://hackmd.io/_uploads/HkNcWGfap.png) -->
### `q_descend`
想法與 `q_ascend` 相同,但將條件改為:若 `prev` 小於 `current`,就刪除 `prev`。
```diff
- if (strcmp(list_entry(current, element_t, list)->value,
- list_entry(prev, element_t, list)->value) <= 0) {
+ if (strcmp(list_entry(current, element_t, list)->value,
+ list_entry(prev, element_t, list)->value) >= 0) {
list_del_init(prev);
q_release_element(list_entry(prev, element_t, list));
}
```
由下圖可發現,若由尾端向左邊走訪,只需紀錄最大值,並將小於當前最大值的節點移除即可。
### `q_merge`
目前作法是將所有佇列接起來,再執行 `q_sort`。
```c
if (list_is_singular(head))
return q_size(head);
queue_contex_t *cur = NULL,
*qhead = list_first_entry(head, queue_contex_t, chain);
list_del_init(&qhead->chain);
list_for_each_entry (cur, head, chain) {
list_splice_init(cur->q, qhead->q);
qhead->size += cur->size;
}
list_add(&qhead->chain, head);
q_sort(qhead->q, descend);
// https://leetcode.com/problems/merge-k-sorted-lists/
return qhead->size;
```
:::danger
改進你的漢語表達。
:::
## 分析記憶體問題
執行 `make valgrind` 以及開啟 `Address Sanitizer` 後,都沒有偵測到記憶體錯誤。
## 在 qtest 提供新的命令 shuffle
### `make` 時產生 eror
:::danger
directory 的翻譯是「目錄」,而非「資料夾」(folder)
> 好的,已修正。
:::
我在寫完 shuffle 函式後,做了以下幾個步驟:
1. 在 `scripts` <s>資料夾</s> 目錄中新增一個 python 檔,內容為教材提供的[測試程式](https://hackmd.io/@sysprog/linux2024-lab0-d#%E6%B8%AC%E8%A9%A6%E7%A8%8B%E5%BC%8F)。
2. 在終端機確認是否有安裝 python。
3. 執行 `make`。
接著就出現數行錯誤訊息,以下節錄其中兩行做紀錄
```shell
/usr/bin/ld: /home/webber/linux2024/lab0-c/qtest.c:884: undefined reference to `__asan_report_load8'
/usr/bin/ld: /home/webber/linux2024/lab0-c/qtest.c:887: undefined reference to `__asan_report_load8'
```
:::danger
這與 Address Sanitizer 有關,查閱 GCC 手冊。
> 好,謝謝老師提醒。
:::
就我的觀察,所有的 `.c` 檔均有產生此錯誤,內容格式為
```shell
/usr/bin/ld: [路徑名稱]: undefined reference to `xxx`
```
嘗試解決:
首先,刪除測試的 Python 檔案和 shuffle 函式,確保在 Visual Studio Code 的版本控制中,沒有任何更改過的項目(版本與成功執行 make valgrind 時一致)。但仍有相同的錯誤訊息。
在老師提醒這與 Address Sanitizer 有關後,我嘗試執行 `make clean` 及 `make SANITIZER=0` 來關閉它,隨後便可順利執行 `make test`。
### 統計方法驗證 shuffle
函式按照 Fisher–Yates shuffle 演算法來實作,以下為測試 [1, 2, 3, 4] 做 shuffle 1,000,000 次的直方圖。
![Figure_1](https://hackmd.io/_uploads/HJ_Lp2U06.png)
## 論文〈Dude, is my code constant time?〉
### 閱讀論文
Dudect 使用 leakage detection tests 在目標平台上進行統計分析,以決定該程式是否能在常數時間內完成,此方法解決了相關研究中,需要對硬體進行建模的困難。
我的疑惑是,為什麼兩種輸入資料的執行時間分佈相同時,就代表此演算法可以在常數時間內完成?
在查閱了 leakage detection 的論文後得知,在兩種輸入資料下,如果演算法的執行時間分佈在統計上沒有顯著差異,則代表演算法的性能不受輸入資料分佈的影響,即時間複雜度為常數時間。
### 解釋本程式的 "simulation" 模式
由 [trace-17-complexity.cmd](https://github.com/sysprog21/lab0-c/blob/d43e0723a034586e2200cfae0263657304ffcd02/traces/trace-17-complexity.cmd#L2) 可得知,在測試 it, ih, rh 及 rt 是否為常數時間時,會開啟 simulation 模式,讓 `qtest.c` 的 [queue_insert()](https://github.com/sysprog21/lab0-c/blob/d43e0723a034586e2200cfae0263657304ffcd02/qtest.c#L184) 與 [queue_remove()](https://github.com/sysprog21/lab0-c/blob/d43e0723a034586e2200cfae0263657304ffcd02/qtest.c#L291C13-L291C25) 函式可以使用 `dudect` 目錄中的 [test_const()](https://github.com/sysprog21/lab0-c/blob/d43e0723a034586e2200cfae0263657304ffcd02/dudect/fixture.c#L154C13-L154C23) 進行測試。
`test_const()` 會依照預先設定的測試次數,持續呼叫 `doit()` 來獲得測試結果,此函式可分為三個部分,分別對應到 `dudect` 中的 `dudect_init()`, `dudect_main()` 及`dudect_free()`。
### 處理 percentile
```c
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);
}
```
在 `dudect_main()` 裡,會捨棄部份的第一批測量結果,根據論文所述,大於閾值 p 的測量值會被捨棄,目的是要讓結果與測量值的分佈盡可能的相符,我目前還不是很理解此意思。
若不是第一次測試,會呼叫 `update_statistics()`,可以發現在 `dudect` 中的函式會捨棄前 10 次的結果,而 `lab0-c` 則是沒有捨棄。
```c
// dudect
for (size_t i = 10 /* discard the first few measurements */; i < (ctx->config->number_measurements-1); i++) {
// lab0-c
for (size_t i = 0; i < N_MEASURES; i++) {
```
因此,我們要改進的部份如下:
* 將處理 percentile 的函式加入 lab0-c。
* 更改 update_statistics() 的迴圈走訪範圍。
做完上述的改進後(commit [cb0eb09](https://github.com/youjiaw/lab0-c/commit/cb0eb09fb67c93b7465a81a499c8da252f086a48)),`make test` 的 `trace-17-complexity` 就可以順利通過了。
## 嘗試引入 lib/list_sort.c 引入到 lab0-c 專案