---
tags: linux2023
---
# 2023q1 Homework1 (lab0)
contributed by < [chiangkd](https://github.com/chiangkd) >
:::warning
:warning: 留意細節!唯有重視小處並步步為營,方可挑戰原始程式碼達到三千萬行的 Linux 核心
:notes: jserv
> Got it! 謝謝老師
:::
## 作業要求
Request followed [2023 年 Linux 核心設計/實作課程作業 —— lab0](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-f)
- [ ] 修改 `queue.[ch]` 以完成 `make test` 自動評分系統的所有項目
- [ ] 運用 Valgrind 排除 `qtest` 實作的記憶體錯誤,搭配 Massif 視覺化
- [ ] 研讀 Linux 核心原始程式碼的 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 並嘗試引入到 lab0-c 專案,比較你自己實作的 merge sort 和 Linux 核心程式碼之間效能落差
- [ ] 確保 `qtest` 提供的 `web` 命令可以搭配上述佇列使用
- [ ] 新增命令 `shuffle`, 參考 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle)
- [ ] 在 `qtest` 中執行 `option entropy 1` 並搭配 `ih RAND 10`,觀察亂數字串分佈
- [ ] 研讀論文〈[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) 及程式實作的原理。
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.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: 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) i7-7700HQ CPU @ 2.80GHz
CPU family: 6
Model: 158
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 9
CPU max MHz: 3800.0000
CPU min MHz: 800.0000
BogoMIPS: 5599.85
```
## 改進 `queue.c`
### 結構體
`list_head` 為 doubly-linked list 的 node 或 head
```c
struct list_head {
struct list_head *prev;
struct list_head *next;
};
```
```graphviz
digraph list_ele {
rankdir=LR;
node[shape=record];
head [label="{<left>prev|<right>next}", style=bold];
}
```
`element_t` 為 linked list 的 element
```c
typedef struct {
char *value;
struct list_head list;
} element_t;
```
`element_t` 這個結構體,除了要用來找尋 `list` 整體的原因,透過嵌入 `list_head` 給除了
`Head` 可以搭配 `container_or` 巨集也可得知
```graphviz
digraph list {
rankdir=LR;
node[shape=record, style=bold];
subgraph cluster_0 {
node [shape=record];
value [label="{value}"];
head [label="{<prev>prev|<next>next}"];
style=bold;
label=element_t
}
}
```
![](https://i.imgur.com/3b5ra8Q.png)
- 參考[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#circular-linked-list)
```c
typedef struct {
struct list_head head;
int size;
} queue_chain_t;
```
```graphviz
digraph list {
rankdir=LR;
node[shape=record, style=bold];
subgraph cluster_0 {
node [shape=record];
head [label="{<prev>prev|<next>next}"];
value [label="{size}"];
style=bold;
label=queue_chain_t
}
}
```
`queue_context_t` 為 chain of queues
```c
typedef struct {
struct list_head *q;
struct list_head chain;
int size;
int id;
} queue_contex_t;
```
```graphviz
digraph list {
rankdir=LR;
node[shape=record, style=bold];
subgraph cluster_1 {
node [shape=record];
q_1 [label="q|{<prev>prev|<next>next}"];
subgraph cluster_A {
chain [label="<label>chain|{<prev>prev|<next>next}"];
s [label="{size}"];
style="dashed,bold";
color=red;
label=queue_chain_t
}
int [label="{int}"];
style=bold;
label=queue_context_t
}
subgraph cluster_2 {
node [shape=record];
q_2 [label="q|{<prev>prev|<next>next}"];
subgraph cluster_B {
chain_2 [label="<label>chain|{<prev>prev|<next>next}"];
s_2 [label="{size}"];
style="dashed,bold";
color=red;
label=queue_chain_t
}
int_2 [label="{int}"];
style=bold;
label=queue_context_t
}
chain:next->chain_2:label
chain_2:prev->chain:label
}
```
在多個 queue 之間的操作,如 `do_queue` 是透過 `chain` 來連接起來的,也因此在 `q_merge` function 中給定的參數為 `list_head *head`,但查看 `do_merge` 之後才會看到這裡傳入的是 `queue_chain_t` 的 `chain`,也是連接各個 queue 的 linked list。
```c
if (current && exception_setup(true))
len = q_merge(&chain.head);
```
做個驗證,在 `q_merge` 中來測試並呼叫 `merge` command,函式中暫時先撰寫透過傳入的 `chain` 找其所屬 `element_t` 的 value
```c
printf("value = %s\n", list_entry(list_entry(head->next, queue_contex_t, chain)->q->next, element_t, list)->value);
```
```shell
cmd> new
l = []
cmd> ih Hello
l = [Hello]
cmd> it CKD
l = [Hello CKD]
cmd> merge
value = Hello
```
- 印出 list 的第一個 `element_t` 的 `value`
:::warning
與其複製程式碼,你應揣摩背後的設計考量,特別是你的推理和驗證。
:notes: jserv
> 新增示意圖及個人認為設計考量(待補充)
:::
:::info
題外話,在化 graphviz 的時候想要虛線功能,但不知道關鍵字是什麼 (加粗為 `bold`,直覺認為是 `dot`),問了一下 chatGPT 之後它告訴我是 `dotted`
![](https://i.imgur.com/IyEAwvM.png)
:::
### q_new
```c
struct list_head *q_new()
{
struct list_head *q_head = malloc(sizeof(*q_head));
if (!q_head) // If allocate failed
return NULL;
INIT_LIST_HEAD(q_head);
return q_head;
}
```
:::warning
改為更精簡的敘述:
```c
struct list_head *q_head = malloc(sizeof(*q_head));
```
修正程式碼註解用語。
:notes: jserv
>Fixed. Thanks!
:::
### q_free
```c
void q_free(struct list_head *l)
{
if(!l)
return;
element_t *c, *n;
list_for_each_entry_safe(c, n, l, list) {
list_del(&c->list);
q_release_element(c);
}
free(l); // q_new function has malloced it
}
```
### q_insert_head
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *new_element = malloc(sizeof(element_t)); // new head element
if(!new_element) // If allocate failed
return false;
size_t len = strlen(s) + 1; // plus 1 for `\0`
new_element->value = malloc(len * sizeof(char));
if (!new_element->value) { // If allocate failed
free(new_element);
return false;
}
memcpy(new_element->value, s, len);
list_add(&new_element->list, head);
return true;
}
```
:::warning
能否避免呼叫 `memcpy` 並精簡程式碼?
:notes: jserv
>改為利用 `strdup`
:::
發現在 `harness.c` 中已有定義 `strdup`(`test_strdup`),此函式回傳輸入 string `s` 的指標,並回傳一個具有同樣內容的指標,即複製字串。
透過此函式可以直接取代掉原實作方法中的計算長度、分配空間、記憶體複製操作
```diff
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *new_element = malloc(sizeof(element_t)); // new head element
if (!new_element) // allocated failed
return false;
- size_t len = strlen(s) + 1; // plus 1 for `\0`
- new_element->value = malloc(len * sizeof(char));
+ new_element->value = strdup(s);
if (!new_element->value) { // allocated failed
free(new_element);
return false;
}
- memcpy(new_element->value, s, len);
list_add(&new_element->list, head);
return true;
}
```
### q_insert_tail
和 `q_insert_head` 思路相同, 僅修改 `head` 和 `tail` 的差別
:::warning
既然 `q_insert_tail` 和 `q_insert_head` 存在相似的流程,能否用 `q_insert_head` 來實作 `q_insert_tail` 呢?
:notes: jserv
>新增相關敘述
:::
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if(!head)
return false;
element_t *new_element = malloc(sizeof(element_t)); // new tail element
if(!new_element) // If allocate failed
return false;
size_t len = strlen(s) + 1; // plus 1 for `\0`
new_element->value = malloc(len * sizeof(char));
if(!new_element->value) { // If allocate failed
free(new_element);
return false;
}
memcpy(new_element->value, s, len);
list_add_tail(&new_element->list, head);
return true;
}
```
`q_insert_head` 以及 `q_insert_tail` 差異僅在新增節點於給定 `head` 的後一個位置 (`next` 所指方向),以及前一個位置 (`prev` 所指方向),故可以透過 `q_insert_head` 來實作 `q_insert_tail`
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
return q_insert_head(head->prev, s);
}
```
翻譯成白話文的意思就是,在 `head` 的前面插入節點等效於在 `head` 的前一個節點的後面插入節點。
### q_remove_head
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if(!head)
return NULL;
sp = malloc(bufsize);
element_t *rm_element = container_of(head->next, element_t, list);
strncpy(sp, rm_element->value, bufsize - 1);
sp[bufsize - 1] = '\0';
list_del(head->next);
return rm_element;
}
```
- `container_of` 這個巨集透過給定成員 (member) 的記憶體地址、結構體的型態,以及成員的名稱, 傳回該結構體的起始地址
```c
#define container_of(ptr, type, member) \
__extension__({ \
const __typeof__(((type *) 0)->member) *__pmember = (ptr); \
(type *) ((char *) __pmember - offsetof(type, member)); \
})
```
上述雖然可以成功 remove head, 但是產生錯誤訊息
```
ERROR: Failed to store removed value
```
查看 `qtest.c` 中 `do_remove` 的實作
```c
removes[string_length + STRINGPAD] = '\0';
if (removes[0] == '\0') {
report(1, "ERROR: Failed to store removed value");
ok = false;
}
```
:::warning
implementation 應翻譯為「實作」,注意[「作」和「做」的落差](https://blog.udn.com/mobile/tsengche/3054517)」。
參見 [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary) 的 "implement" 項目。
:notes: jserv
> 已修正,謝謝老師
:::
- `removes[0] = '\0'` 會報此錯誤, 且 `removes` 在 `qtest.c` 已經 `malloc` 如果在 `q_remove` 中再次 `malloc` 一次的話 sp 的地址會被覆蓋掉, 即原先 `removes` 的地址其實並沒有存東西, 所以這裡的 `sp` 不該 `malloc`
在佇列為空時繼續使用命令 `rh` 會造成 segmentation fault, 當佇列為空時僅有 `Head` 一個點, 沒辦法通過 `container_of` 找到對應節點
```
l = []
cmd> rh
Warning: Calling remove head on empty queue
Segmentation fault occurred. You dereferenced a NULL or invalid pointerAborted (core dumped)
```
加入 `list_empty` 來判斷該鏈結串列是不是 empty 以及外部 `removes` 是否有 `malloc` 成功
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *rm_element = container_of(head->next, element_t, list);
if (sp) {
strncpy(sp, rm_element->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(head->next);
return rm_element;
}
```
這裡的 `container_of` 可以用 `list_first_entry` 巨集代替, 增加程式可讀性 (defined in `list.h`)
```c
#define list_entry(node, type, member) container_of(node, type, member)
#define list_first_entry(head, type, member) \
list_entry((head)->next, type, member)
```
- 接下來的 q_remove_tail 也是一樣可以用 `list_last_entry` 取代 `container_of`
### q_remove_tail
和 `q_remove_head` 思路相同, 僅修改 `head` 和 `tail` 的差別
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if(!head || list_empty(head))
return NULL;
element_t *rm_element = list_last_entry(head, element_t, list);
if(sp) {
strncpy(sp, rm_element->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(head->prev);
return rm_element;
}
```
### q_size
走訪整個鏈結串列以計算長度
```c
int q_size(struct list_head *head)
{
if(!head)
return 0;
int cnt = 0;
struct list_head *n = head->next; // next list node
while (n != head) {
n = n->next;
cnt++;
}
return cnt;
}
```
:::warning
TODO: 善用既有的巨集,撰寫出更精簡的程式碼
:notes: jserv
> 用 `list_for_each` 取代現有 for loop
:::
```c
int q_size(struct list_head *head)
{
if (!head || list_empty(head))
return 0;
int cnt = 0;
struct list_head *n;
list_for_each (n, head)
cnt++;
return cnt;
}
```
其中 `list_for_ecah` 定義為
```c
#define list_for_each(node, head) \
for (node = (head)->next; node != (head); node = node->next)
```
### q_delete_mid
:::warning
本例為 circular doubly-linked list,你應該實作更少記憶體操作的程式碼。
:notes: jserv
:::
:::info
原先版本使用快慢指標,快指標會走訪整個 list 一圈,慢指標會走一半的 list 找到中間點,總共會走大概 1.5 倍長度的 list,參考 [freshLiver](https://hackmd.io/@freshLiver/2022q1-hw1#%E9%87%9D%E5%B0%8D%E4%BD%87%E5%88%97%E7%9A%84%E6%93%8D%E4%BD%9C) 過往作業使用到兩個指標前後從佇列頭尾迭代,透過交會點/即將交會點來判斷佇列中間點
:::
- 參考[你所不知道的 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)
```c
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head **indir;
indir = &head;
for(struct list_head *p = head->prev; (*indir)->next != p && (*indir) != p; ) {
indir = &(*indir)->next;
p = p->prev;
}
struct list_head *temp = (*indir)->next;
list_del(temp);
q_release_element(list_entry(temp, element_t, list));
return true;
}
```
在這邊使用 indirect pointer 看起來沒有特別的優勢,反而在過程多了 `*indir` 這一個記憶體操作
:::warning
對照編譯器輸出的組合語言和 `perf` 來確認你的觀點,否則這個「看來」流於武斷。注意編譯器最佳化的影響
:notes: jserv
> 初步分析以補充,待補如果設定編譯器為 `-O0` 後的組合語言差異
:::
```c
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head *n = head->next;
for(struct list_head *p = head->prev; n->next != p && n != p; ) {
n = n->next;
p = p->prev;
}
list_del(n);
q_release_element(list_entry(n, element_t, list));
return true;
}
```
且如此運算過後 `n` 會正好指向要移除之節點,不需要在額外設定指向要刪除的節點進行操作,
#### 編譯器輸出的組合語言
用 `objdump -d queue.o` 查看編譯器輸出的組合語言
```c
00000000000002d3 <q_delete_mid>:
2d3: f3 0f 1e fa endbr64
2d7: 48 85 ff test %rdi,%rdi
2da: 74 52 je 32e <q_delete_mid+0x5b>
2dc: 53 push %rbx
2dd: 48 8b 5f 08 mov 0x8(%rdi),%rbx
2e1: 48 39 df cmp %rbx,%rdi
2e4: 74 4e je 334 <q_delete_mid+0x61>
2e6: 48 8b 17 mov (%rdi),%rdx
2e9: 48 8b 43 08 mov 0x8(%rbx),%rax
2ed: 48 39 d3 cmp %rdx,%rbx
2f0: 74 19 je 30b <q_delete_mid+0x38>
2f2: 48 39 c2 cmp %rax,%rdx
2f5: 74 14 je 30b <q_delete_mid+0x38>
2f7: 48 8b 12 mov (%rdx),%rdx
2fa: 48 89 c3 mov %rax,%rbx
2fd: 48 8b 40 08 mov 0x8(%rax),%rax
301: 48 39 da cmp %rbx,%rdx
304: 74 05 je 30b <q_delete_mid+0x38>
306: 48 39 d0 cmp %rdx,%rax
309: 75 ec jne 2f7 <q_delete_mid+0x24>
30b: 48 8b 13 mov (%rbx),%rdx
30e: 48 89 10 mov %rdx,(%rax)
311: 48 89 42 08 mov %rax,0x8(%rdx)
315: 48 8b 7b f8 mov -0x8(%rbx),%rdi
319: e8 00 00 00 00 call 31e <q_delete_mid+0x4b>
31e: 48 8d 7b f8 lea -0x8(%rbx),%rdi
322: e8 00 00 00 00 call 327 <q_delete_mid+0x54>
327: b8 01 00 00 00 mov $0x1,%eax
32c: 5b pop %rbx
32d: c3 ret
32e: b8 00 00 00 00 mov $0x0,%eax
333: c3 ret
334: b8 00 00 00 00 mov $0x0,%eax
339: eb f1 jmp 32c <q_delete_mid+0x59>
```
結果上述的兩個版本編譯器編出來的組合語言竟然是一樣的,查看 `makefile` 發現編譯器優化等級設定在 `-O1`,如果禁用優化改成 `-O0` 編譯出來的組合語言才會有差
#### 使用 [perf](https://en.wikipedia.org/wiki/Perf_(Linux)) 確認上述猜測
- 參考 [Linux 效能分析工具: Perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial#%E8%83%8C%E6%99%AF%E7%9F%A5%E8%AD%98),記得先參考[安裝](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial#%E5%AE%89%E8%A3%9D)章節將權限打開
- 雖然上面已經看到經過編譯器優化後的組合語言相同,可想而知效能一定一樣。
```shell
sudo sh -c " echo -1 > /proc/sys/kernel/perf_event_paranoid"
```
定義一個測試案例 `trace-demid.cmd`
```shell
option fail 0
option malloc 0
new
ih RAND 500000
time
dm
time
```
輸入命令
```shell
perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles ./qtest -f traces/trace-demid.cmd
```
`perf stat` 可以測量單個程序運行時間內的性能係數
- `--repeat 5` 重複 5 次
- `-e` 後面接著 [PMU](https://en.wikipedia.org/wiki/Hardware_performance_counter) 的事件,可以用 `perf list` 查看所有事件
結果如下
```
Performance counter stats for './qtest -f traces/trace-demid.cmd' (5 runs):
1530,3333 cache-misses # 90.230 % of all cache refs ( +- 0.34% )
1658,7721 cache-references ( +- 1.66% )
16,8910,5300 instructions # 0.83 insn per cycle ( +- 0.01% )
19,7202,0142 cycles ( +- 2.88% )
0.5664 +- 0.0189 seconds time elapsed ( +- 3.34% )
```
:::spoiler Iteration process
以 [2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/description/) 舉例
```graphviz
digraph del_mid {
node [shape=circle]
rankdir=LR
node1[label="1", rank=2]
node2[label="3"]
node3[label="4"]
node4[label="7"]
node5[label="1"]
node6[label="2"]
node7[label="6"]
// backbone
node1 -> node2 -> node3 -> node4 ->node5->node6->node7
head->node1
// mark
p [shape=plaintext;label="p"]
p->node7[constraint="false"]
n [shape=plaintext;label="n"]
n->node1[constraint="false"]
}
```
```graphviz
digraph del_mid {
node [shape=circle]
rankdir=LR
node1[label="1"]
node2[label="3"]
node3[label="4"]
node4[label="7"]
node5[label="1"]
node6[label="2"]
node7[label="6"]
// backbone
node1 -> node2 -> node3 -> node4 ->node5->node6->node7
head->node1
// mark
p [shape=plaintext;label="p"]
p->node6[constraint="false"]
n [shape=plaintext;label="n"]
n->node2[constraint="false"]
}
```
```graphviz
digraph del_mid {
node [shape=circle]
rankdir=LR
node1[label="1"]
node2[label="3"]
node3[label="4"]
node4[label="7"]
node5[label="1"]
node6[label="2"]
node7[label="6"]
// backbone
node1 -> node2 -> node3 -> node4 ->node5->node6->node7
head->node1
// mark
p [shape=plaintext;label="p"]
p->node5[constraint="false"]
n [shape=plaintext;label="n"]
n->node3[constraint="false"]
}
```
```graphviz
digraph del_mid {
node [shape=circle]
rankdir=LR
node1[label="1"]
node2[label="3"]
node3[label="4"]
node4[label="7", color="red"]
node5[label="1"]
node6[label="2"]
node7[label="6"]
// backbone
node1 -> node2 -> node3 -> node4 ->node5->node6->node7
head->node1
// mark
p [shape=plaintext;label="p"]
p->node4[constraint="false"]
n [shape=plaintext;label="n"]
n->node4[constraint="false", color=red]
}
```
:::
:::spoiler Old version
利用快慢指標 ([Floyd's tortoise and hare](https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare)) 找中間點, 因為在本次作業中已經確保快慢指標 (或龜兔指標) 已經都在環中, 所以設定快指標移動速度為慢指標兩倍, 當快指標繞完一圈時慢指標指向中間點(有 `Head` 節點則差一個節點)
```c
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head **indir;
indir = &head;
struct list_head *fast = head;
while(!(fast->next == head || fast->next->next == head)) {
indir = &(*indir)->next;
fast = fast->next->next;
}
q_release_element(list_entry((*indir)->next, element_t, list));
(*indir)->next = (*indir)->next->next;
return true;
}
```
```
cmd> dm
Segmentation fault occurred. You dereferenced a NULL or invalid pointer
Aborted (core dumped)
```
- 這裡如果直接把 `(*indir)->next` 直接 release 掉的話會造成 `(*indir)->next` 指向 `NULL` 這不是我們想要的, 這裡我先用 `list_del((*indir)->next)` 將 `(*indir)` 和 `(*indir)->next->next` linked 起來,在將原先的 `(*indir)->next` release 掉
```c
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head **indir;
indir = &head;
struct list_head *fast = head;
while (fast->next!= head && fast->next->next != head) {
indir = &(*indir)->next;
fast = fast->next->next;
}
struct list_head* temp = (*indir)->next;
list_del((*indir)->next);
q_release_element(list_entry(temp, element_t, list));
return true;
}
```
**Iteration Process**
```graphviz
digraph del_mid {
node [shape=box]
rankdir=LR
// backbone
node1 -> node2 -> node3 -> node4
head->node1
// mark
indir [shape=plaintext;label="indir"]
indir->head
fast [shape=plaintext;label="fast"]
fast->node1
}
```
```graphviz
digraph del_mid {
node [shape=box]
rankdir=LR
// backbone
node1 -> node2 -> node3 -> node4
head->node1
// mark
indir [shape=plaintext;label="indir"]
indir->node1
fast [shape=plaintext;label="fast"]
fast->node3
}
```
以 [2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/description/) 實際演算一次
```graphviz
digraph del_mid {
node [shape=circle]
rankdir=LR
node1[label="1", rank=2]
node2[label="3"]
node3[label="4"]
node4[label="7"]
node5[label="1"]
node6[label="2"]
node7[label="6"]
// backbone
node1 -> node2 -> node3 -> node4 ->node5->node6->node7
head->node1
// mark
indir [shape=plaintext;label="indir"]
indir->head[constraint="false"]
fast [shape=plaintext;label="fast"]
fast->node1[constraint="false"]
}
```
```graphviz
digraph del_mid {
node [shape=circle]
rankdir=LR
node1[label="1"]
node2[label="3"]
node3[label="4"]
node4[label="7"]
node5[label="1"]
node6[label="2"]
node7[label="6"]
// backbone
node1 -> node2 -> node3 -> node4 ->node5->node6->node7
head->node1
// mark
indir [shape=plaintext;label="indir"]
indir->node1[constraint="false"]
fast [shape=plaintext;label="fast"]
fast->node3[constraint="false"]
}
```
```graphviz
digraph del_mid {
node [shape=circle]
rankdir=LR
node1[label="1"]
node2[label="3"]
node3[label="4"]
node4[label="7"]
node5[label="1"]
node6[label="2"]
node7[label="6"]
// backbone
node1 -> node2 -> node3 -> node4 ->node5->node6->node7
head->node1
// mark
indir [shape=plaintext;label="indir"]
indir->node2[constraint="false"]
fast [shape=plaintext;label="fast"]
fast->node5[constraint="false"]
}
```
```graphviz
digraph del_mid {
node [shape=circle]
rankdir=LR
node1[label="1"]
node2[label="3"]
node3[label="4"]
node4[label="7", color="red"]
node5[label="1"]
node6[label="2"]
node7[label="6"]
// backbone
node1 -> node2 -> node3 -> node4 ->node5->node6->node7
head->node1
// mark
indir [shape=plaintext;label="indir"]
indir->node3[constraint="false", color="red"]
fast [shape=plaintext;label="fast"]
fast->node7[constraint="false"]
}
```
- remove `(*indir)->next` 節點
:::
### q_delete_dup
首先思考使用 `list_for_each_entry_safe` 實作,根據描述
>list_for_each_entry_safe - Iterate over list entries and allow deletes
會走訪整個 list 的 entries (`typeof(entry)`) 並允許刪除當前的節點
```c
bool q_delete_dup(struct list_head *head)
{
if (!head || list_empty(head))
return false;
element_t *c, *n; // current and next element
bool is_dup = false;
list_for_each_entry_safe (c, n, head,
list) { // current node (iterator) is allowed to
// be removed from the list.
if (c->list.next != head &&
strcmp(c->value, n->value) == 0) // duplicate string detection
{
list_del(&c->list); // delete node
q_release_element(c);
is_dup = true;
} else if (is_dup) {
list_del(&c->list);
q_release_element(c);
is_dup = false;
}
}
return true;
}
```
執行 `list_for_each_entry_safe` 可以走訪整個 list 但是需要注意在走訪完時 `n` 會指向 `head`,且 `head` 是無法透過 `container_of` 找到節點的,因為它沒有嵌入到結構體中,這時如果對 `n` 取 `contain_of` 會拿到 `NULL` 如果再做取值 `n->value` 會<s>報錯</s> 成為無效的記憶體操作。
> [invalid page fault](https://en.wikipedia.org/wiki/Page_fault)
故在第一個 `if` 條件使用 `c->list.next != head` 來判斷是否來到最後的節點 (雖然 `list_for_each_entry_safe` 本身就有做),避免在 `strcmp(c->value, n->value) == 0` 時 `n->value` 對 `NULL` 指標取值。
:::warning
TODO: 減少記憶體操作的數量
:notes: jserv
:::
### q_swap
```c
void q_swap(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *n = head->next;
while (n != head && n->next != head) {
struct list_head *t = n;
list_move(n, t->next);
n = n->next;
}
}
```
交換佇列中鄰近的節點,將兩個鄰近的節點中的第一個節點移動至以令一個當作 `head` 節點的開頭 (也就是說會放在另一個節點的 `next`),移動節點可分為兩步驟(刪除以及插入),對應到 `list_move` 中定義的兩步驟 `list_del` 以及 `list_add`
### q_reverse
```c
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *n, *s, *t;
list_for_each_safe (n, s, head) {
t = n->next;
n->next = n->prev;
n->prev = t;
}
t = head->next;
head->next = head->prev;
head->prev = t;
}
```
一開始原本是考慮到沒有刪除節點的問題所以使用 `list_for_each` 來走訪 list ,但是在 reverse 的過程中的 `node` 會把 `next` 和 `prev` 做交換,會讓原本定義的巨集出現錯誤,導致無法順利走訪。
改用 `list_for_each_safe`
```c
#define list_for_each_safe(node, safe, head) \
for (node = (head)->next, safe = node->next; node != (head); \
node = safe, safe = node->next)
```
在迭代過程中 `safe` 指標會始終維持在 `node` 的下一個節點(沒有 reverse 的),使用 `node = safe` 以及 `safe = node->next` 確保迭代過程 `node` 不會往回走。
### q_reverseK
此<s>函數</s> 函式需要將 list 中的 k 個節點進行 reverse,直覺來說會利用到上方已經實作完成的 `q_reverse`,為此在使用時符合 `q_reverse` 參數需求,傳入該 list 的 `head`,故使用 `list_cut_position` 對特定長度的 list 進行分割,並在分割下來的 list 頭部新增 `iter` 作為該 list 的 `head` 供 `q_reverse` 使用
:::warning
注意用語,參見 [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)。上述 "reverse" 應有對應的漢語描述
:notes: jserv
:::
```c
void q_reverseK(struct list_head *head, int k)
{
if (!head || list_empty(head))
return;
struct list_head *n, *s, iter, *tmp_head = head;
int i = 0;
INIT_LIST_HEAD(&iter);
list_for_each_safe (n, s, head) {
i++;
if (i == k) {
list_cut_position(&iter, tmp_head, n);
q_reverse(&iter);
list_splice_init(&iter, tmp_head);
i = 0;
tmp_head = s->prev;
}
}
}
```
- 參考 [25077667](https://hackmd.io/@25077667/lab0-2023#2023q1-Homework1-lab0),及 [komark06](https://hackmd.io/@komark06/SyCUIEYpj)
### q_sort
實作 merge sort,先將環狀結構斷開後,傳入 `head->next` 進去 `merge_recur`
**merge_two_list**
- 結合兩個「已排序」的鏈結串列
```c
struct list_head *merge_two_list(struct list_head *left,
struct list_head *right)
{
struct list_head head;
struct list_head *h = &head;
if (!left && !right) {
return NULL;
}
while (left && right) {
if (strcmp(list_entry(left, element_t, list)->value,
list_entry(right, element_t, list)->value) < 0) {
h->next = left;
left = left->next;
h = h->next;
} else {
h->next = right;
right = right->next;
h = h->next;
}
}
// after merge, there are still one node still not connect yet
if (left) {
h->next = left;
} else if (right) {
h->next = right;
}
return head.next;
}
```
:::warning
TODO: 撰寫更精簡的程式碼,縮減分支
:notes: jserv
:::
**merge_recur**
- 利用快慢指標將 list 的中間節點找出來並遞迴處理
:::warning
避免使用含糊的「中點」,應有對應的定義及說明。
:notes: jserv
:::
```c
struct list_head *merge_recur(struct list_head *head)
{
if (!head->next)
return head;
struct list_head *slow = head;
// split list
for (struct list_head *fast = head->next; fast && fast->next;
fast = fast->next->next) {
slow = slow->next;
}
struct list_head *mid = slow->next; // the start node of right part
slow->next = NULL;
struct list_head *left = merge_recur(head);
struct list_head *right = merge_recur(mid);
return merge_two_list(left, right);
}
```
**q_sort**
- 傳入 `head->next` 進行 merge sort 可以保留原本的 `head` ,在排序過後在進行 `prev` 以及 `circular` 的修補
```c
void q_sort(struct list_head *head)
{
if (!head || list_empty(head))
return;
// disconnect the circular structure
head->prev->next = NULL;
head->next = merge_recur(head->next);
// reconnect the list (prev and circular)
struct list_head *c = head, *n = head->next;
while (n) {
n->prev = c;
c = n;
n = n->next;
}
c->next = head;
head->prev = c;
}
```
### q_descend
此函數會將**右邊存在有嚴格大於的節點的節點**給移除 (remove)
```c
int q_descend(struct list_head *head)
{
struct list_head *c = head->prev, *n = c->prev;
while (n != head) {
if (strcmp(list_entry(n, element_t, list)->value,
list_entry(c, element_t, list)->value) < 0) {
list_del(n);
} else {
c = n;
}
n = n->prev;
}
return q_size(head);
}
```
如果順著 list 方向進行走訪 (以 `next` 指標迭代),無法確定迭代的過程中會不會遇到嚴格大於的節點(如果遇到則走放過的節點都要移除),故這邊我以反方向進行迭代(以 `prev` 指標迭代),找尋下一個節點值只要和當前節點做比較,若下一個節點小於當前節點的話則移除。
執行測試案例的時候發現 `trace-06` 過不了,發現是 `descend` 後被消除的 block 沒有被清掉,一開始看到函式敘述用 `remove` 時以為不用刪除
> Remove every node which has a node with a strictly greater value anywhere to the right side of it
但它也沒有存到像 `q_remove` 系列的 `sp` 中,由 `q_test` 負責刪除,故修正在函式內進行記憶體釋放
```c
int q_descend(struct list_head *head)
{
if (!head)
return 0;
struct list_head *c = head->prev;
element_t *c_ele = list_entry(c, element_t, list);
while (c_ele->list.prev != head) {
element_t *n_ele = list_entry(c_ele->list.prev, element_t, list);
if (strcmp(n_ele->value, c_ele->value) < 0) {
list_del(&n_ele->list);
q_release_element(n_ele);
} else {
c_ele = n_ele;
}
}
return q_size(head);
}
```
### q_merge
先看一下 `do_merge` 要我們做什麼
```c
if (current && exception_setup(true))
len = q_merge(&chain.head);
exception_cancel();
```
- 輸入 chain head 回傳長度
且在 `do_merge` 中會負責把被 merge 掉的 queue 空間 `free` 掉,接著再檢查是否真的有按照 ascending order 來排序
```c
int q_merge(struct list_head *head)
{
if (!head)
return 0;
queue_contex_t *c_cont;
queue_contex_t *n_cont;
struct list_head *sorted = NULL;
list_for_each_entry_safe (c_cont, n_cont, head, chain) { // iterate context
c_cont->q->prev->next = NULL;
c_cont->q->prev = NULL;
sorted = merge_two_list(sorted, c_cont->q->next);
INIT_LIST_HEAD(c_cont->q); // reconnect the lists which are moved and
// merged to "sorted" list;
}
LIST_HEAD(tmp);
struct list_head *t = &tmp;
t->next = sorted;
struct list_head *c = t;
while (sorted) {
sorted->prev = c;
c = sorted;
sorted = sorted->next;
}
c->next = t;
t->prev = c;
int size = q_size(t); // store size before splice to main queue
list_splice(t, list_first_entry(head, queue_contex_t, chain)->q);
return size;
}
```
實作過程中利用在 merge sort 已經寫好的 `merge_two_list` ,不過須注意的是這邊我的 `merge_two_list` 傳入值為兩個**沒有 `head` 的 list**,也就是每一個節點其對應到的 element 都有數值,且不是環狀 linked list ,實做過程將兩個已經 sorted 好的 list 傳入 function 並回傳將兩個 list merged 完成的 list 的第一個節點地址,此時可以把 merged 好的 list 交給主要的 context (迭代中的) 的 list head (其 element 沒有值的那個)
完成之後會有一個 merged 好的 list,接著將環狀的結構復原 (`prev`被各個佇列的 merge 打亂了,僅有 `next` 為正常順序)
最後把處理好的 list 透過 `list_splice` 交給主要 chain `head` 所屬的 `element`
---
## 研讀論文並改進 dudect
- 較細節研讀筆記放在[另外的筆記](https://hackmd.io/@chiangkd/dudect-study)
此節根據〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉修正測試案例 `trace-17-complexity` 檢測程式碼不會常數執行時間的問題,在 `trace-17-complexity` 測試案例中會將 `option simulation` 打開,並執行 insert head (`ih`)、insert tail (`it`)、remove head (`rh`)、remove tail (`rt`) 指令,而在 `qtest.c` 中相對應的 `do_it`、`do_ih`、`do_remove` 都有對應 `simutlation` 開啟狀態下的行為。
### 解釋 simulation 模式
>下述程式碼已在 [#127](https://github.com/sysprog21/lab0-c/commit/bdcfae37505e671c5ba028b8d95fe0e7e40b2afc) 更新
首先查看 `do_ih`
```c
if (simulation) {
if (argc != 1) {
report(1, "%s does not need arguments in simulation mode", argv[0]);
return false;
}
bool ok = is_insert_head_const();
if (!ok) {
report(1,
"ERROR: Probably not constant time or wrong implementation");
return false;
}
report(1, "Probably constant time");
return ok;
}
```
在 `simulation` 下,對應到三種結果
- 不需要 argument
- 執行測試,且 probably not constant time
- 執行測試,且 probably constant time
是否為 constant time 由 `is_insert_head_const()` 函式回傳判斷,
`is_##x##_const` 系列函式由前置處理器 [concatenation](https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html) 處理,其中 `insert_head`, `insert_tail`, `remove_head`, `remove_tail` 也都是由前置處理器處理。
:::warning
這技巧稱為 [X macro](https://en.wikipedia.org/wiki/X_Macro)
:notes: jserv
>Got it! Thanks
:::
`constant.c` 中的 `measure` 函式負責判斷 `ih`、`it`、`rh`、`rt` 是否有正常運作 (透過 size 有沒有隨著 insert 以及 remove 正常的變動)
`fixture.c` 中的 `report` 函式則負責測量是否為 constant time
```c
/* Definitely not constant time */
if (max_t > t_threshold_bananas)
return false;
/* Probably not constant time. */
if (max_t > t_threshold_moderate)
return false;
/* For the moment, maybe constant time. */
return true;
```
此處流程可以參照 [preparaz/dudect](https://github.com/oreparaz/dudect),中的 **Checking your code for constant time** 節
>`dudect` is distributed as a single-file library for easy building. Steps:
>- Copy `dudect.h` to your include directories
>- Add #include "`dudect.h`" from your source files.
>- See [this minimal example](https://github.com/oreparaz/dudect/blob/master/examples/simple/example.c) for a fully contained example. You'll need to write the following functions:
> - `do_one_computation()`,
> - `prepare_inputs()` and
> - call `dudect_main()` from your main function
### Student's t-distribution 及程式實作原理
實作放在 `ttest.c` 相關函式,按照公式計算 `t` 值
$$
t = \frac{\bar{X_0} - \bar{X_1}}{\sqrt{\frac{s^2_1}{N_1} + \frac{s^2_2}{N_2}}}
$$
其中 `t` 值越小代表兩組分佈越接近,注意這裡的 $\bar{X_i}$ 指的是 [sample mean](https://en.wikipedia.org/wiki/Sample_mean_and_covariance)
首先在進入定義全域變數 `t_constext *t`,結構體定義為
```c
typedef struct {
double mean[2];
double m2[2];
double n[2];
} t_context_t;
```
在每一次測試時 (預設測試 10 次 (`TEST_TRIES=10`)),首先呼叫 `init_once()`,在 `init_once()` 中會將 `t` 的各個元素初始化。
接著根據不同的 `mode` (不同的 case 有不同的 mode number) ,進入 `doit` ,在 `prepare_input` 之後進入 `measure` 函式, `measure` 函式根據 `q_size` 判斷 `ih`, `it`, `rh`, `rt` 等操作有沒有順利進行,也計算函式執行時間,以 `insert_head` 為例:
```c
before_ticks[i] = cpucycles();
dut_insert_head(s, 1);
after_ticks[i] = cpucycles();
```
函式若正常執行回傳 `true` 給 `ret`,且更新了 `before_ticks` 以及 `after_ticks` 交給 `differentiate` 進行計算 `exec_time[i] = after_ticks[i] - before_ticks[i]` ,其中 `i` 為每次 `test` 進行的 measurement 次數,預設為 150 次。
接著將計算出的 `exec_time` 及 `classes` 送入 `updata_statistics` ,函式內透過 `t_push` 進行 t-test ,`t_push` 需要的參數包含
- `t_context` 本體 `t`
- 計算出的 `difference` 也就是對應的 `exec_times[i]`
- 對應的 `classes[i]`
在 [dudect.h](https://github.com/oreparaz/dudect/blob/master/src/dudect.h) 原始碼中有提到可以參考《The Art of Computer Programming, Volume 2 : Seminumerical Algorithms》 (待研究),看起來是透過取新的 execution time 與當前平均值的差為變化量 (`delta`) 而新的平均值就會是舊平均值加上變化量的平均值 (`delta/n`),可以避免每次都將所有數值相加取平均的過程,而 `m2` 為變異數尚未平均的值,在後續 `t_compute` 會使用到,每次的 `m2` 更新為舊 `m2` 加上 `delta` 乘上這次的 execution time 與更新過後的平均值的差。
在 `ttest.c` 中
```c
void t_push(t_context_t *ctx, double x, uint8_t class)
{
assert(class == 0 || class == 1);
ctx->n[class]++;
double delta = x - ctx->mean[class];
ctx->mean[class] = ctx->mean[class] + delta / ctx->n[class];
ctx->m2[class] = ctx->m2[class] + delta * (x - ctx->mean[class]);
}
```
之後進入 `report` 函式透過 `t_compute` 及取絕對值的 `fabs` 計算 `max_t`, `max_t` 數值就會根據 `t_threshold_xx` 的值來判斷程式是否 constant time,這裡程式的實作如同上述給定的公式
$$
t = \frac{\bar{X_0} - \bar{X_1}}{\sqrt{\frac{s^2_1}{N_1} + \frac{s^2_2}{N_2}}}
$$
再回傳 `t` 值之後會在透過 `fabs` 取絕對值。
### 修改以達成 constant time
當前 `lab0-c` 並未實作 percentile 功能,也就是沒有將測量誤差給 crop 掉,將 [dudect.h](https://github.com/oreparaz/dudect/blob/master/src/dudect.h) 中的 percentile 引入後即可順利通過 `trace-17-complexity`
[dudect.h](https://github.com/oreparaz/dudect/blob/master/src/dudect.h) 中的結構體定義和 `lab0-c` 有一些不一樣,原專案中使用 `dudect_state_t` 將 `ttest_ctx_t` (等效 `lab0-c` 中的 `t_context_t`) 及執行時間、輸入資料、等必要資訊定義在一起,但在 `lab0-c` 都是拆開的,在這邊引入 `percentile` 至 `t_context_t` 中
```diff
typedef struct {
double mean[2];
double m2[2];
double n[2];
+ int64_t *percentiles;
} t_context_t;
```
詳細改動在 [commit](https://github.com/chiangkd/lab0-c/commit/d014538e300e54d5e21e586f23e3e10a561269ee) 中。
---
## 研讀 list_sort.c 並引入專案
- 研讀筆記放在[這裡](https://hackmd.io/erWlfVMfQqyUe9JVbOLlBA?view#%E7%A0%94%E8%AE%80-Linux-%E6%A0%B8%E5%BF%83%E5%8E%9F%E5%A7%8B%E7%A8%8B%E5%BC%8F%E7%A2%BC%E7%9A%84-list_sortc)
將 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 以及 [list_sort.h](https://github.com/torvalds/linux/blob/master/include/linux/list_sort.h) 引入專案,並新增 `makefile` 的 dependency 以及把無關的 include header 移除
```diff
OBJS := qtest.o report.o console.o harness.o queue.o \
random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \
shannon_entropy.o \
- linenoise.o web.o
+ linenoise.o web.o list_sort.o
```
`list_sort.h`
```diff
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_LIST_SORT_H
#define _LINUX_LIST_SORT_H
-#include <linux/types.h>
+#include "stdint.h"
+#include "list.h"
-struct list_head;
typedef int __attribute__((nonnull(2,3))) (*list_cmp_func_t)(void *,
const struct list_head *, const struct list_head *);
__attribute__((nonnull(2,3)))
void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp);
#endif
```
```diff
static void merge_final(void *priv, list_cmp_func_t cmp, struct list_head *head,
struct list_head *a, struct list_head *b)
{
struct list_head *tail = head;
- u8 count = 0;
+ uint8_t count = 0;
...
}
```
編譯後發現 `likely` 以及 `unlikely` 沒有定義,參考 [linux/compiler.h](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h),其中的 `__builtin_expect()` 是 gcc 的 built-in function,用來將 branch 的相關資訊提供給 compiler,讓其對程式碼改變分支的順序
- 參考 [6.59 Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)
```c
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
```
- 參考連結 [Linux Kernel 慢慢學: likely and unlikely macro](https://meetonfriday.com/posts/cecba4ef/)
在 `list_sort` 的 prototype 中需要三個參數,`priv`, `head`,以及 `cmp`
- `head` 為我們要輸入的 queue 的 `head`
- `cmp` 為 compare function,作為函式指標傳入,根據註解
>The comparison function @cmp must return > 0 if @a should sort after
@b (“@a > @b” if you want an ascending sort), and <= 0 if @a should sort before @b or their original order should be preserved. It is always called with the element that came first in the input in @a, and list_sort is a stable sort, so it is not necessary to distinguish the @a < @b and @a == @b cases.
1. `cmp` return > 0 (a 排在 b 的後面)
2. `cmp` return <=0 (a 排在 b 的前面或保留原本排列)
3. `list_sort` 是一個 [stable sort](https://en.wikipedia.org/wiki/Sorting_algorithm#Stability),故不用區分小於 0 或等於 0
不確定這邊的 `priv` 是要幹麻的,不過根據宣告 `priv` 可以為 `NULL`
```c
__attribute__((nonnull(2,3)))
void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp)
```
參考 `do_sort` 新增 `do_lsort` 代表 linux 核心原始程式碼的 `list_sort` 並定義 `cmp`
```c
int cmp(void *priv, const struct list_head *a, const struct list_head *b)
{
element_t *a_ele = list_entry(a, element_t, list); // get mother element
element_t *b_ele = list_entry(b, element_t, list);
return strcmp(a_ele->value, b_ele->value) < 0 ? 0 : 1;
}
bool do_lsort(int argc, char *argv[])
{
if (argc != 1) {
report(1, "%s takes no arguments", argv[0]);
return false;
}
int cnt = 0;
if (!current || !current->q)
report(3, "Warning: Calling sort on null queue");
else
cnt = q_size(current->q);
error_check();
if (cnt < 2)
report(3, "Warning: Calling sort on single node");
error_check();
set_noallocate_mode(true);
if (current && exception_setup(true))
list_sort(NULL, current->q, cmp);
exception_cancel();
set_noallocate_mode(false);
bool ok = true;
if (current && current->size) {
for (struct list_head *cur_l = current->q->next;
cur_l != current->q && --cnt; cur_l = cur_l->next) {
/* Ensure each element in ascending order */
/* FIXME: add an option to specify sorting order */
element_t *item, *next_item;
item = list_entry(cur_l, element_t, list);
next_item = list_entry(cur_l->next, element_t, list);
if (strcmp(item->value, next_item->value) > 0) {
report(1, "ERROR: Not sorted in ascending order");
ok = false;
break;
}
}
}
q_show(3);
return ok && !error_check();
}
```
**提供新命令** `lsort`
在 `qtest.c` 的 `console_init()` 中新增命令
```c
ADD_COMMAND(lsort, "Sort queue in ascending order provided by linux kernel", "");
```
結果呈現
```
cmd> new
l = []
cmd> ih RAND 10
l = [xajlhbj zblfv veqfmhw kuwzmcl qzzsyenvi xfsgs bocazny saskszhgd xqjrd igiecysjj]
cmd> lsort
l = [bocazny igiecysjj kuwzmcl qzzsyenvi saskszhgd veqfmhw xajlhbj xfsgs xqjrd zblfv]
cmd> shuffle
l = [bocazny kuwzmcl xfsgs igiecysjj veqfmhw saskszhgd zblfv qzzsyenvi xajlhbj xqjrd]
cmd> sort
l = [bocazny igiecysjj kuwzmcl qzzsyenvi saskszhgd veqfmhw xajlhbj xfsgs xqjrd zblfv]
```
`git commit` 的時候會被擋下來
```shell
list_sort.c:14:23: style: Variable 'head' is not assigned a value. [unassignedVariable]
struct list_head *head, **tail = &head;
^
```
考慮到不要修正 `list_sort.c` 的原始碼,用 `cppcheck-suppress` 跨過去
```c
// cppcheck-suppress unassignedVariable
struct list_head *head, **tail = &head;
```
### 效能比較
使用 `perf` 進行效能比較,參考 [Linux 效能分析工具: Perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial)
```shell
perf stat --repeat 5 -e instructions,cycles ./qtest -f traces/trace-sort.cmd
```
定義測資
```shell
option fail 0
option malloc 0
new
ih RAND 500000
time
<sort>
time
```
- `<sort>` 可以為原先 `qtest.c` 中撰寫的 `do_sort` 或是引入的 `do_lsort`,透過 `time` 命令獲取 `sort` 執行的時間,設定 `--repeat 5` 重複五次,並透過 `perf` 拿到 `instructions` 以及 `cycles` 的數據
**自行撰寫的 `q_sort`**
| **q_sort** | time |
| ---------- | ----- |
| 1 | 0.487 |
| 2 | 0.477 |
| 3 | 0.488 |
| 4 | 0.486 |
| 5 | 0.529 |
| Instructions | Cycles |
| ------------ | ------------ |
| 20,9111,6747 | 36,2842,8338 |
**list_sort**
| **q_sort** | time |
| ---------- | ----- |
| 1 | 0.411 |
| 2 | 0.417 |
| 3 | 0.423 |
| 4 | 0.501 |
| 5 | 0.460 |
| Instructions | Cycles |
| ------------ | ------------ |
| 21,4157,1467 | 34,1147,9158 |
雖然每次執行這個測試案例時,Instructions 和 Cycles 的數量會些微變動,但整理來看
- Linux kernel 的 `list_sort` 指令較多,Cycles 較少
---
## 在 qtest 提供新的命令 shuffle
- [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle)
```c
void q_shuffle(struct list_head *head)
{
if (!head || list_empty(head))
return;
int size = q_size(head);
// shuffle
for (int i = 0; i < size;) { // not iterate i , iterate size
struct list_head *start = head->next;
int rand_idx = rand() % size; // random number in range 0~ (size-1)
for (int j = 0; j < rand_idx; j++) {
start = start->next;
}
list_del(start);
list_add_tail(start, head);
size--;
}
return;
}
```
選取一個隨機數,將該位置元素取出後放到尾端,舉例來說
| 隨機範圍 | 隨機數 | 原始數據 | 結果 |
| -------- | ------ | -------- | ---- |
| | | 12345 | |
| 1~5 | 4 | 1235 | 4 |
| 1~4 | 3 | 125 | 34 |
| 1~3 | 1 | 25 | 134 |
| 1~2 | 1 | 5 | 2134 |
### 在命令直譯器中新增命令 shuffle
在 `qtest.c` 中的 `console_init()` 新增 `shuffle` 指令
```c
ADD_COMMAND(shuffle, "Shuffle queue", "");
```
查看其巨集定義
```c
#define ADD_COMMAND(cmd, msg, param) add_cmd(#cmd, do_##cmd, msg, param)
```
此巨集會將 `cmd` 命令的行為去透過巨集展開 `do_##cmd` 去呼叫對應 handler ,如其他命令 `new` 會去呼叫 `do_new` 函式,故在此定義 `do_shuffle` 函式來 handle `shuffle` 命令
```c
void q_shuffle(struct list_head *head);
static bool do_shuffle(int argc, char *argv[])
{
if (argc != 1) {
report(1, "%s takes no argu,emts", argv[0]); //return function error
return false;
}
if (!current || !current->q)
report(3, "Warning: Calling shuffle on null queue");
error_check();
if (exception_setup(true))
q_shuffle(current->q);
exception_cancel();
set_noallocate_mode(false);
q_show(3);
return !error_check();
}
```
參考其他函式 handler 的寫法,有些函式內有定義 `bool ok`,有些則無,觀察到 `ok` 都用在佇列長度有更改或者需要判斷佇列是否為空的情形,所以 `reverse`,`swap` 中不需要 `ok` 來處理佇列長度改變,且對應的 `do_reverse` 以及 `do_swap` 中都有相應的 `if(!head)` 來處理空佇列的問題,綜上所述 `do_shuffle` 不需要有 `bool ok` 定義。
```shell
[!] You are not allowed to change the header file queue.h or list.h
```
- 無法將 `q_shuffle` 宣告加在 `queue.h` 所以就放在 `do_shuffle` 的上面
```shell
cmd> new
l = []
cmd> it a
l = [a]
cmd> it b
l = [a b]
cmd> it c
l = [a b c]
cmd> it d
l = [a b c d]
cmd> it e
l = [a b c d e]
cmd> shuffle
l = [a d b c e]
cmd> shuffle
l = [c b d e a]
cmd> shuffle
l = [c e a b d]
```
---
## 運用 Valgrind 排除 qtest 實作的記憶體錯誤
首先嘗試用 `valgrind` 跑跑看 `trace-01-ops.cmd` 測資
```shell
valgrind ./qtest -f ./traces/trace-01-ops.cmd
```
```shell
==10230== 32 bytes in 1 blocks are still reachable in loss record 1 of 2
==10230== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==10230== by 0x10CE33: do_new (qtest.c:147)
==10230== by 0x10E099: interpret_cmda (console.c:181)
==10230== by 0x10E64E: interpret_cmd (console.c:201)
==10230== by 0x10EA4F: cmd_select (console.c:610)
==10230== by 0x10F33B: run_console (console.c:705)
==10230== by 0x10D48B: main (qtest.c:1307)
==10230==
==10230== 56 bytes in 1 blocks are still reachable in loss record 2 of 2
==10230== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==10230== by 0x10F3AF: test_malloc (harness.c:133)
==10230== by 0x10F7B4: q_new (queue.c:17)
==10230== by 0x10CE6C: do_new (qtest.c:151)
==10230== by 0x10E099: interpret_cmda (console.c:181)
==10230== by 0x10E64E: interpret_cmd (console.c:201)
==10230== by 0x10EA4F: cmd_select (console.c:610)
==10230== by 0x10F33B: run_console (console.c:705)
==10230== by 0x10D48B: main (qtest.c:1307)
```
依據[網站](https://valgrind.org/docs/manual/faq.html#faq.deflost)及作業講解敘述,Valgrind 有 5 種 memory leak
- definitely lost: 真的 memory leak
- indirectly lost: 間接的 memory leak,structure 本身發生 memory leak,而內部的 member 如果是 allocate 的出來的,一樣會 memory leak,但是只要修好前面的問題,後面的問題也會跟著修復。
- possibly lost: allocate 一塊記憶體,並且放到指標 ptr,但事後又改變 ptr 指到這塊記憶體的中間
- still reachable: 程式結束時有未釋放的記憶體,不過卻還有指標指著,通常會發生在 global 變數,即便是在所用的函式庫裡頭配置的記憶體,也可偵測到,這也是動態分析手法的優勢。
這裡發生的 still reachable 在 `do_new` 及 `test_malloc` 中,但是如果直接執行,`qtest` 且逐行輸入命令不會有這個問題。
沒什麼頭緒,參考 [yanjiew1](https://hackmd.io/@yanjiew/linux2023q1-lab0#Valgrind-%E8%88%87-Address-Sanitizer-%E8%A8%98%E6%86%B6%E9%AB%94%E6%AA%A2%E6%9F%A5),上述問題已在 [#121](https://github.com/sysprog21/lab0-c/commit/c7d31497c5845bc0af308bf4601e7636c18f0153), [#122](https://github.com/sysprog21/lab0-c/commit/9dcf3ed44617b2d530c4365dbc60c4437e38e622) 被修正
:::warning
TODO: 搞懂別人做了什麼
:::
## 透過 Massif 視覺化 "simulation" 過程中的記憶體使用量
選用 `trace-17-complexity.cmd`
先透過 Valgrind 產生 massif 檔案
```
valgrind --tool=massif ./qtest -f traces/trace-eg.cmd
```
再使用 `massif-visualizer` 查看
```
massif-visualizer ./massif.out.<number>
```
![](https://i.imgur.com/qL8qQOq.png)
- 這邊參考 [alanjiang85](https://hackmd.io/@alanjian85/lab0-2023#%E9%80%8F%E9%81%8E-Massif-%E5%88%86%E6%9E%90%E8%A8%98%E6%86%B6%E9%AB%94%E4%BD%BF%E7%94%A8%E9%87%8F) 的說明改用 `trace-eg.cmd` 進行分析
- >alanjiang85: 要注意的是,挑選適當的測試資料時要避免效能類型的測試,因為以 Valgrind 執行測試程式會比較慢,容易導致出現超過時間限制的狀況。
## Reference
:::warning
參考資料不是讓你發表致謝詞的地方
:notes: jserv
> Got it, Thanks!
:::
- [komark06 PR#111](https://github.com/sysprog21/lab0-c/commit/ae7c2985b583126dd62ae6378941a93b2f73785f)
- [ISO/IEC 9899:1999](https://zh.wikipedia.org/zh-tw/C99)
- [3.11 Options That Control Optimization](https://gcc.gnu.org/onlinedocs/gcc-12.2.0/gcc/Optimize-Options.html#Optimize-Options)
- [學習 Shell Scripts](https://linux.vbird.org/linux_basic/centos7/0340bashshell-scripts.php)
- [你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor?type=view)
- [ANSI escape code](https://en.wikipedia.org/wiki/ANSI_escape_code)