# 2024q1 Homework1 (lab0)
contributed by < [`yourui1017`](https://github.com/yourui1017) >
### Reviewed by `yu-hsiennn`
可以利用 `list.h` 裡面的 API 來實作,以精簡呈現出來的程式碼。
## 開發環境
``` shell
$ gcc --version
(Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.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): 12
On-line CPU(s) list: 0-11
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
CPU family: 6
Model: 158
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 10
CPU max MHz: 4100.0000
CPU min MHz: 800.0000
BogoMIPS: 4399.99
Virtualization: VT-x
Caches (sum of all):
L1d: 192 KiB (6 instances)
L1i: 192 KiB (6 instances)
L2: 1.5 MiB (6 instances)
L3: 9 MiB (1 instance)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-11
```
## 針對佇列的操作
### 重要的結構體
```c
struct list_head {
struct list_head *prev;
struct list_head *next;
};
```
```c
typedef struct {
char *value;
struct list_head list;
} element_t;
```
### q_new
```c
struct list_head *q_new()
{
struct list_head *head = malloc(sizeof(struct list_head));
if (!head)
return NULL;
INIT_LIST_HEAD(head);
return head;
}
```
:::danger
allocate 的翻譯是「配置」,以區格 dispatch (分發/分配) 的翻譯。
> 已更改
:::
首先用 malloc <s>分配</s> 配置計憶體大小為 `struct list_head` 的空間,並根據 C 語言規格書[7.20.3],如果記憶體空間不足以配置,則函式應該回傳 NULL 。
> If the space cannot be allocated, a null pointer is returned
相反的,如果記憶體空間足夠配置,則使用 `list.h` 的參考函式`INIT_LIST_HEAD` 函式初始化 head 並且回傳 head 。
### q_free
```c
void q_free(struct list_head *head) {
if(!head)
return;
element_t *entry, *safe;
list_for_each_entry_safe(entry, safe, head, list){
free(entry->value);
free(entry);
}
free(head);
}
```
先判斷 `head` 是否為 NULL ,再使用 `list.h` 的`list_for_each_entry_safe` 函式走訪每個 `entry` ,並考量到 `value` 可能會以動態記憶體宣告,因此先將 `char *` 變數型別存放的記憶體釋放,再釋放 `entry` 的記憶體。
:::danger
改善漢語表達,上述說法不精確。
:::
### q_insert_head
```diff
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *new = malloc(sizeof(element_t));
if(!new)
return false;
-
- new->value = s;
+ int str_size = sizeof(char) * (strlen(s) + 1);
+ new->value = malloc(str_size);
+ if (!new->value) {
+ free(new);
+ return false;
+ }
+ strncpy(new->value, s, str_size - 1);
list_add(&new->list, head);
return true;
}
```
原本的寫法沒有考慮到 `value` 沒有被 `malloc` 的情況,因此經過考慮後,先配置 `element_t` 的記憶體位置後再配置 `value` 的記憶體位置,並將 `s` 字串複製給予 `value` 。
:::danger
HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」
:::
在完成 `q_insert_head` 和 `q_remove_head` 兩個函式並使用 `make test` 測試,發現在 trace-01-ops 出現錯誤
```
+++ TESTING trace trace-01-ops:
# Test of insert_head and remove_head
ERROR: Removed value dolphinU��� != expected value dolphin
ERROR: Removed value bearU��� != expected value bear
ERROR: Removed value gerbilU��� != expected value gerbil
```
因此回頭檢查`q_insert_head` 和 `q_remove_head` 兩個函式,發現在`q_insert_head` 中的 `strcpyn` 的引數錯誤,引數更正如下:
```diff
- strncpy(new->value, s, str_size - 1);
+ strncpy(new->value, s, str_size);
```
### q_remove_head
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *remove = list_first_entry(head, element_t, list);
if (sp)
strncpy(sp, remove->value, bufsize - 1);
list_del(&remove->list);
return remove;
}
```
使用 `list.h` 的 `list_first_entry` 找到首個 `element_t` ,並將該 `element_t` 的 value 複製給 `sp` ,再將該 `element_t` 移走。
:::danger
改進你的漢語表達。
:::
### q_delete_mid
```diff
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head **indir = &head->next;
for (struct list_head *fast = head->next;
fast != head && fast->next != head; fast = fast->next->next)
indir = &(*indir)->next;
struct list_head *del = *indir;
list_del(del);
- free(del);
+ free(list_entry(del, element_t, list)->value);
+ free(list_entry(del, element_t, list));
return true;
}
```
參考 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-Leetcode-2095-Delete-the-Middle-Node-of-a-Linked-List) **案例探討: Leetcode 2095. Delete the Middle Node of a Linked List**使用快慢指標找到中間節點,再根據此節點找到 `element_t` 並釋放該 `element_t` 和 `value`的記憶體位置。
### q_sort
原始版本的 `mergeTwoLists` 程式碼如下:
```c
struct list_head *mergeTwoLists(struct list_head *L1, struct list_head *L2, bool descend) {
struct list_head *head = NULL, **ptr = &head, **node;
for (node = NULL; L1 && L2; *node = (*node)->next) {
if (descend)
node = (list_entry(L1, element_t, list)->value > list_entry(L2, element_t, list)->value) ? &L1: &L2;
else
node = (list_entry(L1, element_t, list)->value < list_entry(L2, element_t, list)->value) ? &L1: &L2;
*ptr = *node;
ptr = &(*ptr)->next;
}
*ptr = (struct list_head *)((uintptr_t) L1 | (uintptr_t) L2);
return head;
}
```
但因為再跑 `make test` 時,一直解決不了在trace-03-ops出現的報錯如下:
```
+++ TESTING trace trace-03-ops:
# Test of insert_head, insert_tail, remove_head, reverse and merge
ERROR: Not sorted in ascending order
ERROR: Not sorted in ascending order
ERROR: Not sorted in ascending order
ERROR: Removed value a != expected value z
ERROR: Removed value b != expected value r
Error limit exceeded. Stopping command execution
```
由於多次的檢查都認為程式碼的邏輯是正確的,因此參考 [Risheng1128](https://hackmd.io/kZtEa_LdSGKVQGg8jczrLQ?view#q_sort) Hackmd ,發現該同學的程式碼中有 `strcmp` <s>語法</s> ,才得知在 `make test` 時輸入的會是字串而不是數字,因而針對程式碼進行更正。
:::danger
strcmp 不是語法 (syntax),謹慎用詞!
:::
更正的部份如下:
```diff
+ element_t *L1_entry = list_entry(L1, element_t, list);
+ element_t *L2_entry = list_entry(L2, element_t, list);
if (descend)
- node = (list_entry(L1, element_t, list)->value >
- list_entry(L2, element_t, list)->value)
- ? &L1
- : &L2;
+ node = (strcmp(L1_entry->value, L2_entry->value) > 0) ? &L1 : &L2;
else
- node = (list_entry(L1, element_t, list)->value <
- list_entry(L2, element_t, list)->value)
- ? &L1
- : &L2;
+ node = (strcmp(L1_entry->value, L2_entry->value) < 0) ? &L1 : &L2;
```
### q_reverseK, q_descend
原始的程式碼如下:
```c
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
if (!head || list_empty(head) || list_is_singular(head))
return;
bool flag = false;
struct list_head **ptr = &head;
struct list_head **tmp = &head;
while (true) {
for (int i = 0; i < k; i++) {
ptr = &(*ptr)->next;
if (*ptr == head) {
flag = true;
break;
}
}
if (flag)
break;
ptr = tmp;
for (int j = 0; j < k; j++) {
list_move((*ptr)->next, (*tmp));
ptr = &(*ptr)->next;
}
tmp = ptr;
}
}
```
```c
int q_descend(struct list_head *head)
{
if (!head || list_empty(head))
return 0;
struct list_head *cur = head->prev->prev, *tmp = head->prev;
int count = 1;
while (cur != head) {
count++;
int comp = strcmp(list_entry(cur, element_t, list)->value,
list_entry(tmp, element_t, list)->value);
if (comp <= 0) {
struct list_head *del = cur;
cur = cur->prev;
list_del(del);
q_release_element(list_entry(del, element_t, list));
count--;
} else
cur = cur->prev;
}
// https://leetcode.com/problems/remove-nodes-from-linked-list/
return 0;
return count;
}
```
但因為再跑 `make test` 時,發現在trace-06-ops出現的報錯如下:
```
+++ TESTING trace trace-06-ops:
# Test of insert_head, insert_tail, delete duplicate, sort, descend and reverseK
ERROR: Removed value c != expected value d
ERROR: Removed value d != expected value c
ERROR: Removed value c != expected value b
ERROR: Removed value b != expected value a
ERROR: Removed value c != expected value a
Error limit exceeded. Stopping command execution
```
經過檢查發現是 `q_reverseK` 和 `q_descend` 函式的邏輯錯誤。
釐清整體的架構後,更正 `q_reverseK` 函式的指標使用方法;更正 `q_descend` 函式中最小值,修正邏輯錯誤。
更正的部份如下:
**q_reverseK**
```diff
bool flag = false;
- struct list_head **ptr = &head;
- struct list_head **tmp = &head;
+ struct list_head *ptr = head;
+ struct list_head *tmp = head;
while (true) {
for (int i = 0; i < k; i++) {
- ptr = &(*ptr)->next;
- if (*ptr == head) {
+ ptr = ptr->next;
+ if (ptr == head) {
flag = true;
break;
}
```
**q_descend**
```diff
q_release_element(list_entry(del, element_t, list));
count--;
- } else
+ } else {
+ tmp = cur;
cur = cur->prev;
+ }
```
## 研讀論文 Dude, is my code constant time?
本篇論文測量執行時間的方式是對執行時間進行洩漏偵測測試。
步驟一:測量執行時間
1. Classes definition : [dudect](https://github.com/oreparaz/dudect/tree/master) 會輸入 fix-vs-random 兩種不同類別的資料並且使用 `leakage detection` 進行測試。
2. Cycle counters : 使用CPU的 `cycle counter` 計算實際的執行時間。
3. Environmental conditions : 隨機選擇資料類別,希望減少環境改變對結果的影響。
步驟二:後處理
1. Cropping : 執行時間較久測資很有可能會有偏差,因此把超過時間 `threshold` 的測量刪除。
2. Higher-order preprocessing : 使用[CJRR99](https://link.springer.com/chapter/10.1007/3-540-48405-1_26) 的Higher-order preprocessing。
步驟三:靜態測試
1. t-test : 透過 [Welch’s t-test](https://en.wikipedia.org/wiki/Welch%27s_t-test) ,輸入兩筆測試資料,確認兩筆資料的執行時間分布是否有違反 `null hypothesis: "the two timing distributions are equal"` 。如果檢測失敗代表執行時間不是常數時間。
3. Non-parametric tests : 使用 `Non-parametric tests` 針對這些未知分布做更穩健的測試。
## trace-17-ops
有鑑於 trace-17-ops 一直報錯,因此追蹤 make test 給定的命令檔如下:
```
# Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant
option simulation 1
it
ih
rh
rt
option simulation 0
```
可以發現它會將 simulation flag 設成1,並執行 it 等等指令。
因此再追蹤到 qtest.c 的 queue_insert 中,找到 qtest.c 會呼叫 is_insert_tail_const() ,最後追蹤到 fixture.c 會根據 [論文 Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf) 進行執行時間為 constant time 的驗證。
:::info
在[作業要求](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-f)有提到 [oreparaz/dudect](https://github.com/oreparaz/dudect) 的程式碼具備 percentile 的處理,但在 lab0-c 則無,因此比較兩者差異。
:::
在比較完 `fixture.c` 和 [oreparaz/dudect](https://github.com/oreparaz/dudect) 可以發現 lab0-c 並沒有考慮到 [論文 Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf) 中,步驟二後處理的 CROP ,因此將 [oreparaz/dudect](https://github.com/oreparaz/dudect)中的 percentile 在 fixture 中實做。
其中要特別注意到 `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.
*/
```
根據 [oreparaz/dudect](https://github.com/oreparaz/dudect) 在 `prepare_percentiles` 的註解中,可以知道 `prepare_percentiles` 會給定要被去除的 threshole ,因此必須保持 threshold 固定。
最後 `doit` 就會如以下的程式碼:
```c
prepare_inputs(input_data, classes);
bool ret = measure(before_ticks, after_ticks, input_data, mode);
bool first_time = percentiles[NUMBER_PERCENTILES - 1] == 0;
differentiate(exec_times, before_ticks, after_ticks);
if (first_time) {
// throw away the first batch of measurements.
// this helps warming things up.
prepare_percentiles(percentiles, exec_times);
} else {
update_statistics(exec_times, classes, percentiles);
ret &= report();
}
```
## 引入 Linux 核心原始碼到 lab0-c
參考 [Risheng1128](https://hackmd.io/kZtEa_LdSGKVQGg8jczrLQ?view#%E6%AF%94%E8%BC%83%E8%87%AA%E5%B7%B1%E5%AF%A6%E4%BD%9C%E7%9A%84-merge-sort-%E5%92%8C-Linux-%E6%A0%B8%E5%BF%83%E7%A8%8B%E5%BC%8F%E7%A2%BC) 將list_sort 引入 lab0-c。
在 `Makefile` 中新增 compare 執行 `traces/trace-sort.cmd`
`traces/trace-sort.cmd` 的內容如下:
可藉由調整 Rand 調整資料的數量。
```
option fail 0
option malloc 0
new
ih RAND 100000/300000/500000
time
sort
time
```
輸入指令
```
make compare
```
結果如下:
| 資料總數 | q_sort() 測試1(s) | q_sort() 測試2(s) | q_sort() 測試3(s) | q_sort() 測試4(s) | q_sort() 測試5(s) |
| -------- | ----------------- | ----------------- | ----------------- | ----------------- | ----------------- |
| 100000 | 0.08 | 0.076 | 0.078 | 0.075 | 0.076 |
| 300000 | 0.302 | 0.309 | 0.316 | 0.309 | 0.313 |
| 500000 | 0.614 | 0.585 | 0.578 | 0.576 | 0.585 |
| 資料總數 | list_sort() 測試1(s) | list_sort() 測試2(s) | list_sort() 測試3(s) | list_sort() 測試4(s) | list_sort() 測試5(s) |
| -------- | -------------------- | -------------------- | -------------------- | -------------------- | -------------------- |
| 100000 | 0.052 | 0.053 | 0.053 | 0.056 | 0.052 |
| 300000 | 0.205 | 0.207 | 0.206 | 0.202 | 0.204 |
| 500000 | 0.383 | 0.372 | 0.374 | 0.375 | 0.374 |
| 資料總數 | q_sort() 平均(s) | list_sort() 平均(s) | 效能比較(%) |
| -------- | -------- | -------- | --- |
| 100000 | 0.077 | 0.053 | 31.16% |
| 300000 | 0.310 | 0.205 | 33.87% |
| 500000 | 0.588 | 0.375 | 36.22% |
可以看到 list_sort() 的效能比起 q_sort() 還要好,且也可以觀察到隨著資料總數大小增加,效能的差距也變得愈加明顯。
接下來使用 [Linux 效能分析工具: Perf](https://wiki.csie.ncku.edu.tw/embedded/perf-tutorial#%E7%B0%A1%E4%BB%8B) 比較效能細項的部份。
使用更改kernel權限
```
sudo sysctl kernel.perf_event_paranoid=<parameter>
```
輸入指令
```
perf stat --repeat 10 -e cache-misses,cache-references,instructions,cycles ./qtest -f traces/trace-sort.cmd
```
讓它根據 `trace-sort.cmd` (此處的 Rand 為500000)針對 `cache-misses` 、 `cache-references` 、 `instructions` 、 `cycles` 做10次測試並且輸出平均值。
結果如下:
| | q_sort | list_sort |
| ---------------- | ------ | --------- |
| cache-misses | 56,154,930 | 45,669,350 |
| cache-references | 102,054,812 | 79,353,435 |
| instructions | 2,414,610,171 | 2,394,503,043 |
| cycles | 4,502,998,337 | 3,601,189,041 |
| insn per cycle | 0.54 | 0.66 |
可以看到不管是 `cache-misses` 、 `cache-references` 、 `instructions` 還是 `cycles` , list_sort() 都是優於 q_sort()。
---
在 [Linux 核心專題: 改進 lib/list_sort.c](https://hackmd.io/@sysprog/Hy5hmaKBh) 中提及可以使用 bottom-up 的方法改善 [cache-thrshing](https://en.wikipedia.org/wiki/Thrashing_(computer_science)) 的問題,以加快速度。
因為不懂 top-down 和 bottom-up 的實作差別,所以參考了 [動態規劃(Dynamic Programming)](https://hackmd.io/@giver/SJCIN4GMr),裡面提及 top-down 和 bottom-up 的差別,且通常 top-down 會使用遞迴來實作,而 bottom-up 通常使用迭代實作,因此我又開始疑惑,所以遞迴的速度一定會比迭代還要慢嗎?
又參考了 [關於遞迴加快速度的迷思?](https://www.ptt.cc/man/C_and_CPP/DDD2/M.1460743098.A.800.html)才終於對遞迴和迭代有了一點點的概念。
接著,參考 [Merge Sort 與它的變化](https://hackmd.io/@lambert-wu/list-merge-sort) 使用了迭代的方式完成 `Merge sort`,並且發現,因為程式碼實作的關係,遞迴版的 `Merge sort` 比起迭代版的 `Merge sort`有更多的 Cache miss,導致速度變慢。
參考自 [csotaku0926](https://hackmd.io/@csotaku0926/linux2024-homework1#%E6%8E%A2%E8%A8%8E-liblib_sortc) 同學。
>bottom-up 的過程就是一直合併,cache 元素參照的機會更大。
top-down 涉及 parition,這麼做會導致元素不斷從 cache 進出,而導致 cache thrashing 的問題。
:::info
TODO:嘗試實作 `Timsort` 演算法,引入 lab0-c 與 list_sort 進行比較。
:::
Timsort 內容請看 [2024q1 Homework2 (quiz1+2)](https://hackmd.io/@Yourui/linux2024-homework2) 。
## 參考資料
[CPU Cache 原理探討](https://hackmd.io/@drwQtdGASN2n-vt_4poKnw/H1U6NgK3Z)