# lab0-c link list sorting 實作
## 開發工具
```shell
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
```
## 針對佇列排序的程式碼實作
### q_sort
實作方式基於buttom up merge sort,一共寫了三個版本和改動linux kernel的list_sort.c來比較有無避免cache thrashing,由於要使用descend去決定遞增遞減,所以我將`q_sort`寫成:
```
void q_sort(struct list_head *head, bool descend)
{
if (!head || list_empty(head))
return;
if (descend)
_q_sort(head, q_descend_cmp);
else
_q_sort(head, q_ascend_cmp);
}
```
`_q_sort`定義如下,並實作對應遞增遞減的排序邏輯,可以藉此簡化排序中判斷遞增遞減的邏輯
`static void _q_sort(struct list_head *head, list_cmp_func_t qcmp);`
### 第一版
> Commit: [ad4d266](https://github.com/sysprog21/lab0-c/commit/ad4d266d95f274282e6f5252d172e3235627c105)
>
我每次都從頭跑完整個`link list`,第一次走訪整個`link list`時,將兩兩相鄰的node合併,第 n 次走訪時將`2^n`個相鄰node合併,直到`2^n`大於`link list`的`size`即完成排序
* 可以發現這版使用大量的int去計算位置與大小,十分的低效,cache thrashing的問題也很嚴重
```
void _q_sort(struct list_head *head, list_cmp_func_t qcmp)
{
if (!head || list_empty(head))
return;
struct list_head *tmp, *l = head, *r = head;
for (int t = 1, len = 2, size = q_size(head); t < size;
t <<= 1, len <<= 1, l = r = head) {
for (int k = 0, i = 0, j = 0; k < size; k += len, i = 0, j = 0) {
for (j = 0; j < t; j++)
r = r->next;
while (i < t && j < len && j + k < size) {
if (qcmp(l->next, r->next)) {
tmp = r->next;
r->next = tmp->next;
tmp->next = l->next;
l->next = tmp;
j++;
} else
i++;
l = l->next;
}
for (; j < len; j++)
r = r->next;
l = r;
}
}
for (l = head->next, l->prev = head; l != head; l = l->next)
l->next->prev = l;
}
```
### 第二版
> Commit: [4b59d29](https://github.com/sysprog21/lab0-c/commit/4b59d2993f0c83fefae7b158360567b154395f82)
>
我參考list_sort.c的操作流程,實作成比較看得懂的版本
### 重點解釋
方便理解可以將整個array視為一顆binary tree的leaves,整個sorting的過程就是Postorder Traversal整個binary tree。
binary tree的內點所代表的位置會是該點子樹最前面的位置,畫出來會像是:
```
[0]
[0] [4]
[0] [2] [4] [6]
[0] [1] [2] [3] [4] [5] [6] [7]
```
Postorder Traversal:
```
0->1->0->2->3->2->0->4->5->4->6->7->6->4->0
```
* 過程中第一次出現的0單純指的就是第0個元素的位置,第二次出現則是代表第0跟第1個元素排序的結果,第三次則是第0,1,2,3個元素排序的結果,依此類推。
* 既然知道要處理的index我們可以存下那些位置的pointers避免花時間去找它們,然而另外開memory去記錄這些pointers實在太浪費,透過觀察合併的操作不需要使用到node的prev指標,所以使用每個node裡面的prev去存這些位置會很大姆指。排序完成要記得把prev補回來。
```
void _q_sort(struct list_head *head, list_cmp_func_t qcmp)
{
if (!head || list_empty(head))
return;
struct list_head *tmp, *l, *r = head, *cur = head, *nxt = head->next;
for (int t = 0; cur->next != head; t++) {
nxt = nxt->next;
int bits = (nxt == head) ? end_bits(t) : t;
for (; bits & 1; bits >>= 1) {
r = cur;
cur = l = cur->prev;
while (l != r && r->next != nxt) {
if (qcmp(l->next, r->next)) {
tmp = r->next;
r->next = tmp->next;
tmp->next = l->next;
l->next = tmp;
}
l = l->next;
}
nxt->prev = (r->next == nxt) ? r : nxt->prev;
}
nxt->prev->prev = cur;
cur = nxt->prev;
}
for (cur = head->next, cur->prev = head; cur != head; cur = cur->next)
cur->next->prev = cur;
}
```
* struct list_head *cur 是用來指向接下來要處理的list_head,*nxt則是記錄尚未處理過的第一個node,`l->next`和`r->next`則是當前做比較的node。
* 這邊的 `end_bits` 目的是將剩餘的link list合併,因為當link list長度不是2^n,我的postorder traversal會在完成前停下所以要將沒完成的一併處理。經過計算可知最後一個位置在處理時會剩下和t二進制裡面1的次數一樣多的link list,故用以下計算處理此例外。
```
int end_bits(int t)
{
int res = 0;
for (; t; t >>= 1)
if (t & 1)
res = (res << 1) | 1;
return res;
}
```
### 第三版
> Commit: [31539a9](https://github.com/sysprog21/lab0-c/commit/31539a9d806aec4c36de2957160ae64e123e2ef4)
>
這版主要是做一點小優化,假設當前比較的l->next比r的後面很多個都大,這樣我們可以把這些要搬到l->next的nodes一次搬過去就可以減少一些搬動的操作。
```diff
while (l != r && r->next != nxt) {
- if (qcmp(l->next, r->next)) {
- tmp = r->next;
+ for (tmp = r; tmp->next != nxt && qcmp(l->next, tmp->next);
+ tmp = tmp->next)
+ ;
+ if (tmp != r) {
+ r->prev = r->next;
r->next = tmp->next;
tmp->next = l->next;
- l->next = tmp;
+ l->next = r->prev;
+ l = tmp;
}
l = l->next;
}
```
## 修改 lib/list_sort.c 並比較
### 將 `list_sort.c` 加入 `lab0-c` 專案
> Commit: [82f45ec](https://github.com/sysprog21/lab0-c/commit/82f45ec799be0007685dbe3caf4d88a7a6a26f31)
將 Linux 核心原始程式碼的 `list_sort.c` 複製到 `lab0-c` 專案中,並且為了成功編譯做了以下修改:
* 刪掉 `EXPORT_SYMBOL(list_sort)`
* 將 `u8` 改成 `uint8_t` 作為 `count`,並加入 `#include <stdint.h>`
* 移除不必要的 `#include`,手動加入 `likely` ,`unlikely`和`list_cmp_func_t` 的定義。
### 效能分析
為了測大數據下的表現,停止超時的功能,[參考](https://hackmd.io/@yanjiew/linux2023q1-lab0)改動執行檔如下:
```
make
cp qtest nolimit_qtest
chmod u+x nolimit_qtest
sed -i "s/alarm/isnan/g" nolimit_qtest
```
執行 `./nolimit_qtest` 並使用以下的命令來分析排序效能:
```
new
ih RAND 1000000
time sort
```
不同版本都測試十次,執行結果如下:
| | q_sort v1 | q_sort v2 | q_sort v3 | linux list_sort |
|-----------| -------- | -------- | -------- | -------- |
|0 | 2.147 | 1.296 | 1.319 | 1.254 |
|1 | 2.243 | 1.530 | 1.337 | 1.418 |
|2 | 2.023 | 1.468 | 1.354 | 1.282 |
|3 | 2.059 | 1.403 | 1.255 | 1.286 |
|4 | 2.034 | 1.415 | 1.290 | 1.325 |
|5 | 2.076 | 1.444 | 1.273 | 1.335 |
|6 | 2.080 | 1.539 | 1.386 | 1.442 |
|7 | 2.061 | 1.414 | 1.301 | 1.285 |
|8 | 2.143 | 1.377 | 1.301 | 1.328 |
|9 | 2.008 | 1.507 | 1.260 | 1.468 |
|avg. | 2.087 | 1.439 | 1.307 | 1.342 |
可以發現每次改動,q_sort的表現都有所進步,也可以從v1到v2大幅的進步表現可以了解到cache thrashing對程式的執行效率有嚴重的影響,另外透過小小的觀察改動可以再擠出一點效能,甚至和linux kernel的list_sort相當的執行效率。另外自認為linux kernel的list_sort即便有註解也還是反人類的難讀。