# 2024q1 Homework 1 (lab0)
[Github](https://github.com/AsaneZeto/lab0-c)
## 實驗環境
```
❯ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
❯ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 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
```
## 實作佇列操作
### q_new
使用`malloc`配置一個`list_head`大小的記憶體 (由`sizeof(struct list_head)`決定),根據`man malloc`:
> On error, these functions return NULL. NULL may also be returned by a successful call to malloc() with a size of zero, or by a successful call to calloc() with nmemb or size equal to zero.
因此在使用`list.h`中定義的`INIT_LIST_HEAD`函式前須檢查這段記憶體位址。
```c
struct list_head *q_new()
{
struct list_head *head = malloc(sizeof(struct list_head));
if (head)
INIT_LIST_HEAD(head);
return head;
}
```
### q_free
根據`queue.h`的描述得知,需要釋放所有佇列元素`element_t`及找到佇列整體的`list_head`
> q_free() - Free all storage used by queue, no effect if header is NULL
在`list.h`裡面有定義巨集`list_for_each_entry_safe`,可以安全的逐一走訪並移除包含鏈結串列節點的結構。
```c
void q_free(struct list_head *head)
{
if(!head)
return;
element_t *ptr, *safe;
list_for_each_entry_safe(ptr, safe, head, list) {
q_release_element(ptr);
}
free(head);
}
```
### q_insert_head
檢查配置後的記憶體是否為有效的(`strdup`也是使用`malloc`),若不是表示失敗回傳false,否則使用`list_add`把`node`加入佇列開頭後回傳true
```c
bool q_insert_head(struct list_head *head, char *s)
{
element_t *node = malloc(sizeof(element_t));
if (!node)
return false;
node->value = strdup(s);
if (!node->value) {
free(node);
return false;
}
list_add(&node->list, head);
return true;
}
```
### q_insert_tail
其實就是把佇列的最後一個元素當作head,並對它做`q_insert_head`可以節省重複的程式碼。
```c
bool q_insert_tail(struct list_head *head, char *s)
{
return q_insert_head(head->prev, s);
}
```
### q_remove_head
如果`head`是無效記憶體位址或是佇列為空的話直接回傳NULL。否則利用`container_of`巨集找到第一個元素的位址`element_t *node`後利用`list_del`把該節點從佇列中移除,接著利用`strncpy`把`node`的字串`node->value`複製到`sp`。
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *node = container_of(head->next, element_t, list);
list_del(&node->list);
if (sp) {
strncpy(sp, node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
return node;
}
```
### q_remove_tail
使用和`q_insert_tail`一樣的概念來減少重複的程式碼,只是這邊的`head`是最後一個元素的前一個(`head->prev->prev`)
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
return q_remove_head(head->prev->prev, sp, bufsize);
}
```
### q_delete_mid
使用快慢指標技巧找到中間節點。假設佇列大小是`n`,當`n`是
* 基數時會在`fast == head`時結束
* 偶數時會在`fast == head->prev`時結束
且`slow`都會停在第`n/2`個節點,也就是中間節點。
```c
bool q_delete_mid(struct list_head *head)
{
// https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
if (!head || list_empty(head))
return false;
struct list_head *slow = head->next, *fast = head->next->next;
while (fast != head && fast->next != head) {
fast = fast->next->next;
slow = slow->next;
}
list_del_init(slow);
q_release_element(list_entry(slow, element_t, list));
return true;
}
```
### q_delete_dup
`struct list_head *node`走訪鏈結串列的節點,`struct list_head *ptr`用來記錄`node`之後的節點,並計算總共有幾個字串相同的節點(`cnt`),超過一個表示存在副本。因為要刪除需要先從鏈結串列中移除後再釋放記憶體,所以用`element_t *tmp`來指向包含`node`的下一個節點所在的元素後再進行操作,直到副本全部刪除完畢。
```c
bool q_delete_dup(struct list_head *head)
{
// https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
if (!head)
return false;
struct list_head *node = head;
while (node->next != head) {
struct list_head *ptr = node->next;
int cnt = 1;
while (ptr->next != head) {
element_t *curr = list_entry(ptr, element_t, list);
element_t *next = list_entry(ptr->next, element_t, list);
if (strcmp(curr->value, next->value) == 0) {
ptr = ptr->next;
cnt++;
} else
break;
}
// found duplicated element
if (cnt > 1) {
while (cnt--) {
element_t *tmp = list_entry(node->next, element_t, list);
list_del(&tmp->list);
q_release_element(tmp);
}
} else
node = node->next;
}
return true;
}
```
### q_swap
`node`從第一個節點走訪整個鏈結串鍊,把`node`移動到`node->next`後面即可。
```c
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
if (!head || list_is_singular(head))
return;
struct list_head *node;
for (node = head->next; node != head && node->next != head;
node = node->next) {
list_del(node);
list_add(node, node->next);
}
}
```
### q_reverse
`node`指向第一個節點,把`node->next`移到佇列開頭直到`node`為佇列最後一個節點即可反轉佇列。
```c
void q_reverse(struct list_head *head)
{
if (!head || list_is_singular(head))
return;
struct list_head *node = head->next;
while (node->next != head) {
struct list_head *next = node->next;
list_del(next);
list_add(next, head);
}
}
```
### q_reverseK
和`q_reverse`概念一樣,每k個節點作為一組子佇列,且`node`指向上一個子佇列的最後一個節點,當反轉下一組子佇列時節點會加在`node`後方。
```c
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
if (!head || list_is_singular(head))
return;
int count = q_size(head);
struct list_head *node = head;
struct list_head *curr, *next;
while (count >= k) {
curr = node->next;
for (int i = 0; i < k - 1; i++) {
next = curr->next;
list_del(next);
list_add(next, node);
}
count -= k;
node = curr;
}
}
```
### q_descend & q_ascend
這一部分的函式實作會涉及到對`element_t`中的`value`值進行比較,為了讓程式碼更精簡,我參考了[list_sort.h](https://github.com/torvalds/linux/blob/master/include/linux/list_sort.h#L9)並自定義一個function pointer type `list_cmp_func_t`,允許透過實作不同的比較函數以滿足不同的需求。考慮到`element_t`的`value`是字串,所以會使用`strcmp`作為基礎比較函數,所以`list_cmp_func_t`回傳值的型別也是int。
```c
typedef int
__attribute__((nonnull(1, 2))) (*list_cmp_func_t)(const struct list_head *,
const struct list_head *);
static inline int q_less(const struct list_head *a, const struct list_head *b)
{
return strcmp(list_entry(a, element_t, list)->value,
list_entry(b, element_t, list)->value);
}
static inline int q_greater(const struct list_head *a, const struct list_head *b)
{
return strcmp(list_entry(b, element_t, list)->value,
list_entry(a, element_t, list)->value);
}
```
`q_descend`的功用是要刪除所有後方有比自身更大的值的節點。因為`list_head`是雙向鏈結,直接從最後一個節點往前走訪,並刪除前方任何比它小的節點即可。
`q_ascend`的目標則是和`q_descend`正好相反,因此只需要替換`cmp`,其他的程式碼保持不變,為了避免程式碼的重複,將共用的部分抽出成一個函式`q_remove_cmp`,並讓cmp成為其中一個參數。如此`q_descend`和`q_ascend`只需要呼叫`q_remove_cmp`並傳入`q_less`或`q_greater`即可。
```c
static inline int q_remove_cmp(struct list_head *head, list_cmp_func_t cmp)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
if (!head || list_empty(head) || list_is_singular(head))
return q_size(head);
struct list_head *node = head->prev;
while (node != head && node->prev != head) {
if (cmp(node, node->prev) < 0) {
struct list_head *tmp = node->prev;
list_del_init(tmp);
q_release_element(list_entry(tmp, element_t, list));
} else
node = node->prev;
}
return q_size(head);
}
int q_descend(struct list_head *head)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
return q_remove_cmp(head, q_greater);
}
int q_ascend(struct list_head *head)
{
return q_remove_cmp(head, q_less);
}
```
### q_sort
- 首先參考了[Merge Sort 與它的變化](https://hackmd.io/@lambert-wu/list-merge-sort)的實驗結果,選擇一開始要將鏈結串列分割成排序好的子串列,注意每個子串列都是單向的。
- 但是在合併階段遇上問題,因為Linux kernel並不支援VLA,需要使用其他方法表達各個子串列。
- 參考[LJP-TW的實作](https://hackmd.io/@LJP/SyRFuull9#q_sort)後發現他使用和[linux/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 中相似的技巧: 用sorted sublists開頭節點的`prev`指向上一個sorted sublists的開頭節點,因為在排序過程中皆是考慮單向的串列鏈結,`prev`會失去原本意義。
- `chain`這個變數名有點難理解它的作用,事實上他會是合併前後sublist的第一個節點,因此在這邊重新命名為`subhead`。
- 最後將排序好的鏈結串列修復成雙向的。
```c
static void merge(struct list_head **head,
struct list_head *a,
struct list_head *b,
list_cmp_func_t cmp)
{
struct list_head **tail = head;
while (a && b) {
if (cmp(a, b) <= 0) {
*tail = a;
a = a->next;
} else {
*tail = b;
b = b->next;
}
tail = &(*tail)->next;
}
if (a) {
*tail = a;
} else if (b) {
*tail = b;
}
}
/* Sort elements of queue in ascending order */
void q_sort(struct list_head *head, bool descend)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
list_cmp_func_t q_cmp = descend ? q_greater : q_less;
// Split original list into sorted sublists.
struct list_head *h = head->next, *t = head->next;
struct list_head *sublist = NULL;
while (h != head) {
while (t->next != head && q_cmp(t, t->next) <= 0) {
t = t->next;
}
h->prev = sublist;
sublist = h;
t = t->next;
t->prev->next = NULL;
h = t;
}
// Merge sublists.
struct list_head *tmp, *currsub;
while (sublist->prev) {
struct list_head **subhead;
currsub = sublist;
subhead = &sublist;
while (currsub && currsub->prev) {
tmp = currsub->prev->prev;
merge(subhead, currsub, currsub->prev, q_cmp);
subhead = &(*subhead)->prev;
currsub = tmp;
}
*subhead = currsub;
}
// Fix doubly linked list
head->next = sublist;
tmp = head;
while (sublist) {
sublist->prev = tmp;
tmp = sublist;
sublist = sublist->next;
}
head->prev = tmp;
tmp->next = head;
}
```
### q_merge
- 因為涉及到排序,所以一開始先把所有佇列變成單向鏈結
- `queue_contex_t`中有`int id`這個成員,我把它當作陣列的索引,使用分段合併
- 和`q_sort`一樣,最後把合併後的串列修復成雙向鏈結。
```c
static inline int get_contex_id(struct list_head *head)
{
return list_entry(head, queue_contex_t, chain)->id;
}
/* Merge all the queues into one sorted queue, which is in ascending order */
int q_merge(struct list_head *head, bool descend)
{
// https://leetcode.com/problems/merge-k-sorted-lists/
int n_queue = 0;
queue_contex_t *ptr = NULL;
list_for_each_entry (ptr, head, chain) {
n_queue++;
ptr->q->prev->next = NULL;
}
list_cmp_func_t q_cmp = descend ? q_greater : q_less;
struct list_head *l1, *l2;
struct list_head *contex1, *contex2;
for (int interval = 1; interval < n_queue; interval *= 2) {
contex1 = head->next;
contex2 = head->next;
for (int i = 0; i < interval; i++)
contex2 = contex2->next;
while (get_contex_id(contex1) + interval < n_queue) {
l1 = list_entry(contex1, queue_contex_t, chain)->q;
l2 = list_entry(contex2, queue_contex_t, chain)->q;
struct list_head *subhead = l1->next;
struct list_head **indir = &subhead;
merge(indir, subhead, l2->next, q_cmp);
l1->next = *indir;
INIT_LIST_HEAD(l2);
if (get_contex_id(contex1) + 2 * interval > n_queue)
break;
for (int i = 0; i < interval * 2; i++) {
contex1 = contex1->next;
contex2 = contex2->next;
}
}
}
// Fix doubly linked list
l1 = list_entry(head->next, queue_contex_t, chain)->q;
struct list_head *prev = l1, *curr = l1->next;
while (curr) {
curr->prev = prev;
prev = curr;
curr = curr->next;
}
l1->prev = prev;
prev->next = l1;
return q_size(l1);
}
```
## 研讀論文〈Dude, is my code constant time?〉
### 論文貢獻
用統計分析測量一段程式碼是不是常數時間,藉此規避需要硬體模型的問題
### 方法: Timing Leakage Detection
- 對程式執行時間做leakage detection test
- 測量兩種資料的執行時間並對它們的分布(timing distribution in this context)做統計檢定
- 三個步驟:Measure execution time, Apply post-processing, Apply statistical test
- Step 1: Measure execution time:
1. Classes definition: Leakage detection對需要兩種data class採取兩套測量。例如fix-vs-random tests: 第一種資料會固定在某個常數值(for corner case),而第二種資料是隨機採樣。
2. Cycle counters: 用來精確的測量執行時間
3. Environmental conditions: class assignment和input preparation都在測量執行時間前完成,以最小化環境對測量結果的影響
- Step 2: Apply post-processing
1. Cropping: 除去measurment artifacts (e.g., interrupted by OS)或修剪到某個特定的threshold
2. Higher-order preprocessing: 根據採用的統計檢定,做某些higher-order preprocessing可能有好處。
- Step 3: Apply statistical test
- 用統計檢定拒絕虛無假設: 兩筆資料集的執行時間分布相同
- t-test: Welch’s t-test
- Non-parametric tests
### Student's t-distribution
- 在只知道樣本的情況下使用
- 自由度決定PDF的形狀,自由度越高越像常態分佈
- 統計量$t=\frac{\bar{X}_n-\mu}{S_n /\sqrt{N}}$
- $N$為樣本數
- $\mu$為母體平均數
- $\bar{X}_n$為樣本平均數
- $S_n$為樣本標準差
#### Welch's t-test
- Student's t-test的變化版,在兩樣本有不一樣的變異數及樣本數量的情況下更可靠
- 統計量 $t=\frac{\bar{X}_1-\bar{X}_2}{\sqrt{s^2_{\bar{X}_1}+s^2_{\bar{X}_1}}}$
- $\bar{X}_i$: 第i種樣本的平均數
- $s_{\bar{X}_1}=\frac{s_i}{N_i}$: 第i種樣本的標準誤差 (standard error)
- $s_i=\sqrt{\frac{1}{N_i-1}\sum^N_{j=1}(X_{ij}-\bar{X_i})^2}$: Corrected sample standard deviation
### 補齊lab0-c處理percentile的程式碼
將[oreparaz/dudect](https://github.com/oreparaz/dudect)中處理percentile的程式碼補進lab0-c的dudect裡即可,注意percentiles的記憶體空間要記得釋放。push後即可看到卡比。

## 研讀 Linux 核心原始程式碼的 lib/list_sort.c 並嘗試引入到 lab0-c 專案
### 研讀[lib/list_sort.c]((https://github.com/torvalds/linux/blob/master/lib/list_sort.c))
#### list_cmp_func_t cmp
```c
typedef int __attribute__((nonnull(2,3))) (*list_cmp_func_t)(void *,
const struct list_head *, const struct list_head *);
```
- `list_cmp_func_t cmp`
- is a pointer to function that takes a pointer to void and two pointers to const struct list_head which are non-NULL, returning int.
- 功用:自定義的排序關係 (ordering relation)
- 如果要把串列排序成遞增
- return > 0: 把a排在b後面 (a > b)
- return <= 0: 把a排在b前面,或是保留相對順序 (a <= b)
- 讓`list_sort`是stable sort
#### merge
- 給定自定義的compare function,合併兩個sublists`a`和`b`
- tail使用pointer to pointer的技巧
- 視覺化`merge`裡會發生什麼事,假設現在要合併兩個sorted sublists `a=[1,3]`和`b=[2]`成一個遞增的串列
```graphviz
digraph merge{
rankdir=LR
graph [fontname="DFKai-SB"];
node [fontname="DFKai-SB"];
edge [fontname="DFKai-SB"];
subgraph sg1 {
nil[label = "NIL",shape=point]
head[shape=plaintext]
tail[shape=plaintext]
tail->head->nil
}
subgraph sgb{
b[label="b", shape=plaintext]
ln2[label = "{<value> 2|{<next> next}}", shape=record]
nil2[label = "",shape=point]
ln2:next->nil2
b->ln2:value
}
subgraph sga{
a[label="a" shape=none]
ln1[label = "{<value> 1|{<next> next}}", shape=record]
ln3[label = "{<value> 3|{<next> next}}", shape=record]
nil1[label = "" shape=point]
a->ln1:value
ln1:next->ln3
ln3:next->nil1
}
}
```
- 第一個迭代`cmp(priv, a, b) <= 0`成立:
- `head`會指向兩個sublist中最小的節點`a`,並且`head`會成為合併後的第一個節點。
- `tail`從指向`head`轉為指向`a->next`
- `a`指向下一個節點
```graphviz
digraph merge{
rankdir=LR
graph [fontname="DFKai-SB"];
node [fontname="DFKai-SB"];
edge [fontname="DFKai-SB"];
subgraph sgb{
b[label="b", shape=plaintext]
ln2[label = "{<value> 2|{<next> next}}", shape=record]
nil2[label = "",shape=point]
ln2:next->nil2
b->ln2:value
}
subgraph sga{
a[label="a" shape=none]
ln1[label = "{<value> 1|{<next> next}}", shape=record]
ln3[label = "{<value> 3|{<next> next}}", shape=record]
nil1[label = "" shape=point]
}
head[shape=plaintext]
tail[shape=plaintext]
head->ln1
tail->ln1:next
ln1:next->ln3
a->ln3
ln3:next->nil1
}
```
- 第二個迭代`cmp(priv, a, b) > 0`成立:
- `*tail = b`: 讓`tail`所指向的`next`指向下一個要加入的節點,這邊是`b`
- `tail`從指向`a->next`轉為指向`b->next`
- `b`指向下一個節點
```graphviz
digraph merge{
rankdir=LR
graph [fontname="DFKai-SB"];
node [fontname="DFKai-SB"];
edge [fontname="DFKai-SB"];
subgraph sgb{
b[label="b", shape=plaintext]
ln2[label = "{<value> 2|{<next> next}}", shape=record]
nil2[label = "",shape=point]
ln2:next->nil2
}
subgraph sga{
a[label="a" shape=none]
ln1[label = "{<value> 1|{<next> next}}", shape=record]
ln3[label = "{<value> 3|{<next> next}}", shape=record]
nil1[label = "" shape=point]
}
head[shape=plaintext]
tail[shape=plaintext]
head->ln1
tail->ln2:next
ln1:next->ln2
a->ln3
ln3:next->nil1
b->nil2
}
```
- 因為`b`已經是NULL,`if (!b)`成立,表示`b`已經沒有結點要合併,直接讓`tail`接上`a`剩下的節點即可。
```graphviz
digraph merge{
rankdir=LR
graph [fontname="DFKai-SB"];
node [fontname="DFKai-SB"];
edge [fontname="DFKai-SB"];
subgraph sgb{
b[label="b", shape=plaintext]
ln2[label = "{<value> 2|{<next> next}}", shape=record]
nil2[label = "",shape=point]
}
subgraph sga{
a[label="a" shape=none]
ln1[label = "{<value> 1|{<next> next}}", shape=record]
ln3[label = "{<value> 3|{<next> next}}", shape=record]
nil1[label = "" shape=point]
}
head[shape=plaintext]
tail[shape=plaintext]
head->ln1
tail->ln2:next
ln1:next->ln2
a->ln3
ln2:next->ln3
ln3:next->nil1
b->nil2
}
```
- 可以獲得結論:`head`會是已合併串列的第一個節點,並且是`merge`的回傳值;`tail`從第一個節點加入後指向已合併串列的最後一個節點的`next`。
#### merge_final
合併最後兩個sorted sublists,並且重新連結prev以復原雙重鏈結串列。
#### list_sort
- 核心思想:維持$2:1$的sublist size,當有兩個$2^k$大小的sublist,且蒐集到第三個大小為$2^k$的sublist時才進行合併。
- 在排序過程中,所有的串列都是單向鏈結且最後一個節點的next指向NULL。
- 變數`pending`:
- is a prev-linked "list of lists" of sorted sublists awaiting further merging.
- 排序好的子串列大小必為2的冪
- 不會有超過2個同樣大小的sublist
- 變數`count`:
- 目前pending lists的元素個數
- 每當增加`count`時,會使某個位元變成1(假設是第k個),並且第k-1到第0個位元變成0。這時候會合併兩個大小為$2^k$的sublists成一個$2^{(k+1)}$的sublist (counts第一次要到達$2^{(k+1)}$的時候不合併 i.e, 所有位元都是1).
```c
do {
size_t bits;
struct list_head **tail = &pending;
/* Find the least-significant clear bit in count */
for (bits = count; bits & 1; bits >>= 1)
tail = &(*tail)->prev;
/* Do the indicated merge */
if (likely(bits)) {
struct list_head *a = *tail, *b = a->prev;
a = merge(priv, cmp, b, a);
/* Install the merged result in place of the inputs */
a->prev = b->prev;
*tail = a;
}
/* Move one element from input list to pending */
list->prev = pending;
pending = list;
list = list->next;
pending->next = NULL;
count++;
} while (list);
```
- 這邊觀察merge前後(如果需要)`count`和`pending`的狀態,以利了解程式碼。紅色標示`bits`會停在哪個位元(i.e., `tail`會停在pending的哪個sorted sublist),以及下次迭代會合併的兩個sublists大小。假設我們有16個節點要排序:
| Iteration | 迭代開始時的 counts | Merge? | 迭代結束時的counts | 迭代結束時的pending |
| --------- |:---------------------------:|:------:|:------:| ------------------------------ |
| 0 | 0 | X | 1 | 1 |
| 1 | 1 | X | 10 | <font color=red>1-1</font> |
| 2 | 1<font color=red>0</font> | V | 11 | 1-2 |
| 3 | 11 | X | 100 | <font color=red>1-1</font>-2 |
| 4 | 10<font color=red>0</font> | V | 101 | 1-<font color=red>2-2</font> |
| 5 | 1<font color=red>0</font>1 | V | 110 | <font color=red>1-1</font>-4 |
| 6 | 11<font color=red>0</font> | V | 111 | 1-2-4 |
| 7 | 111 | X | 1000 | <font color=red>1-1</font>-2-4 |
| 8 | 100<font color=red>0</font> | V | 1001 | 1-<font color=red>2-2</font>-4 |
| 9 | 10<font color=red>0</font>1 | V | 1010 | <font color=red>1-1</font>-4-4 |
| 10 | 101<font color=red>0</font> | V | 1011 | 1-2-<font color=red>4-4</font> |
| 11 | 1<font color=red>0</font>11 | V | 1100 | <font color=red>1-1</font>-2-8 |
| 12 | 110<font color=red>0</font> | V | 1101 | 1-<font color=red>2-2</font>-8 |
| 13 | 11<font color=red>0</font>1 | V | 1110 | <font color=red>1-1</font>-4-8 |
| 14 | 111<font color=red>0</font> | V | 1111 | 1-2-4-8 |
| 15 | 1111 | X | 10000 | <font color=red>1-1</font>-2-4-8 |
- 把所有節點都加進pending後`do-while`迴圈就會結束,這時候還不一定是完整的sorted list,所以需要把`pending`所有的sorted sublist合併。
```c
/* End of input; merge together all the pending lists. */
list = pending;
pending = pending->prev;
for (;;) {
struct list_head *next = pending->prev;
if (!next)
break;
list = merge(priv, cmp, pending, list);
pending = next;
}
```
- 假設要排序一個8個節點的鏈結串列`[3 6 7 5 1 4 8 2]`,`do-while`迴圈結束時的狀態是
```graphviz
digraph
{ rankdir=LR
node[shape=box]
nil1[label="" shape=none]
nil2[label="" shape=none]
nil3[label="" shape=none]
nil4[label="" shape=none]
nil5[label="" shape=none]
p[label="pending" shape=plaintext]
2
8
1 4
3 5 6 7
2->8->1->3->nil5 [label=prev]
p->2
2->nil1 [label=next]
8->nil2 [label=next]
1->4->nil3 [label=next]
3->5->6->7->nil4 [label=next]
{rank=same 2 8 1 3 nil5}
}
```
- `for`迴圈結束時的狀態
```graphviz
digraph
{ rankdir=LR
node[shape=box]
nil1[label="" shape=none]
nil2[label="" shape=none]
nil3[label="NULL" shape=none]
nil4[label="" shape=none]
p[label="pending" shape=plaintext]
l[label="list" shape=plaintext]
n[label="next" shape=plaintext]
2
8
1 4
3 5 6 7
l->1
p->3
n->nil3
1->3->nil3 [label=prev]
1->2->4->8->nil1 [label=next]
3->5->6->7->nil2 [label=next]
{rank=same 1 3 nil3}
}
```
- 最後需要用`merge_final`合併`pending`和`list`,並且重新建立`prev`以復原雙重鏈結。
```c
/* The final merge, rebuilding prev links */
merge_final(priv, cmp, head, pending, list);
```
### 引入list_sort到lab0-c專案
1. 將`list_sort.h`、`list_sort.c`放在專案根目錄下
- `list_sort.h`
- 需要補充定義在`/include/linux/compiler.h`的巨集`likely()`、`unlikeky()`以及`u8`。
- 為了支援在`qtest`的使用,修改`list_sort`的宣告。
```diff
+ #define likely(x) __builtin_expect(!!(x), 1)
+ #define unlikely(x) __builtin_expect(!!(x), 0)
+ typedef uint8_t u8;
- __attribute__((nonnull(2,3)))
- void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp)
+ __attribute__((nonnull(2))) void list_sort(void *priv,
+ struct +list_head *head,
+ bool descend);
```
- `list_sort.c`
- 實作compare function
- `q_less(a,b)`< 0時表示`a < b`,`a`要優先加入已排序的串列,最後會得到遞增數列。
- `q_greater(a,b)` < 0時表示`a > b`,`a`要優先加入已排序的串列,實作只需要交換`q_less(a,b)`的順序。最後會得到遞減數列。
```diff
+ static inline int q_less(void *priv,
+ const struct list_head *a,
+ const struct list_head *b)
+ {
+ element_t *elem_a = list_entry(a, element_t, list);
+ element_t *elem_b = list_entry(b, element_t, list);
+ return strcmp(elem_a->value, elem_b->value);
+ }
+ static inline int q_greater(void *priv,
+ const struct list_head *a,
+ const struct list_head *b)
+ {
+ return q_less(priv, b, a);
+ }
```
- `list_sort()`需要補上判斷使用哪個compare function的表達式。
```diff
__attribute__((nonnull(2))) void list_sort(void *priv,
struct list_head *head,
bool descend)
{
...
+ list_cmp_func_t q_cmp = descend ? q_greater : q_less;
...
do{
...
if (likely(bits)) {
...
- a = merge(priv, cmp, b, a);
+ a = merge(priv, q_cmp, b, a);
...
}
...
} while (list);
...
for (;;) {
...
- list = merge(priv, cmp, pending, list);
+ list = merge(priv, q_cmp, pending, list);
...
}
- merge_final(priv, cmp, head, pending, list);
+ merge_final(priv, q_cmp, head, pending, list);
}
```
3. `qtest.c`
- 為了未來可擴充不同排序演算法,事先對演算法排編號,並定義一個靜態全域變`sort`,在`do_sort`裡面就可以依據`sort`的值搭配`switch`來切換 (預設就是`q_sort`)。
- 在`console_init()`裡加上`sort`參數後,在命令列就可以使用`option sort [value]`來取代`sort`的值。
```diff
+ #include "list_sort.h"
+ /* Which sorting algorithm will be used? */
+ #define LISTSORT 1
+ static int sort = 0;
bool do_sort(int argc, char *argv[])
{
...
if (current && exception_setup(true)) {
- q_sort(current->q, descend);
+ switch (sort) {
+ case LISTSORT:
+ list_sort(NULL, current->q, descend);
+ break;
+ default:
+ q_sort(current->q, descend);
+ break;
+ }
}
...
}
static void console_init()
{
+ add_param("sort", &sort, "Specify the sorting algorithm.", NULL);
}
```
5. Makefile的dependency需要加上`list_sort.o`
```diff
- linenoise.o web.o
+ linenoise.o web.o list_sort.o
```
- 發送commit時出現靜態分析失敗,錯誤訊息說在`list_sort.c`的`q_less`出現Null pointer dereference,但`list_entry`是`container_of`的包裝,`0`只是當作運算元而不會真正去存取,因此cppcheck誤判`list_entry`有dereference `0`。
> 參考:[Linux 核心原始程式碼巨集: container_of](https://hackmd.io/@sysprog/linux-macro-containerof)
```
list_sort.c:16:25: error: Null pointer dereference: (struct element_t*)0 [nullPointer]
element_t *elem_a = list_entry(a, element_t, list);
^
```
* 查閱[Cppcheck manual](https://cppcheck.sourceforge.io/manual.pdf),提及可以直接使用註解作inline suppression (p.17):
```
You can suppress a warning aaaa with:
// cppcheck-suppress aaaa
```
* 加上`// cppcheck-suppress nullPointer`後能夠順利commit。
```diff
static inline int q_less(void *priv,
const struct list_head *a,
const struct list_head *b)
{
+ // cppcheck-suppress nullPointer
element_t *elem_a = list_entry(a, element_t, list);
+ // cppcheck-suppress nullPointer
element_t *elem_b = list_entry(b, element_t, list);
return strcmp(elem_a->value, elem_b->value);
}
```
P.S. 在`script/pre-commit.hook`裡也能看到對`queue.c`和`qtest.c`有一樣的抑制。
### 比較效能
[list_sort論文研讀](https://hackmd.io/@ri_0LQgKRHOwpfTXUW3Rxg/By7w2_Oaa)
## References
1. [Merge Sort 與它的變化](https://hackmd.io/@lambert-wu/list-merge-sort)
2. [2022q1 Homework1 (lab0) by LJP-TW](https://hackmd.io/@LJP/SyRFuull9#2022q1-Homework1-lab0)
3. [Linux 核心原始程式碼巨集: container_of](https://hackmd.io/@sysprog/linux-macro-containerof)
4. [Cppcheck manual](https://cppcheck.sourceforge.io/manual.pdf)