# 2021q1 Homework1 (quiz1)
contributed by < `D4nnyLee` >
> [quiz1 題目連結](https://hackmd.io/@sysprog/linux2021-quiz1)
## 重新回答問題並解釋程式運作原理
### 各個重要函式的功能
#### `list_add_node_t`
:::info
這個函式的功能則是將一個新的節點加到一個 linked list 的開頭。
:::
```graphviz
digraph {
subgraph objects {
rank = same
node [shape = box];
node_t [label = "new\nnode"];
head_node [label = "head\nnode"];
many [label = "...", shape = none]
null [label = "NULL" shape = none]
head_node -> node1 -> many -> nodex -> null
}
subgraph pointers {
node [shape = ellipse]
_node_t [label = "node_t"]
_list [label = "*list"]
__list [label = "list"]
}
_node_t -> node_t
__list -> _list -> head_node
}
```
圖中方形的圖像為實體物件,而圓形的則是指標。
上圖為將代入的參數視覺化之後的預期結果,從上圖中可以看到:
* `node_t` 為指向一個即將新增到 linked list 裡的新節點 `new node` 的指標
* `list` 為一個指標的指標, `**list` 為一個 linked list 的開頭節點
具體執行的步驟如下:
1. 新節點指向舊開頭
```cpp
node_t->next = *list;
```
這段程式碼會新增一條從 `new node` 指向 `head node` 的邊,如下圖:
```graphviz
digraph {
subgraph objects {
rank = same
node [shape = box];
node_t [label = "new\nnode"];
head_node [label = "head\nnode"];
many [label = "...", shape = none]
null [label = "NULL" shape = none]
node_t -> head_node [color = red]
head_node -> node1 -> many -> nodex -> null
}
subgraph pointers {
node [shape = ellipse]
_node_t [label = "node_t"]
_list [label = "*list"]
__list [label = "list"]
}
_node_t -> node_t
__list -> _list -> head_node
}
```
2. 更新開頭節點的位址
因為此函式的使用方法為先有一個 `node_t *head_ptr` 變數儲存開頭節點的位址,然後在呼叫函式的時候傳入 `&head_ptr` ,所以可以在此函式裡面就直接更新到外部的變數。
此技巧可以讓在使用這個函式時不用特地去維護 `head_ptr` ,因此用起來更加方便。
這邊利用以下程式碼去更新外部的 `head_ptr` 變數
```cpp
*list = node_t;
```
造成的結果如下圖:
```graphviz
digraph {
subgraph objects {
rank = same
node [shape = box];
node_t [label = "new\nnode"];
head_node [label = "head\nnode"];
many [label = "...", shape = none]
null [label = "NULL" shape = none]
node_t -> head_node -> node1 -> many -> nodex -> null
}
subgraph pointers {
node [shape = ellipse]
_node_t [label = "node_t"]
_list [label = "*list"]
__list [label = "list"]
}
_node_t -> node_t
__list -> _list
_list -> node_t [color = red]
}
```
這邊的 `*list` 實際上就是外部的 `head_ptr` 變數
#### `list_concat`
:::info
根據函式名稱可以知道這個函式的功能是將兩個 linked list 連接在一起。
:::
將參數視覺化:
```graphviz
digraph {
subgraph objects {
node [shape = box]
head_node [label = "head\nnode"]
many [label = "...", shape = none]
null [label = "NULL", shape = none]
{
rank = same
head_node -> node1 -> many -> nodex -> null
}
}
subgraph pointers {
node [shape = ellipse]
_left [label = "*left"]
left -> _left -> head_node
}
}
```
```graphviz
digraph {
subgraph objects {
node [shape = box]
head_node [label = "head\nnode"]
many [label = "...", shape = none]
null [label = "NULL", shape = none]
{
rank = same
head_node -> node1 -> many -> nodex -> null
}
}
subgraph pointers {
node [shape = ellipse]
right -> head_node
}
}
```
可以看到這個函式的一部分程式碼被替換成 `LLL` ,而 `LLL` 的答案應該為
`left = &((*left)->next)` 。
以下簡略解釋其他三個選項的錯誤原因:
* `left = left->next`
會造成編譯錯誤,因為 `node_t **` 指向的 `node_t *` 物件並沒有 `next` 這個欄位。
* `left = (*left)->next`
`left` 的型別和 `(*left)->next` 的型別不符。
* `*left = (*left)->next`
雖然這個選項可以正常編譯,但是試著將 `LLL` 替換為這個選項之後:
```cpp
while (*left)
*left = (*left)->next;
*left = right;
```
可以看到從頭到尾都只是一直修改外部的 `*left` 變數,且沒有什麼實質的作用。
更慘的是這可能會讓程式無法存取到原本在左半部的 linked list 裡的節點,也無法釋放那些空間,而造成記憶體的浪費。
在這個函式中,也有利用到指標的指標來實作,而這邊可以看到使用這個技巧的另一個好處,就是不用特判空 list 的情況。
如果 `left` 的型別是 `node_t *` ,雖然也可以做到同樣的功能,但是就需要分成 `left` 為 NULL 以及 `left` 不為 NULL 的情況來撰寫。
利用指標的指標的技巧就可以用相同的步驟處理這兩種情況。
#### `quicksort`
:::info
將 linked list 中的節點依照 `value` **由小到大**來排序
:::
實作可以分成以下四個步驟:
1. 選出 pivot
```cpp
node_t *pivot = *list;
int value = pivot->value;
node_t *p = pivot->next;
pivot->next = NULL;
```
這邊只有單純的選第一個節點當作 `pivot` ,並且把 `pivot` 與剩下的節點分離,形成兩個獨立的 linked list 。
2. 將剩下的節點分成兩個 linked list
```cpp
node_t *left = NULL, *right = NULL;
while (p) {
node_t *n = p;
p = p->next;
list_add_node_t(n->value > value ? AAA : BBB, n);
}
```
依據 `pivot` 的值分成 `left`, `right` 兩個 linked list ,因為是**由小到大**排序,所以 `AAA` 應該選 `&right` ,且 `BBB` 為 `&left` 。
這段程式碼執行完之後 `left` 為所有 `value` 小於等於 `pivot` 的節點, `right` 則是其他剩下的 (所有 `value` 大於 pivot 的) 節點。
3. 分別排序小的 linked list
```cpp
quicksort(&left);
quicksort(&right);
```
利用遞迴呼叫,將兩個小的 linked list 進行排序。
4. 合併結果
```cpp
node_t *result = NULL;
list_concat(&result, left);
CCC;
*list = result;
```
在合併之前,原本的 linked list 總共被分為三個部分,分別為 `left`, `pivot` 以及 `right` 。
因為 `left`, `right` 都已經排序完畢,且 `left` 裡的所有節點都小於等於 `pivot` , `right` 的都大於 `pivot` ,這就代表依照 `left`, `pivot`, `right` 的順序把 linked list 連接起來也會是個排序好的 linked list 。
因此 `CCC` 應該選 `list_concat(&result, pivot); list_concat(&result, right)` 。
### 修正 `rand()` 問題
題目中的測試程式執行多次之後會發現明明有用到 `rand()` 來隨機產生排序前的值,但是每次出來的結果都是一模一樣的。
根據 [man page](https://man7.org/linux/man-pages/man3/rand.3.html) 中的敘述:
> The srand() function sets its argument as the seed for a new
> sequence of pseudo-random integers to be returned by rand().
> These sequences are repeatable by calling srand() with the same
> seed value.
>
> If no seed value is provided, the rand() function is
> automatically seeded with a value of 1.
只要給相同的 `seed` ,就可以讓 `rand()` 回傳相同的順序。
而如果沒有事先用 `srand()` 函式設定 `seed` 的話 `seed` 的預設值為 `1` 。
這也就代表只要沒有呼叫 `srand()` ,每次執行程式時 `rand()` 的行為都會一樣。
解決方法也很簡單,就是多呼叫 `srand()` 就可以了。
## 避免遞迴呼叫
### 開發歷程
為了減少 `rand()` 所帶來的影響,以下測試都會先將前面[修正 `rand()` 問題](#修正-rand-問題)所提到的 `srand()` 註解掉。
#### stack-use-after-scope
先參照 [Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/) 試著實做出非遞迴版本的 quicksort :
```cpp=
void quicksort(node_t **list)
{
if (!*list)
return;
#define MAX_LEVEL 300
node_t **beg[MAX_LEVEL], *end[MAX_LEVEL];
beg[0] = list;
end[0] = NULL;
for (int i = 0; i >= 0;) {
if (*beg[i] == end[i])
--i;
else {
node_t *pivot = *beg[i], *n = pivot->next;
int val = pivot->value;
pivot->next = NULL;
node_t *left = NULL, *right = NULL;
while (n != end[i]) {
node_t *tmp = n;
n = n->next;
list_add_node_t(tmp->value > val ? &right : &left, tmp);
}
list_concat(&right, end[i]);
list_concat(&pivot, right);
list_concat(&left, pivot);
*beg[i] = left;
beg[i + 1] = &right;
end[i + 1] = end[i];
end[i++] = pivot;
}
}
#undef MAX_LEVEL
}
```
將上述程式編譯後執行時,很不幸的發生了 segmentation fault :
```shell
$ ./test
NOT IN ORDER : 1016 84 706 124 326 483 763 498 939 186 205 809 236 74 255 81 115 105 966 359
zsh: segmentation fault ./test
```
在編譯參數中多加了 `-fsanitize=address -g` 之後再執行一次之後可以看到:
```shell
NOT IN ORDER : 1016 84 706 124 326 483 763 498 939 186 205 809 236 74 255 81 115 105 966 359
=================================================================
==3477==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7ffeddf2e660 at pc 0x55ef0a448b2a bp 0x7ffeddf2e5c0 sp 0x7ffeddf2e5b8
READ of size 8 at 0x7ffeddf2e660 thread T0
#0 0x55ef0a448b29 in quicksort /home/d4nnylee/Documents/linux_kernel_internal/quiz1/list.c:59
#1 0x55ef0a44859a in main /home/d4nnylee/Documents/linux_kernel_internal/quiz1/test.c:46
#2 0x7efc00f77d09 in __libc_start_main ../csu/libc-start.c:308
#3 0x55ef0a448199 in _start (/home/d4nnylee/Documents/linux_kernel_internal/quiz1/test+0x1199)
Address 0x7ffeddf2e660 is located in stack of thread T0 at offset 96 in frame
#0 0x55ef0a4488d2 in quicksort /home/d4nnylee/Documents/linux_kernel_internal/quiz1/list.c:47
This frame has 5 object(s):
[32, 40) 'pivot' (line 62)
[64, 72) 'left' (line 66)
[96, 104) 'right' (line 66) <== Memory access at offset 96 is inside this variable
[128, 8128) 'beg' (line 53)
[8384, 16384) 'end' (line 53)
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-scope /home/d4nnylee/Documents/linux_kernel_internal/quiz1/list.c:59 in quicksort
Shadow bytes around the buggy address:
0x10005bbddc70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005bbddc80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005bbddc90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005bbddca0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005bbddcb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10005bbddcc0: f1 f1 f1 f1 f8 f2 f2 f2 f8 f2 f2 f2[f8]f2 f2 f2
0x10005bbddcd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005bbddce0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005bbddcf0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005bbddd00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x10005bbddd10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==3477==ABORTING
```
從上面的錯誤訊息可以看到,這是因為發生了 `stack-use-after-scope` 的錯誤。
問題的主因為上述程式碼第 33 行的地方,這邊我是將 `&right` 存到 `beg[i + 1]` 且 `beg[i + 1]` 有可能會在下一次的迴圈被 dereference 。
但是 `right` 為 for 迴圈內的區域變數, scope 的範圍只有在一次的迴圈,並不會延續到下一次的迴圈,因此上述 `beg[i + 1] = &right` 的寫法就會出錯。
我的解法為將 `&right` 改成 `&pivot->next` 。
這兩者的共通點為 dereference 之後都是一個指向右邊 linked list 開頭的指標,
而不同的點是 `pivot->next` 為待排序節點中的變數,因此 scope 並不限定在 `quicksort()` 內,所以不會發生前面的 `stack-use-after-scope` 錯誤。
最終修正:
```diff
- beg[i + 1] = &right;
+ beg[i + 1] = &pivot->next;
```
#### No-Fail Version
Quicksort 的 Worst-Case 非常經典,就是接近排序完成的序列。
這樣的情況時間複雜度會變成 $O(N^2)$ ,$N$ 為序列長度。
且遞迴深度也會變成 $O(N)$ ,容易造成 stack 空間不夠用。
而在這個非遞迴實做中,則會造成 `i >= MAX_LEVEL` 情況。
將 `main()` 做以下修改:
```diff
int main(int argc, char **argv)
{
// srand(time(NULL));
- size_t count = 20;
+ size_t count = 2000;
node_t *list = NULL;
while (count--)
- list = list_make_node_t(list, rand() % 1024);
+ list = list_make_node_t(list, count);
list_display(list);
quicksort(&list);
list_display(list);
if (!list_is_ordered(list))
return EXIT_FAILURE;
list_free(&list);
return EXIT_SUCCESS;
}
```
測試後也如同預期的發生了 segmentation fault :
```shell
$ ./test
IN ORDER : 0 1 2
...
1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999
zsh: segmentation fault ./test
```
在 [Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/) 中提到的 No-Fail Version 的原理是先將原本的 `[beg[i], end[i])` 範圍分成兩個部份後, 先從比較短的部份開始做,並使的 `i` 不會超過 `MAX_LEVEL`。
修改如下:
```diff
+ int cnt_left = 0, cnt_right = 0;
node_t *left = NULL, *right = NULL;
while (n != end[i]) {
node_t *tmp = n;
n = n->next;
- list_add_node_t(tmp->value > val ? &right : &left, tmp);
+
+ if (tmp->value > val) {
+ ++cnt_right;
+ list_add_node_t(&right, tmp);
+ } else {
+ ++cnt_left;
+ list_add_node_t(&left, tmp);
+ }
}
```
```diff
beg[i + 1] = &pivot->next;
end[i + 1] = end[i];
end[i++] = pivot;
+
+ if (cnt_left < cnt_right) {
+ node_t **beg_swap = beg[i];
+ beg[i] = beg[i - 1];
+ beg[i - 1] = beg_swap;
+
+ node_t *end_swap = end[i];
+ end[i] = end[i - 1];
+ end[i - 1] = end_swap;
+ }
```
### 小優化
在合併 `left`, `pivot`, `right` 的地方我稍微調換了一下順序。
照著原本題目的寫法,最直觀的用法為:
```cpp
*beg[i] = left;
list_concat(beg[i], pivot);
list_concat(beg[i], right);
list_concat(beg[i], end[i]);
```
但是這個寫法會多跑了許多不必要的迴圈。
舉個例子:
```graphviz
digraph {
subgraph pointers {
node [shape = ellipse]
{
rank = same
left
pivot
right
}
}
subgraph objects {
node [shape = box]
{
rank = same
node_1 -> node_2 -> node_3
node_4
node_5
}
}
left -> node_1
pivot -> node_4
right -> node_5
}
```
來算一下 `list_concat()` 中的 while 迴圈執行次數:
* `list_concat(beg[i], pivot)` : 3 次
* `list_concat(beg[i], right)` : 4 次
* `list_concat(beg[i], end[i])` : 5 次
總共跑了 12 次迴圈
較好的合併方法為:
```cpp
list_concat(&right, end[i]);
list_concat(&pivot, right);
list_concat(&left, pivot);
*begin = left;
```
再算一次 while 迴圈的執行次數:
* `list_concat(&right, end[i])` : 1 次
* `list_concat(&pivot, right)` : 1 次
* `list_concat(&left, pivot)` : 3 次
總共只有執行 5 次,可以看到調換順序之後可以減少無謂的操作。
以下為利用 `perf stat` 來測試兩者排序 2 * 10^5^ 個節點的效率的結果:
重排前:
```
Performance counter stats for './worst':
606.27 msec task-clock # 0.818 CPUs utilized
37 context-switches # 0.061 K/sec
1 cpu-migrations # 0.002 K/sec
1,613 page-faults # 0.003 M/sec
2,030,268,114 cycles # 3.349 GHz
1,684,564,030 instructions # 0.83 insn per cycle
247,652,632 branches # 408.483 M/sec
1,563,034 branch-misses # 0.63% of all branches
0.740974650 seconds time elapsed
0.591300000 seconds user
0.015873000 seconds sys
```
重排後:
```
Performance counter stats for './better':
365.69 msec task-clock # 0.823 CPUs utilized
8 context-switches # 0.022 K/sec
0 cpu-migrations # 0.000 K/sec
1,615 page-faults # 0.004 M/sec
1,206,934,798 cycles # 3.300 GHz
1,345,761,978 instructions # 1.12 insn per cycle
205,406,361 branches # 561.698 M/sec
1,209,013 branch-misses # 0.59% of all branches
0.444207927 seconds time elapsed
0.359031000 seconds user
0.008068000 seconds sys
```
可以看到重排後的排序比較省時。