# 2024q1 Homework2 (quiz1+2)
contributed by < [`yourui1017`](https://github.com/yourui1017) >
## 第一週測驗題
### 測驗1
::: success
1. 解釋程式碼的運作原理,提出改進方案並予以實作。
2. 使用 Linux 核心風格的 List API 改寫上述程式碼,並針對鏈結串列,提出可避免最差狀況的快速排序實作,應設計效能評比的測試程式。
:::
:::spoiler 遮蔽的部分
首先先針對遮蔽的部分進行推敲。
* AAAA
```c
node_t *list_tail(node_t **left)
{
while ((*left) && (*left)->next)
left = &(AAAA);
return *left;
}
```
可以看到函式的目的是要找出 list 的最後一個 node,因此在指標的指標 left 還沒指到 NULL 之前,left 都應該持續往下一個節點移動,故 AAAA = (*left)->next。
* BBBB
```c
int list_length(node_t **left)
{
int n = 0;
while (*left) {
++n;
left = &(BBBB);
}
return n;
}
```
可以看到函式的目的是要找出 list 的長度,因此在指標的指標 left 還沒指到 NULL 之前,left 都應該持續往下一個節點移動,故 BBBB = (*left)->next。
* CCCC
```c
while (p) {
node_t *n = p;
p = CCCC;
list_add(n->value > value ? &right : &left, n);
}
```
該段程式碼推測是想要用 left 或 right 指標指向當前的節點(若比 pivot 小,則 left 會指向該節點,相反亦然。),且因為 list_add 會改變當前節點指向的位置,因此需要用 p 指標儲存下一個節點位置,故 CCCC = p->next
* DDDD && EEEE
```c
begin[i] = left;
end[i] = DDDD;
begin[i + 1] = pivot;
end[i + 1] = pivot;
begin[i + 2] = right;
end[i + 2] = EEEE;
```
在 [2024q1 第 1 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz1) 中有提到,begin 和 end 這兩個矩陣是為了用堆疊模擬原本的遞迴呼叫。
其中,begin 依照順序會儲存 left、pivot 和 right 的首指標,而 end 應該要儲存 left、pivot 和 right 的尾指標,故 DDDD = list_tail(&left)
EEEE = list_tail(&right)
:::
#### quick_sort 運作原理
1. pivot 會被指定在第一個節點的位置,並根據 pivot 的值與其他節點進行比較,如果小於 pivot 則會使用到 left 指標,將這些節點串接起來。相反的,如果大於 pivot 則會使用到 right 指標。
2. 將當前 left 和 right 指標指向的位置儲存在 begin 和 end 當中,以堆疊的形式模擬遞迴的寫法。
3. 直到分割成單一節點後,堆疊才會被 pop 出來,並且新增到 result 中。
* 首先 pivot 會被指定在第一個節點的位置。
```graphviz
digraph _graph_name_ {
rankdir=LR; # 順序由左至右(上下是"TD")
graph [fontname="DFKai-SB"]; # 此三行是設定字型
node [fontname="DFKai-SB"]; # 中文務必指定
edge [fontname="DFKai-SB"]; # 不然可能會出現亂碼
# {rank=same R D} # 同層(強迫上下)
# 這邊是 node (節點)
head[shape=plaintext,fontcolor = red]
pivot[shape = plaintext]
A[label = "3"]
B[label = "4"]
C[label = "1"] # 不給 label 就會直接用名稱
D[label = "2"]
E[label = "NULL"]
# 這邊是 edge (邊)
pivot->A
head->A->B->C->D->E # 可以一直連
}
```
* 因為之後要存進堆疊中,所以將 pivot 斷開成單一節點。
```graphviz
digraph _graph_name_ {
rankdir=LR; # 順序由左至右(上下是"TD")
graph [fontname="DFKai-SB"]; # 此三行是設定字型
node [fontname="DFKai-SB"]; # 中文務必指定
edge [fontname="DFKai-SB"]; # 不然可能會出現亂碼
head[shape=plaintext,fontcolor = red]
pivot[shape = plaintext]
A[label = "3"]
B[label = "4"]
C[label = "1"] # 不給 label 就會直接用名稱
D[label = "2"]
E[label = "NULL"]
F[label = "NULL"]
# 這邊是 edge (邊)
pivot->A
A->F
head->A
B->C->D->E # 可以一直連
}
```
* 使用 left 指標指向比 pivot 還要小的節點;使用 right 指標指向比 pivot 還要大的節點
```graphviz
digraph _graph_name_ {
rankdir=LR; # 順序由左至右(上下是"TD")
graph [fontname="DFKai-SB"]; # 此三行是設定字型
node [fontname="DFKai-SB"]; # 中文務必指定
edge [fontname="DFKai-SB"]; # 不然可能會出現亂碼
head[shape=plaintext,fontcolor = red]
pivot[shape = plaintext]
left[shape=plaintext,fontcolor = blue]
right[shape=plaintext,fontcolor = blue]
A[label = "3"]
B[label = "4"]
C[label = "1"] # 不給 label 就會直接用名稱
D[label = "2"]
E[label = "NULL"]
F[label = "NULL"]
G[label = "NULL"]
# 這邊是 edge (邊)
pivot->A
A->E
head->A
right->B->F
left->D->C->G
}
```
* 儲存到 begin 和 end 的堆疊中。
```graphviz
digraph _graph_name_ {
rankdir=LR; # 順序由左至右(上下是"TD")
graph [fontname="DFKai-SB"]; # 此三行是設定字型
node [fontname="DFKai-SB"]; # 中文務必指定
edge [fontname="DFKai-SB"]; # 不然可能會出現亂碼
head[shape=plaintext,fontcolor = red]
pivot[shape = plaintext]
left[shape=plaintext,fontcolor = blue]
right[shape=plaintext,fontcolor = blue]
begin0[label = "begin[0]", shape=plaintext]
begin1[label = "begin[1]", shape=plaintext]
begin2[label = "begin[2]", shape=plaintext]
end0[label = "end[0]", shape=plaintext]
end1[label = "end[1]", shape=plaintext]
end2[label = "end[2]", shape=plaintext]
A[label = "3"]
B[label = "4"]
C[label = "1"] # 不給 label 就會直接用名稱
D[label = "2"]
E[label = "NULL"]
F[label = "NULL"]
G[label = "NULL"]
# 這邊是 edge (邊)
pivot->A
A->E
head->A
right->B->F
left->D->C->G
begin0->D
end0->C
begin1->A
end1->A
begin2->B
end2->B
}
```
* 因為 begin[2] 指向單一節點,將begin[2] 節點 pop 出來,並且加入 result 中。
```graphviz
digraph _graph_name_ {
rankdir=LR; # 順序由左至右(上下是"TD")
graph [fontname="DFKai-SB"]; # 此三行是設定字型
node [fontname="DFKai-SB"]; # 中文務必指定
edge [fontname="DFKai-SB"]; # 不然可能會出現亂碼
head[shape=plaintext,fontcolor = red]
pivot[shape = plaintext]
left[shape=plaintext,fontcolor = blue]
right[shape=plaintext,fontcolor = blue]
begin0[label = "begin[0]", shape=plaintext]
begin1[label = "begin[1]", shape=plaintext]
begin2[label = "begin[2]", shape=plaintext]
end0[label = "end[0]", shape=plaintext]
end1[label = "end[1]", shape=plaintext]
end2[label = "end[2]", shape=plaintext]
result[shape=plaintext,fontcolor = red]
A[label = "3"]
B[label = "4"]
C[label = "1"] # 不給 label 就會直接用名稱
D[label = "2"]
E[label = "NULL"]
F[label = "NULL"]
G[label = "NULL"]
# 這邊是 edge (邊)
pivot->A
A->E
head->A
right->B->F
left->D->C->G
begin0->D
end0->C
begin1->A
end1->A
begin2->B
end2->B
result->B
}
```
重複以上步驟,直到堆疊中沒有東西即可完成排序。
:::warning
問題:
1. 能否減少堆疊需要使用到的記憶體大小?(在此方法中的最差狀況下的 max_level 為2*n -1,想辦法減少 max_level)
2. 能否藉由改變 pivot 的選擇方式,改善最差狀況的快速排序實作?
:::
**max_level**
在單向鍊結串列的 `quicksort` 中,最差狀況下的 max_level 會是 `2*n-1`,因為在堆疊中會儲存到 NULL,導致空間的浪費。
* 解決辦法
* 使用環狀鍊結串列改善,如此一來就不需要儲存 NULL,導致空間浪費,使 max_level 降為 n+1。
**pivot**
* 解決辦法
* 隨機選取 pivot 的位置,避免 pivot 選取到最小或最大的值上,導致最差狀況的發生。
**使用 Linux 核心風格的 List API 改寫**
* 將 `quick_sort` 使用 List API 改寫,精簡程式碼並增加易讀性。
```diff
node_t *L = begin[i], *R = end[i];
- if (L != R) {
- node_t *pivot = L;
- value = pivot->value;
- node_t *p = pivot->next;
- pivot->next = NULL;
-
- while (p) {
- node_t *n = p;
- p = p->next;
- list_add(n->value > value ? &right : &left, n);
+ struct list_head *L = begin[i], *R = list_tail(begin[i]);
+ if (L->next != R) {
+
+ // struct list_head *pivot = L->next;
+ // element_t *pivot_entry = list_entry(pivot, element_t, list);
+ // list_del_init(pivot);
+
+ struct list_head *pivot = select_pivot(L);
+ element_t *pivot_entry = list_entry(pivot, element_t, list);
+ list_del_init(pivot);
+
+ element_t *entry, *safe;
+ list_for_each_entry_safe(entry, safe, L, list){
+ list_move(&entry->list, ((strcmp(entry->value, pivot_entry->value) > 0) ? right : left));
}
```
**實驗結果**
* 使用 `perf stat --repeat 5 -e cache-references,instructions,cycles ./main
` 指令計算效能
* 將 pivot 設為第一個節點
* ::: spoiler 結果
```
Size 10
Performance counter stats for './main' (5 runs):
63,068 cache-references ( +- 1.08% )
1,132,332 instructions # 0.83 insn per cycle ( +- 0.40% )
1,368,200 cycles ( +- 5.47% )
0.0007218 +- 0.0000499 seconds time elapsed ( +- 6.91% )
```
```
Size 100
Performance counter stats for './main' (5 runs):
66,776 cache-references ( +- 3.01% )
1,484,857 instructions # 0.97 insn per cycle ( +- 0.15% )
1,530,310 cycles ( +- 5.18% )
0.0008237 +- 0.0000400 seconds time elapsed ( +- 4.86% )
```
```
Size 1000
Performance counter stats for './main' (5 runs):
70,253 cache-references ( +- 2.31% )
25,101,039 instructions # 2.92 insn per cycle ( +- 0.02% )
8,585,825 cycles ( +- 1.91% )
0.0025760 +- 0.0000577 seconds time elapsed ( +- 2.24% )
```
```
Size 10000
Performance counter stats for './main' (5 runs):
6,822,270 cache-references ( +- 7.93% )
2,266,479,072 instructions # 3.46 insn per cycle ( +- 0.00% )
655,706,507 cycles ( +- 0.98% )
0.16660 +- 0.00165 seconds time elapsed ( +- 0.99% )
```
```
Size 100000
Performance counter stats for './main' (5 runs):
4,499,222,447 cache-references ( +- 0.86% )
235,180,643,595 instructions # 3.40 insn per cycle ( +- 2.69% )
69,161,118,332 cycles ( +- 1.74% )
18.203 +- 0.198 seconds time elapsed ( +- 1.09% )
```
:::
* 隨機選取 pivot
* :::spoiler 結果
```
Size 10
Performance counter stats for './main' (5 runs):
62,809 cache-references ( +- 1.47% )
1,135,753 instructions # 0.84 insn per cycle ( +- 0.24% )
1,352,131 cycles ( +- 3.86% )
0.0007908 +- 0.0000288 seconds time elapsed ( +- 3.64% )
```
```
Size 100
Performance counter stats for './main' (5 runs):
67,972 cache-references ( +- 2.31% )
1,567,539 instructions # 0.89 insn per cycle ( +- 0.17% )
1,754,914 cycles ( +- 3.85% )
0.0010084 +- 0.0000784 seconds time elapsed ( +- 7.77% )
```
```
Size 1000
Performance counter stats for './main' (5 runs):
82,764 cache-references ( +- 3.04% )
28,938,278 instructions # 2.35 insn per cycle ( +- 0.01% )
12,310,694 cycles ( +- 1.21% )
0.01314 +- 0.00108 seconds time elapsed ( +- 8.23% )
```
```
Size 10000
Performance counter stats for './main' (5 runs):
28,574,071 cache-references ( +- 5.96% )
2,522,037,077 instructions # 2.46 insn per cycle ( +- 0.00% )
1,023,736,256 cycles ( +- 1.45% )
0.26103 +- 0.00528 seconds time elapsed ( +- 2.02% )
```
```
Size 100000
Performance counter stats for './main' (5 runs):
9,848,674,522 cache-references ( +- 0.54% )
246,962,163,428 instructions # 2.39 insn per cycle ( +- 0.00% )
103,465,962,705 cycles ( +- 0.47% )
26.567 +- 0.251 seconds time elapsed ( +- 0.95% )
```
:::
* 發現結果其實沒有變比較好,推測是因為鍊結串列每次在選取隨機點時需要走訪節點,且此走訪的時間會超過 worst case 搜尋的時間,導致效能下降。
```c
struct list_head *select_pivot(struct list_head *L){
struct list_head *pivot = L->next;
int listsize = list_length(L);
int rnum = rand();
for (int i = 0; i < (rnum % listsize); i++)
pivot = pivot->next;
return pivot;
}
```
### 測驗2
::: success
延伸問題:
1. 解釋上述程式碼的運作原理,提出改進方案並予以實作。
2. 將改進過的 Timsort 實作整合進 sysprog21/lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。
:::
:::spoiler 遮蔽的部分
首先先針對遮蔽的部分進行推敲。
* AAAA
```c
static struct list_head *merge(void *priv,
list_cmp_func_t cmp,
struct list_head *a,
struct list_head *b)
{
struct list_head *head;
struct list_head **tail = AAAA;
```
這邊宣告了 head 指標和 tail 為指標的指標,並考量到下面的程式碼是讓 tail 在該串列中進行走訪,故 AAAA = &head。
* BBBB && CCCC
```c
for (;;) {
/* if equal, take 'a' -- important for sort stability */
if (cmp(priv, a, b) <= 0) {
*tail = a;
tail = BBBB;
a = a->next;
if (!a) {
*tail = b;
break;
}
} else {
*tail = b;
tail = CCCC;
b = b->next;
if (!b) {
*tail = a;
break;
}
}
}
```
這邊的程式碼是要讓 tail 根據比較的結果指向 a 或 b 指標,並往下個指標移動,故 BBBB = CCCC = &(*tail)->next
* DDDD && EEEE
```c
static void build_prev_link(struct list_head *head,
struct list_head *tail,
struct list_head *list)
{
tail->next = list;
do {
list->prev = tail;
tail = list;
list = list->next;
} while (list);
/* The final links to make a circular doubly-linked list */
DDDD = head;
EEEE = tail;
}
```
build_prev_link 函式是要讓指標根據當前的排列順序指向正確的 link,故 DDDD = tail->next,EEEE = head->prev。
* FFFF
```c
/* The final merge; rebuild prev links */
struct list_head *stk0 = tp, *stk1 = stk0->prev;
while (stk1 && stk1->prev)
stk0 = stk0->prev, stk1 = stk1->prev;
if (stk_size <= FFFF) {
build_prev_link(head, head, stk0);
return;
}
merge_final(priv, cmp, head, stk1, stk0);
```
這邊是要判斷 stk_size 的大小,如果只有一個以下的 link 就不需要進行 build_prev_link,故 FFFF = 1。
:::
因為篇幅的緣故所以又開了一個筆記 [Timsort 筆記](https://hackmd.io/TCRWYzfSSjOOo3-ezNEeKA) 紀錄學習過程。
#### Timsort 程式運作原理
```graphviz
digraph _graph_name_ {
# 基本宣告
label = "Minrun = 3, stk_size = 0";
rankdir=LR; # 順序由左至右(上下是"TD")
node [shape=record, fontname=Helvetica, fontsize=10]
# node
list_head[label="{<p> prev | <n> next}"];
result[label="{<h> head | <n> next}"];
head_tim[shape=plaintext,fontcolor = red]
list_tim[shape=plaintext,fontcolor = red]
tp_tim[shape=plaintext,fontcolor = red]
A[label = "1"]
B[label = "2"]
C[label = "8"]
D[label = "4"]
E[label = "3"]
F[label = "5"]
G[label = "7"]
H[label = "10"]
NULL[label = "NULL", shape=plaintext]
# 指標
list_tim->A
tp_tim->NULL
# list_head 的 next
head_tim->list_head->A->B->C->D->E->F->G->H->NULL
# list_head 的 prev
H->G->F->E->D->C->B->A->list_head->H
}
```
進行這段的程式碼。
```c
do {
/* Find next run */
struct pair result = find_run(priv, list, cmp);
result.head->prev = tp;
tp = result.head;
list = result.next;
stk_size++;
tp = merge_collapse(priv, cmp, tp);
} while (list);
```
執行第一次的 while loop ,尋找下一個 run ,把該 run 的長度儲存在 run_head 的 next 的 prev 中,並且讓 run_head 的 prev 指向 NULL 。
除此之外,還宣告了 pair 的結構體儲存當前 run 的 head 和待尋找的節點位置。
```graphviz
digraph _graph_name_ {
# 基本宣告
label = "Minrun = 3, stk_size = 1";
rankdir=LR; # 順序由左至右(上下是"TD")
node [shape=record, fontname=Helvetica, fontsize=10]
# node
list_head[label="{<p> prev | <n> next}"];
result[label="{<h> head | <n> next}"];
len_1[label = "len_1 = 3", shape=plaintext]
head_tim[shape=plaintext,fontcolor = red]
list_tim[shape=plaintext,fontcolor = red]
tp_tim[shape=plaintext,fontcolor = red]
A[label = "1", style=filled, fillcolor = aquamarine1]
B[label = "2", style=filled, fillcolor = aquamarine1]
C[label = "8", style=filled, fillcolor = aquamarine1]
D[label = "4", style=filled, fillcolor = gold]
E[label = "3"]
F[label = "5"]
G[label = "7"]
H[label = "10"]
// NULL[label = "NULL", shape=plaintext]
NULL_1[label = "NULL", shape=plaintext]
NULL_2[label = "NULL", shape=plaintext]
NULL_3[label = "NULL", shape=plaintext]
NULL_4[label = "NULL", shape=plaintext]
# 指標
list_tim->D
tp_tim->A
# list_head 的 next
head_tim->list_head->A->B->C->NULL_1
D->E->F->G->H->NULL_2
# list_head 的 prev
H->G->F->E->D->C->B->len_1
A->NULL_3
# result
result:h->A
result:n->D
# 區域變數
// list[shape=plaintext, fontcolor =blue]
// next[shape=plaintext, fontcolor =blue]
// head[shape=plaintext, fontcolor =blue]
// prev[shape=plaintext, fontcolor =blue]
// list->C
// head->A
// next->D
// prev->NULL_4
}
```
進行第二次的 while loop ,尋找下一個 run ,並且因為 stack 的長度滿足 $B>C$ ,所以不合併。
```graphviz
digraph _graph_name_ {
# 基本宣告
label = "Minrun = 3, stk_size = 2";
rankdir=LR; # 順序由左至右(上下是"TD")
node [shape=record, fontname=Helvetica, fontsize=10]
# node
list_head[label="{<p> prev | <n> next}"];
result[label="{<h> head | <n> next}"];
len_1[label = "len_1 = 3", shape=plaintext]
len_2[label = "len_2 = 2", shape=plaintext] head_tim[shape=plaintext,fontcolor = red]
list_tim[shape=plaintext,fontcolor = red]
tp_tim[shape=plaintext,fontcolor = red]
A[label = "1", style=filled, fillcolor = coral1]
B[label = "2", style=filled, fillcolor = coral1]
C[label = "8", style=filled, fillcolor = coral1]
D[label = "4", style=filled, fillcolor = aquamarine1]
E[label = "3", style=filled, fillcolor = aquamarine1]
F[label = "5", style=filled, fillcolor = gold]
G[label = "7"]
H[label = "10"]
// NULL[label = "NULL", shape=plaintext]
NULL_1[label = "NULL", shape=plaintext]
NULL_2[label = "NULL", shape=plaintext]
NULL_3[label = "NULL", shape=plaintext]
NULL_4[label = "NULL", shape=plaintext]
# 指標
list_tim->F
tp_tim->E
# list_head 的 next
head_tim->list_head->A->B->C->NULL_1
D->NULL_4
E->D
F->G->H->NULL_2
# list_head 的 prev
H->G->F->E->A
D->len_2
C->B->len_1
A->NULL_3
# result
result:h->E
result:n->F
# 區域變數
// list[shape=plaintext, fontcolor =blue]
// next[shape=plaintext, fontcolor =blue]
// head[shape=plaintext, fontcolor =blue]
// prev[shape=plaintext, fontcolor =blue]
// list->E
// head->E
// next->F
// prev->D
}
```
進行第三次的 while loop,尋找下一個 run 。
```graphviz
digraph _graph_name_ {
# 基本宣告
label = "Minrun = 3, stk_size = 3";
rankdir=LR; # 順序由左至右(上下是"TD")
node [shape=record, fontname=Helvetica, fontsize=10]
# node
list_head[label="{<p> prev | <n> next}"];
result[label="{<h> head | <n> next}"];
len_1[label = "len_1 = 3", shape=plaintext]
len_2[label = "len_2 = 2", shape=plaintext]
len_3[label = "len_3 = 3", shape=plaintext]
head_tim[shape=plaintext,fontcolor = red]
list_tim[shape=plaintext,fontcolor = red]
tp_tim[shape=plaintext,fontcolor = red]
A[label = "1", style=filled, fillcolor = coral1]
B[label = "2", style=filled, fillcolor = coral1]
C[label = "8", style=filled, fillcolor = coral1]
D[label = "4", style=filled, fillcolor = coral1]
E[label = "3", style=filled, fillcolor = coral1]
F[label = "5", style=filled, fillcolor = aquamarine1]
G[label = "7", style=filled, fillcolor = aquamarine1]
H[label = "10", style=filled, fillcolor = aquamarine1]
// NULL[label = "NULL", shape=plaintext]
NULL_1[label = "NULL", shape=plaintext]
NULL_2[label = "NULL", shape=plaintext]
NULL_3[label = "NULL", shape=plaintext]
NULL_4[label = "NULL", shape=plaintext]
NULL_5[label = "NULL", shape=plaintext]
# 指標
list_tim->NULL_2
tp_tim->F
# list_head 的 next
head_tim->list_head->A->B->C->NULL_1
D->NULL_4
E->D
F->G->H->NULL_2
# list_head 的 prev
H->G->len_3
F->E
E->A
D->len_2
C->B->len_1
A->NULL_3
# result
result:h->F
result:n->NULL_5
# 區域變數
// list[shape=plaintext, fontcolor =blue]
// next[shape=plaintext, fontcolor =blue]
// head[shape=plaintext, fontcolor =blue]
// prev[shape=plaintext, fontcolor =blue]
// list->H
// head->F
// next->NULL_2
// prev->NULL_4
}
```
進行第三次的 while loop 。
因為堆疊的長度不滿足 $A>B+C$ 所以將 stack[1] 和 stack[2] 合併,完成 `merge_collapse` ,跳出 while 。
```graphviz
digraph _graph_name_ {
# 基本宣告
label = "Minrun = 3, stk_size = 2";
rankdir=LR; # 順序由左至右(上下是"TD")
node [shape=record, fontname=Helvetica, fontsize=10]
# node
list_head[label="{<p> prev | <n> next}"];
result[label="{<h> head | <n> next}"];
len_1[label = "len_1 = 3", shape=plaintext]
len_2[label = "len_2 = 2", shape=plaintext]
len_3[label = "len_3 = 5", shape=plaintext]
head_tim[shape=plaintext,fontcolor = red]
list_tim[shape=plaintext,fontcolor = red]
tp_tim[shape=plaintext,fontcolor = red]
A[label = "1", style=filled, fillcolor = coral1]
B[label = "2", style=filled, fillcolor = coral1]
C[label = "8", style=filled, fillcolor = coral1]
D[label = "4", style=filled, fillcolor = chartreuse1]
E[label = "3", style=filled, fillcolor = chartreuse1]
F[label = "5", style=filled, fillcolor = chartreuse1]
G[label = "7", style=filled, fillcolor = chartreuse1]
H[label = "10", style=filled, fillcolor = chartreuse1]
// NULL[label = "NULL", shape=plaintext]
NULL_1[label = "NULL", shape=plaintext]
NULL_2[label = "NULL", shape=plaintext]
NULL_3[label = "NULL", shape=plaintext]
NULL_5[label = "NULL", shape=plaintext]
# 指標
list_tim->NULL_2
tp_tim->E
# list_head 的 next
head_tim->list_head->A->B->C->NULL_1
E->D
D->F->G->H->NULL_2
# list_head 的 prev
H->G->len_3
F->E
E->A
D->len_2
C->B->len_1
A->NULL_3
# result
result:h->F
result:n->NULL_5
# 區域變數
// at[shape=plaintext, fontcolor =blue]
// prev[shape=plaintext, fontcolor =blue]
// list[shape=plaintext, fontcolor =blue]
// at->F
// prev->A
// list->E
}
```
因為已經搜尋完所有的節點,且堆疊只剩下 stack[0] 和 stack[1] ,故將它們合併。
執行 `merge_force_collapse` 。
```graphviz
digraph _graph_name_ {
# 基本宣告
label = "Minrun = 3, stk_size = 1";
rankdir=LR; # 順序由左至右(上下是"TD")
node [shape=record, fontname=Helvetica, fontsize=10]
# node
list_head[label="{<p> prev | <n> next}"];
result[label="{<h> head | <n> next}"];
len_1[label = "len_1 = 8", shape=plaintext]
len_2[label = "len_2 = 2", shape=plaintext]
len_3[label = "len_3 = 5", shape=plaintext]
head_tim[shape=plaintext,fontcolor = red]
list_tim[shape=plaintext,fontcolor = red]
tp_tim[shape=plaintext,fontcolor = red]
A[label = "1", style=filled, fillcolor = chartreuse1]
B[label = "2", style=filled, fillcolor = chartreuse1]
C[label = "8", style=filled, fillcolor = chartreuse1]
D[label = "4", style=filled, fillcolor = chartreuse1]
E[label = "3", style=filled, fillcolor = chartreuse1]
F[label = "5", style=filled, fillcolor = chartreuse1]
G[label = "7", style=filled, fillcolor = chartreuse1]
H[label = "10", style=filled, fillcolor = chartreuse1]
// NULL[label = "NULL", shape=plaintext]
NULL_2[label = "NULL", shape=plaintext]
NULL_3[label = "NULL", shape=plaintext]
NULL_5[label = "NULL", shape=plaintext]
# 指標
list_tim->NULL_2
tp_tim->A
# list_head 的 next
head_tim->list_head->A->B->C->E->D->F->G->H->NULL_2
# list_head 的 prev
H->G->len_3
F->E
E->A
D->len_2
C->B->len_1
A->NULL_3
# result
result:h->F
result:n->NULL_5
// # 區域變數
// at[shape=plaintext, fontcolor =blue]
// prev[shape=plaintext, fontcolor =blue]
// list[shape=plaintext, fontcolor =blue]
// at->E
// prev->NULL_3
// list->A
}
```
完成合併,將 prev 指標接上,完成 timsort。
```graphviz
digraph _graph_name_ {
# 基本宣告
label = "Minrun = 3, stk_size = 1";
rankdir=LR; # 順序由左至右(上下是"TD")
node [shape=record, fontname=Helvetica, fontsize=10]
# node
list_head[label="{<p> prev | <n> next}"];
result[label="{<h> head | <n> next}"];
head_tim[shape=plaintext,fontcolor = red]
list_tim[shape=plaintext,fontcolor = red]
tp_tim[shape=plaintext,fontcolor = red]
A[label = "1"]
B[label = "2"]
C[label = "8"]
D[label = "4"]
E[label = "3"]
F[label = "5"]
G[label = "7"]
H[label = "10"]
NULL[label = "NULL", shape=plaintext]
NULL_1[label = "NULL", shape=plaintext]
# 指標
list_tim->NULL
tp_tim->A
# list_head 的 next
head_tim->list_head->A->B->C->E->D->F->G->H->list_head
# list_head 的 prev
H->G->F->D->E->C->B->A->list_head->H
# result
result:h->F
result:n->NULL_1
}
```
在上述的示意圖中可以發現在此程式碼中並沒有規定 Minrun 的大小,run 的大小都是根據升冪 / 降冪的數量決定,導致 run 的數量不固定,無法確保分組接近 2 的冪。
另外,合併的方式也只有使用到合併排序,因此如果輸入資料大量的連續升冪 / 降冪將會導致效能低落,故可以實作 galloping mode。
:::info
TODO:
1. 加入 minrun 的判斷,並實作 insertion sort 保證達成 minrun。
2. 實作 exponentioal search 並在 Timsort 中加入 galloping mode。
3. 將改進過的 Timsort 實作整合進 sysprog21/lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。
:::
#### 實驗結果
1. 加入 minrun 的判斷,並實作 insertion sort 保證達成 minrun。
首先,先計算該鍊結串列的 minrun ,其中 minrun 的大小不得超過 64 ,否則會使 insertion sort 的效能低落。
```c
static size_t find_minrun (struct list_head *head)
{
size_t len = list_length(head);
// find first 6 bit & add up remain bits
size_t minrun = 0;
while(len >> 6) {
minrun += (len & 1);
len >>= 1;
}
minrun += len;
return minrun;
}
```
在 `find_run` 中加入 insertion sort ,使 run 的大小達到指定的 minrun 。
```diff
- if (cmp(priv, list, next) > 0) {
- /* decending run, also reverse the list */
- struct list_head *prev = NULL;
- do {
- len++;
- list->next = prev;
- prev = list;
- list = next;
- next = list->next;
- head = list;
- } while (next && cmp(priv, list, next) > 0);
- list->next = prev;
- else {
- do {
- len++;
- list = next;
- next = list->next;
- } while (next && cmp(priv, list, next) <= 0);
- list->next = NULL;
+ while (len < minrun && next) {
+ if (cmp(priv, list, next) > 0) {
+ /* decending run, also reverse the list */
+ do {
+ if (cmp(priv, *ptr, next) > 0) {
+ len++;
+ list->next = next->next;
+ next->next = *ptr;
+ *ptr = next;
+ next = list->next;
+ ptr = &head;
+ }
+ else
+ ptr = &(*ptr)->next;
+ } while (next && cmp(priv, list, next) > 0 && len < minrun );
+ } else {
+ do {
+ len++;
+ list = next;
+ next = list->next;
+ } while (next && cmp(priv, list, next) <= 0 && len < minrun );
+ }
}
+
+ list->next = NULL;
head->prev = NULL;
+ runs++;
head->next->prev = (struct list_head *) len;
result.head = head, result.next = next;
return result;
```
使用 `perf stat --repeat 3 -e cache-misses,cache- references,instructions,cycles ./main` 針對隨機資料進行測試。
未使用 minrun :
```
Sample size = 10000
runs = 4132
Performance counter stats for './main' (3 runs):
41,237 cache-misses # 20.13% of all cache refs ( +- 8.36% )
204,818 cache-references ( +- 8.36% )
10,369,779 instructions # 1.01 insn per cycle ( +- 0.34% )
10,267,786 cycles ( +- 2.64% )
0.01182 +- 0.00119 seconds time elapsed ( +- 10.10% )
```
```
Sample size = 100000
runs = 41293
Performance counter stats for './main' (3 runs):
444,764 cache-misses # 11.31% of all cache refs ( +- 12.41% )
3,931,395 cache-references ( +- 3.49% )
107,925,882 instructions # 0.85 insn per cycle ( +- 0.15% )
126,984,584 cycles ( +- 1.90% )
0.0432 +- 0.0103 seconds time elapsed ( +- 23.85% )
```
```
Sample size = 1000000
runs = 412970
Performance counter stats for './main' (3 runs):
29,468,058 cache-misses # 36.47% of all cache refs ( +- 6.93% )
80,794,375 cache-references ( +- 4.52% )
1,235,257,451 instructions # 0.48 insn per cycle ( +- 0.34% )
2,549,099,081 cycles ( +- 3.84% )
0.6463 +- 0.0246 seconds time elapsed ( +- 3.80% )
```
使用 minrun :
```
Sample size = 10000
runs = 250
Performance counter stats for './main' (3 runs):
23,796 cache-misses # 12.57% of all cache refs ( +- 38.20% )
189,267 cache-references ( +- 5.48% )
14,209,541 instructions # 1.41 insn per cycle ( +- 0.18% )
10,082,736 cycles ( +- 0.62% )
0.004440 +- 0.000234 seconds time elapsed ( +- 5.26% )
```
```
Sample size = 100000
runs = 1924
Performance counter stats for './main' (3 runs):
418,569 cache-misses # 10.57% of all cache refs ( +- 10.14% )
3,959,824 cache-references ( +- 0.76% )
166,587,040 instructions # 1.29 insn per cycle ( +- 0.04% )
128,848,686 cycles ( +- 1.20% )
0.033213 +- 0.000508 seconds time elapsed ( +- 1.53% )
```
```
Sample size = 1000000
runs = 15873
Performance counter stats for './main' (3 runs):
26,499,448 cache-misses # 34.56% of all cache refs ( +- 1.07% )
76,686,563 cache-references ( +- 0.06% )
1,988,304,575 instructions # 0.83 insn per cycle ( +- 0.00% )
2,400,667,775 cycles ( +- 0.53% )
0.60511 +- 0.00330 seconds time elapsed ( +- 0.54% )
```
可以觀察到加入 minrun 後, run 的數量都會維持在略小於 2 的冪,這樣一來就會使合併的效能提昇。
2. 實作 exponentioal search 並在 Timsort 中加入 galloping mode。
因為 exponentioal search 的效率取決於能夠隨機訪問元素,但對於鍊結串列來說,因為需要走訪所有的節點,導致隨機訪問的效率較低,因此判斷即使加入 exponentioal search 反而可能會讓整體 timsort 的效能降低。
3. 將改進過的 Timsort 實作整合進 sysprog21/lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。
參考 [csotaku0926](https://hackmd.io/@csotaku0926/linux2024-homework1#%E6%95%88%E8%83%BD%E5%88%86%E6%9E%90%E5%AF%A6%E9%A9%97) 同學的方法進行效能測試的實驗。
詳細請看 [commit40fb4](https://github.com/yourui1017/lab0-c/commit/7d2c3debde207bd6b5c22938bb483bda8a4f2295) 。
## 第二週測驗題
### 測驗1
::: success
延伸問題:
1. 解釋上述程式碼的運作,撰寫完整的測試程式,指出其中可改進之處並實作
2. 在 Linux 核心的 cgroups 相關原始程式碼,找出 pre-order walk 程式碼並探討
:::
:::spoiler 遮蔽的部分
首先先針對遮蔽的部分進行推敲。
* AAAA
```diff
- n->next = AAAA;
+ n->next = h->first;
```
讓新增的 node 指向原本 h->first 指向的位置。
* BBBB
```diff
- hlist_for_each (p, BBBB) {
+ hlist_for_each (p, &heads[hash]) {
```
針對對應的 heads[hash] 去找內部的 inorder tree 。
* CCCC
```diff
- struct order_node *on = CCCC(p, struct order_node, node);
+ struct order_node *on = container_of(p, struct order_node, node);
```
使用 container_of 找到包含此 node 的 order_node 。
* DDDD
```diff
- hlist_add_head(&on->node, DDDD);
+ hlist_add_head(&on->node, &heads[hash]);
```
針對對應的 heads[hash] 加入 node 。
:::
**Construct Binary Tree from Preorder and Inorder Traversal 原理解釋**
本測驗的目的是藉由輸入樹的前序和中序的序列還原完整的樹。
前序遍歷:順序是根節點、左子節點、右子節點。
中序遍歷:順序是左子節點、根節點、右子節點。
後序遍歷:順序是左子節點、右子節點、根節點。
因此可以先透過前序序列找到根節點,並且找到根節點對應到中序序列的 index 。如此一來,就可以藉由中序序列找到根節點、左子樹和右子樹。
並且使用 dfs(Depth-first Search) ,依據順序找到根節點、左子樹和右子樹,在此程式碼中是使用遞迴實作 dfs 。
**示意圖**
```graphviz
digraph order
{
rankdir=TB;
inorder [label="Inorder:";shape="none"]
preorder[label="Preorder:";shape="none"]
{rank=same inorder A_in B_in C_in D_in E_in}
{rank=same preorder A_pre B_pre C_pre D_pre E_pre}
A_in [label="9"]
B_in [label="3"]
C_in [label="15"]
D_in [label="20"]
E_in [label="7"]
inorder
A_pre [label="3"]
B_pre [label="9"]
C_pre [label="20"]
D_pre [label="15"]
E_pre [label="7"]
preorder->inorder[style=invis]
preorder->A_pre [style=invis]
A_pre->B_pre->C_pre->D_pre->E_pre
inorder->A_in [style=invis]
A_in->B_in->C_in->D_in->E_in
}
```
因此可以找到根節點、左子樹和右子樹。
根節點:3
左子樹:9
右子樹:15、20、7
```graphviz
digraph order
{
rankdir=TB;
inorder [label="Inorder:";shape="none"]
preorder[label="Preorder:";shape="none"]
// root [label="root";shape="none"]
// left [label="left";shape="none"]
// right [label="right";shape="none"]
{rankdir=TB preorder inorder}
{rank=same inorder A_in B_in C_in D_in E_in}
{rank=same preorder A_pre B_pre C_pre D_pre E_pre}
A_in [label="9"]
B_in [label="3"]
C_in [label="15"]
D_in [label="20"]
E_in [label="7"]
A_pre [label="3"]
B_pre [label="9"]
C_pre [label="20"]
D_pre [label="15"]
E_pre [label="7"]
preorder->inorder[style=invis]
preorder->A_pre [style=invis]
A_pre->B_pre->C_pre->D_pre->E_pre
inorder->A_in [style=invis]
A_in->B_in->C_in->D_in->E_in
A_pre->B_in [color=red]
}
```
接下來就可以針對左右子樹找根節點、左子樹和右子樹。
最後就可以建立原始的樹如下圖:
```graphviz
digraph order
{
rankdir=TB;
{rank=TB A B}
{rank=same B C}
{rank=same D E}
A [label="3"]
B [label="9"]
C [label="20"]
D [label="15"]
E [label="7"]
A->B
A->C
C->D
C->E
}
```
**雜湊表示意圖**
另外,因為本測驗是基於鍊結串列做實作,因此為保證查閱時間為 O(1) ,使用 hash table 儲存中序序列,以便在 dfs 函式中可以使用前序序列查閱。
![image](https://hackmd.io/_uploads/SkOPkX610.png)
上圖取自 [Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable)
:::info
TODO
1. 在 Linux 核心的 cgroups 相關原始程式碼,找出 pre-order walk 程式碼並探討。
:::
### 測驗2
:::success
延伸問題:
1. 解釋上述程式碼的運作,撰寫完整的測試程式,指出其中可改進之處並實作
2. 在 Linux 核心找出 LRU 相關程式碼並探討
:::
:::spoiler 遮蔽的部份
* EEEE
```diff
- EEEE = pprev;
+ next->pprev = pprev;
```
* FFFF
```diff
- LRUNode *cache = list_entry(pos, LRUNode, FFFF);
+ LRUNode *cache = list_entry(pos, LRUNode, link);
```
* GGGG
```diff
- list_del(GGGG);
+ list_del(pos);
```
* HHHH
```diff
- LRUNode *cache = list_entry(pos, LRUNode, HHHH);
+ LRUNode *cache = list_entry(pos, LRUNode, node);
```
* IIII
```diff
- list_move(IIII, &obj->dhead);
+ list_move(&cache->link, &obj->dhead);
```
* JJJJ
```diff
- LRUNode *c = list_entry(pos, LRUNode, JJJJ);
+ LRUNode *c = list_entry(&c->dhead, LRUNode, node);
```
* KKKK
```diff
- list_move(KKKK, &obj->dhead);
+ list_move(&c->link, &obj->dhead);
```
:::
**LRU 原理解釋**
Least Recently Used (LRU) 的核心觀念就是將比較常用到的資料放置在記憶體空間的前面,減少對磁盤的存取,讓讀取的速度可以更快。
LRU 使用的是先進先出的概念,所以如果佇列滿了,則會將 tail 的 Node 移除,並在 Head 加上新使用的 Node 。如此一來,佇列就會儲存最近被使用到的資料。
**示意圖**
```graphviz
digraph {
rankdir=LR;
subgraph cluster_1{
node [shape=record];
label = "LRUCache"
other[label = "<cap> capacity|<count> count"]
list_head [label = "<h> dhead|{<prev> prev|<next> next}", group = list];
hlist_head [label = "<h> hhead[]|<ht0>|<ht1>|<ht2>|<ht3>|<ht4> "];
}
subgraph cluster_2{
node [shape=record];
label = "LRUNode 2"
other2[label = "<key> key|<val> value"]
list_head2 [label = "<h> link|{<prev> prev|<next> next}", group = list];
hlist_node2 [label = "<h> node|{<pprev> pprev|<next> next}", group = list];
}
subgraph cluster_3{
node [shape=record];
label = "LRUNode 1"
other1[label = "<key> key|<val> value"]
list_head1 [label = "<h> link|{<prev> prev|<next> next}", group = list];
hlist_node1 [label = "<h> node|{<pprev> pprev|<next> next}", group = list];
}
list_head:next->list_head2:h
list_head2:prev->list_head:h
list_head2:next->list_head1:h
list_head1:prev->list_head2:h
hlist_head:ht0->hlist_node2:h
hlist_node2:pprev->hlist_head:ht0
hlist_node2:next->hlist_node1:h
hlist_node1:pprev->hlist_node2:next
}
```
其中 `count` 代表目前 Cache使用的大小、 `capacity` 代表 hhead 的大小。
除了使用雙向鍊結串列連接以外,還有使用到 hash table 儲存鍊結,這樣一來就可以加快存取鍊結的速度。
**程式碼解釋**
```c
int lRUCacheGet(LRUCache *obj, int key)
{
int hash = key % obj->capacity;
struct hlist_node *pos;
hlist_for_each (pos, &obj->hhead[hash]) {
LRUNode *cache = list_entry(pos, LRUNode, node);
if (cache->key == key) {
list_move(&cache->link, &obj->dhead);
return cache->value;
}
}
return -1;
}
```
lRUCacheGet 會將 key 模除得到 hash值,並且根據 hash 值去找對應的 hash table 中的 CacheNode ,如果找到話就回傳該 key 對應的 value。
藉由存取 hash table 加快鍊結串列的存取速度。
```c
void lRUCachePut(LRUCache *obj, int key, int value)
{
LRUNode *cache = NULL;
int hash = key % obj->capacity;
struct hlist_node *pos;
hlist_for_each (pos, &obj->hhead[hash]) {
LRUNode *c = list_entry(pos, LRUNode, node);
if (c->key == key) {
list_move(&c->link, &obj->dhead);
cache = c;
}
}
if (!cache) {
if (obj->count == obj->capacity) {
cache = list_last_entry(&obj->dhead, LRUNode, link);
list_move(&cache->link, &obj->dhead);
hlist_del(&cache->node);
hlist_add_head(&cache->node, &obj->hhead[hash]);
} else {
cache = malloc(sizeof(LRUNode));
hlist_add_head(&cache->node, &obj->hhead[hash]);
list_add(&cache->link, &obj->dhead);
obj->count++;
}
cache->key = key;
}
cache->value = value;
}
```
lRUCachePut 則是會先判斷 Cache 的容量是否已經滿了。
1. count == capacity
移除 hlist 的最後一個 node ,並且將剛剛存取的 node 移至 hlist 的 head 後。
2. count != capacity
直接將剛剛存取的 node ,加入在hlist 的 head 後。
此處的 hash function 都是使用模除的方式,但針對不知道 key 的範圍以及分佈情形,適用 Multiplication Method 。
:::info
TODO:
1. 將 hash function 改成 Multiplication Method 。
2. 在 Linux 核心找出 LRU 相關程式碼並探討。
:::
### 測驗3
::: success
延伸問題:
1. 解釋上述程式碼的運作原理
2. 在 Linux 核心原始程式碼找出 find_nth_bit 的應用案例,並予以解說和分析,需要涵蓋 CPU affinity 相關的原始程式碼。
:::
:::spoiler 遮蔽的部份
* AAAA
```diff
- if ((word & AAAA) == 0) {
+ if ((word & 0xfffffff) == 0) {
```
* BBBB
```diff
- *p &= BBBB;
+ *p &= ~mask;
```
* CCCC
```diff
- if (sz CCCC BITS_PER_LONG)
+ if (sz % BITS_PER_LONG)
```
:::
**原理解釋**
參考自 [yuyuan0625](https://hackmd.io/@yuyuan/linux2024-homework2)
```c
static inline unsigned long __ffs(unsigned long word)
{
int num = 0;
#if BITS_PER_LONG == 64
if ((word & AAAA) == 0) {
num += 32;
word >>= 32;
}
#endif
if ((word & 0xffff) == 0) {
num += 16;
word >>= 16;
}
if ((word & 0xff) == 0) {
num += 8;
word >>= 8;
}
if ((word & 0xf) == 0) {
num += 4;
word >>= 4;
}
if ((word & 0x3) == 0) {
num += 2;
word >>= 2;
}
if ((word & 0x1) == 0)
num += 1;
return num;
}
```
`__ffs()` 是用來查詢一個 word 中最低位 1 的所在位置, 若 (word & 0xffffffff) == 0 即可知道較低的32位元內沒有任一位元為1, 因此將 word 右移 32 位元再進行檢查。
```c
static inline void __clear_bit(unsigned long nr, volatile unsigned long *addr)
{
unsigned long mask = BIT_MASK(nr);
unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr);
*p &= BBBB;
}
```
此段程式碼是透過 BIT_MASK(nr) 產生出只有第 nr 位為 1 ,其他位皆為 0 的遮罩,並利用此遮罩將該 addr 的第 nr 位清除。
```c
static inline unsigned long fns(unsigned long word, unsigned int n)
{
while (word) {
unsigned int bit = __ffs(word);
if (n-- == 0)
return bit;
__clear_bit(bit, &word);
}
return BITS_PER_LONG;
}
```
此段程式碼目的為找到第 n 個被設為 1 的位置,它會不斷地呼叫 __ffs 找到最低被設為 1 的位元,若還不是第 n 個就會使用 __clear_bit 將目前的位元清除,再繼續找下一個被設為 1 的位元。
```c
static inline unsigned long find_nth_bit(const unsigned long *addr,
unsigned long size,
unsigned long n)
{
if (n >= size)
return size;
if (small_const_nbits(size)) {
unsigned long val = *addr & GENMASK(size - 1, 0);
return val ? fns(val, n) : size;
}
return FIND_NTH_BIT(addr[idx], size, n);
}
```
運作原理:
先利用 `small_const_nbits` 比較要找的位數是否超過限制的 `size` , 並利用 `GENMASK(h, l)` 將 h 到 l 間的位元變為 1和 `addr` 做 & 運算 ,若 `val` 有值代表 `addr` h 到 l 間有位元被設為1,因此再利用 fns 找到為 第 n 個被設為 1 的位元並回傳其位置。若不符合 `small_const_nbits` 就利用 `FIND_NTH_BIT` 來處理。
```c
#define FIND_NTH_BIT(FETCH, size, num) \
({ \
unsigned long sz = (size), nr = (num), idx, w, tmp; \
\
for (idx = 0; (idx + 1) * BITS_PER_LONG <= sz; idx++) { \
if (idx * BITS_PER_LONG + nr >= sz) \
goto out; \
\
tmp = (FETCH); \
w = hweight_long(tmp); \
if (w > nr) \
goto found; \
\
nr -= w; \
} \
\
if (sz CCCC BITS_PER_LONG) \
tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz); \
found: \
sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz); \
out: \
sz; \
})
```
`FIND_NTH_BIT` 能夠搜尋超出單個 `BITS_PER_LONG `長度的範圍。如果要查找的位元不在目前處理的字節中,能透過迴圈繼續在下一個 `BITS_PER_LONG` 長度的區塊中搜尋,直到找到該位為止。
因為 `size` 可能無法被 `BITS_PER_LONG` 整除,因此搜尋到最後一輪時應避免找到超出 `size` 範圍的字元,因此使用取餘數的方式找到最後一個字節所需要搜尋的字元數量。
:::info
TODO:
1. 在 Linux 核心原始程式碼找出 find_nth_bit 的應用案例,並予以解說和分析,需要涵蓋 CPU affinity 相關的原始程式碼。
:::
## 參考資料
[2024q1 第 1 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz1)
[演算法-快速排序法Quick Sort](https://ithelp.ithome.com.tw/articles/10278644)
[timsort.txt](https://svn.python.org/projects/python/trunk/Objects/listsort.txt)
[V8 內的排序演算法 — Timsort](https://yuanchieh.page/posts/2019/2019-08-09-v8-%E5%85%A7%E7%9A%84%E6%8E%92%E5%BA%8F%E6%BC%94%E7%AE%97%E6%B3%95-timsort/)
[排序算法 (六) - Timsort](https://www.sakuratears.top/blog/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95%EF%BC%88%E5%85%AD%EF%BC%89-TimSort.html)
[世界上最快的排序算法——Timsort](https://www.cnblogs.com/sunshuyi/p/12680918.html)
[指數搜尋 Exponential Search](https://rust-algo.club/searching/exponential_search/)