---
tags: Linux Kernal
---
# 2023q1 Homework1 (quiz1)
contributed by < `POCHUN-CHEN` >
[2023q1 第 1 週測驗題](https://hackmd.io/@sysprog/linux2023-quiz1)
## 測驗1
`struct item` 為參考 [list.h](https://github.com/sysprog21/lab0-c/blob/master/list.h)內的結構體,負責存值與順序的 `list` 分開寫,往後可以運用 [`contain_of`](https://hackmd.io/@sysprog/linux-macro-containerof) 。
```c
#include <stdint.h>
#include "list.h"
struct item {
uint16_t i;
struct list_head list;
};
```
```graphviz
digraph list{
rankdir=LR;
subgraph cluster_1 {
node [shape=record];
value1 [label="{i}"];
subgraph cluster_11 {
node [shape=record];
addr1 [label="{<prev> prev|<next> next}"];
label="struct list_head list";
}
label=item;
}
node [shape=none]
none1 [label = ""];
none2 [label = ""];
edge[weight=2];
none1:right:e -> addr1;
addr1:right:e -> none2;
edge[weight=1];
}
```
---
[為什麼要用 `static inline` ?](https://gcc.gnu.org/onlinedocs/gcc-4.9.4/gcc/Inline.html)
->主要目的為提升程式速度。
>the program behaves the same as if you had not used the inline keyword, except for its speed.
---
用以對節點內含值比較的函式:
```c
static inline int cmpint(const void *p1, const void *p2)
{
const uint16_t *i1 = (const uint16_t *) p1;
const uint16_t *i2 = (const uint16_t *) p2;
return *i1 - *i2;
}
```
`cmpint` 函數返回的整數值代表了比較 `p1` 和 `p2` 這兩個指向 `uint16_t` 數據的指標的結果。這種方式是用於標準庫函數 qsort 中,qsort 函數會根據 cmpint 函數的返回值來對數組進行排序。
| 比較結果 | 回傳 | Column 3 |
| -------- | -------- | -------- |
| `*i1` 大於 `*i2` | 正整數 | `p1` 排在 `p2` 後面 |
| `*i1` 等於 `*i2` | 0 | 排序不變 |
| `*i1` 小於 `*i2` | 負整數 | `p1` 排在 `p2` 前面 |
這樣可以實現**對任意類型**(因為有 `(const uint16_t *)` 指標轉型)的數據進行排序的功能。
Q: 為什麼要把兩個指標函式傳進 `cmpint` 函式,再用 `i1` 、 `i2` 來被賦值呢?
>`p1` 從指向 void 顯示轉型成 `(const uint16_t *)` (這邊是指轉換成指向 `const uint16_t` 的指標,會有這種括號寫法,是因為 `p1` 本來就是指標了)。
>再賦值給指向 `const uint16_t` 的 `i1` 。
>宣告 `i1` 、 `i2` 才不會影響到本來的。
:::info
上述問題
```c
static inline int cmpint(const void *p1, const void *p2)
{
return *((const uint16_t *) p1) - *((const uint16_t *) p2);
}
```
顯示指標轉型不會影響到原本 `p1` 、 `p2` 的值,因此這樣寫應該更精簡?
:::
顯式的指標型別轉換會更改指標指向型別的編碼方式嗎?
>顯式的指標型別轉換通常**不會**更改指標指向型別的編碼方式。指標的編碼方式(如指標的位元組長度、對齊方式等)是由編譯器和操作系統決定的,與指標指向的型別無關。因此,進行顯式的指標型別轉換時,==被指向的二進制內容不會改變==,只是指標的型別被視為不同的型別。把顯示指標轉型的指標解構後,會將指向的型別的二進位數值,利用轉換後的型別方式顯示值(可能會與原本不同,但是==二進位的部分是一樣==的,但有可能長度不同)
---
首先,來看看 `list_sort` 的程式碼
```c
if (list_empty(head) || list_is_singular(head))
return;
```
先檢查串列是否為空,或者是只有一個值(就不用排了)。
---
```c
struct list_head list_less, list_greater;
INIT_LIST_HEAD(&list_less);
INIT_LIST_HEAD(&list_greater);
```
接下來建立 `list_less` 、 `list_greater` ,等等來存放,比原串列中某個數值大 / 小的數值們。
接下來 `INIT_LIST_HEAD`,負責把新建立的 `list_head` 初始化,一定要把 `prev` / `next` 的前 / 後指標定義。
```c
static inline void INIT_LIST_HEAD(struct list_head *head)
{
head->next = head;
head->prev = head;
}
```
```graphviz
digraph structs {
node[shape=record]
struct0 [label="<prev> prev|<next> next"];
struct0:prev -> struct0[arrowhead=vee, tailclip=true, arrowsize=1];
struct0:next -> struct0[arrowhead=vee, tailclip=true, arrowsize=1];
}
```
補充說明,以 `list_less` 為例, `&list_less` 與 `&(list_less.prev)` 是同樣地址,因為結構體 `list_head` 內是按照 `*prev` 、 `*next` 排序,所以第一個結構的起始地址就代表結構體的地址。
:::info
是否可善用 Micro 來簡化程式碼?作法如下。
```c
LIST_HEAD(list_less);
LIST_HEAD(list_greater);
```
由於 `LIST_HEAD()` 本身就包含宣告、初始化,故可以簡化程式碼。
```c
#define LIST_HEAD(head) struct list_head head = {&(head), &(head)}
```
:::
`LIST_HEAD(list_less)` = `struct list head list_less` + `INIT_LIST_HEAD(&list_less)`。
---
```c
struct item *pivot = list_first_entry(head, AAA, BBB);
list_del(&pivot->list);
```
接下來就要把剛剛 `list_sort` 函式的輸入參數引進來比較排序。這時候 `item` 結構可以用來包含順序串列與其節點內該有的值。之後我們就要來看 `list_first_entry()` 代表什麼意思,參考[`list.h`](https://github.com/sysprog21/lab0-c/blob/master/list.h)中,可得到以下定義。
```c
/**
* list_first_entry() - Get first entry of the list
* @head: pointer to the head of the list
* @type: type of the entry containing the list node
* @member: name of the list_head member variable in struct @type
*
* Return: @type pointer of first entry in list
*/
#define list_first_entry(head, type, member) \
list_entry((head)->next, type, member)
```
這時可以查詢到 [`list_entry`](https://hackmd.io/@sysprog/c-linked-list#list_entry) 是 `container_of` 等價的包裝,符合以 list_ 開頭的命名慣例,此處的 entry 就是 list 內部的節點。
**特別注意:在 `list_` 系列的 `member` ==都限定在 `list_head member`== 。**
:::info
如果 `list_` 系列的 `member` ,故意輸入 `struct` 中,不是 `list_head member` 的成員,會不會發生錯誤?
:::
接著了解 [`container_of(ptr, type, member)`](https://hackmd.io/@sysprog/linux-macro-containerof#container_of-實作手法) 的實作手法。
程式碼是從 struct 中的 member 推算出原本 struct 的位址。
* @ptr: 指向 `member` 的指標
* @type: 結構物件的資料型態
* @member: 結構物件中,成員變數名稱
* Return: 會回傳一個指向包含指定ptr的type結構的指標。
```graphviz
digraph list{
rankdir=LR;
subgraph cluster_0 {
node [shape=record];
list0 [label="{<prev> prev|<next> next}"];
label="struct list_head head";
}
subgraph cluster_1 {
node [shape=record];
value1 [label="{i}"];
subgraph cluster_11 {
node [shape=record];
list1 [label="{<prev> prev|<next> next}"];
label="struct list_head list";
}
label=item;
}
list0:prev:w -> list1:next:e[arrowhead=vee, tailclip=true, arrowsize=1];
list0:next -> list1:prev[arrowhead=vee, tailclip=true, arrowsize=1];
list1:prev:w -> list0:next:e[arrowhead=vee, tailclip=true, arrowsize=1];
list1:next:e -> list0:prev:w[arrowhead=vee, tailclip=true, arrowsize=1];
}
```
這裡需要討論一下細節,由目前的程式碼中,我們只知道,我們把 `list_head` 結構的 `head` 傳進來 `list_sort` 函式。但熟知 linux 風格的 linked list ,我們知道在這個未經過排序的串列中,也是由 `item` 結構,包裹著決定串列順序的 `list_head` linked list 結構。並且第一個 `list_head` 沒有被包含在 `item` 結構。
由此我們可以知道, `list_first_entry(head, AAA, BBB)` 可以看成 `container_of((head)->next, type, member)` ,藉此得到原本串列的第一個 `item` 記憶體位置。再將其賦值給 `pivot` 。
#### AAA,BBB = ?
由 `container_of(ptr, type, member)` 可知,
* AAA應為type,在這個程式內應為`item`。
* BBB應為member,在這個程式內應為`list` (因為我們知道linux 風格的 linked list ,是由 `item` 結構包裹,所以成員函數可以用 `item` 內部的 `struct list_head list` )。
再將 `pivot` 利用 `list_del(&pivot->list)` 從 `head` 這個串列中移除。
>這邊 `list_del(&pivot->list)` 應該也可以寫成 `list_del(&head->next)` 但是這樣很不直觀,我們都寫了 `pivot` ,就直接專注在該節點就好。
---
#### CCC = ?
觀察到後面出現 `cmpint` 比較大小的函示,可以推斷 CCC 應該為推進整個串列比較的迴圈函式。為了在宣告完 `pivot` 後,確保可以比較到串列中的每一個值,再參考輸入參數後,使用函式 `list_for_each_entry_safe`。
* CCC為`list_for_each_entry_safe`。
```c
#define list_for_each_entry_safe(entry, safe, head, member) \
for (entry = list_entry((head)->next, __typeof__(*entry), member), \
safe = list_entry(entry->member.next, __typeof__(*entry), member); \
&entry->member != (head); entry = safe, \
safe = list_entry(safe->member.next, __typeof__(*entry), member))d
```
透過額外的 `safe` 紀錄每個迭代 (iteration) 所操作的節點的下一個節點,因此目前的節點可允許被移走,其他操作則同為不可預期行為。
---
#### DDD,EEE = ?
當把 `head` 串列中的值比較大小後,移動到 `list_less` / `list_greater` 的尾部,使用函式 `list_move_tail` 。
* DDD 為 `list_move_tail` 。
* EEE 為 `list_move_tail` 。
---
```c
list_sort(&list_less);
list_sort(&list_greater);
```
這邊可以看出遞迴的結構,把原本串列按照數值大小分類。依序持續切到排序完成。
:::warning
比較[Merge Sort實作](https://hackmd.io/@sysprog/c-linked-list#Merge-Sort-的實作)。
:::
---
#### FFF = ?
當分類完比 `pivot` 大或小所組成的兩串列後,且經由迭代即可完成排序。將 `pivot` 與排序後的串列,依序串成與原先串列長度一致的新串列。
將 `pivot` 加入 list 中的 head , `list_less` 用 `list_splice` 加在 `head` 後; `list_gearter` 用 `list_splice_tail` 加在 `head` 前。
* FFF為 `list_splice_tail` 。
---
補充資料:
#### [Timsort](https://en.wikipedia.org/wiki/Timsort)
:::spoiler ChatGPT對於Timsort的解釋
Timsort是一種在排序演算法中廣泛使用的混合排序演算法。它由Tim Peters於2002年開發,並在Python中成為預設排序演算法。Timsort結合了插入排序和歸併排序的優點,因此具有高效的排序速度和佔用較小的內存空間。
Timsort的基本思路是將數組分為若干個小塊,對每個小塊進行插入排序,然後將這些小塊進行歸併排序。在實際應用中,Timsort還有一些優化措施,例如檢測已經有序的塊,跳過已經有序的塊,使用二分查找尋找插入位置等等。
Timsort的時間複雜度為O(n log n),在最壞情況下的時間複雜度為O(n log n),空間複雜度為O(n)。Timsort是一種穩定的排序演算法,可以處理大規模的數據集,被廣泛應用於Java、Python等編程語言的標準庫中。
:::
#### [Introsort](https://en.wikipedia.org/wiki/Introsort)
:::spoiler ChatGPT對於Introsort的解釋
:::
---
### 延伸問題
- [x] 1. 解釋程式碼運作原理
1. Check linked list in ```head``` has more than two entry and not empty. If not return it.
2. Use ```INIT_LIST_HEAD``` to initialize two list structure we made.
3. Use ```list_first_entry``` to get first entry to set it as pivot.
4. Delete the pivot from linked list(use ```list_del```)
5. Compare through all list. Use ```strncmp``` to compare ```item->value``` to ```pivot->value```, and seperate them to each ```list_less``` and ```list_greater```.
6. Call ```q_sort``` to regress ```list_less``` & ```list_greater```.
7. Add pivot to list
8. Splice ```list_less``` before pivot in list.
9. Splice ```list_greater``` after pivot in list.
- [ ] 2. 針對 Quick sort 提出程式碼的改進方案並實作
- [ ] 3. 引入 hybrid sort 策略並在 Linux 核心風格的 circular doubly-linked list 實作,需要量化性能表現並探討不同資料集 (data set) 的影響
- [ ] 4. BONUS: 嘗試對 Linux 核心提出排序程式碼改進的貢獻
## 測驗2
參考 [list.h](https://github.com/sysprog21/lab0-c/blob/master/list.h)
### 答案解析
#### GGGG,HHHH = ?
由程式碼可知GGGG,此程式碼與遞迴版本對應,在比對`pivot`前,必需先確保可以比較到串列中的每一個值,使用函式`list_for_each_entry_safe`。
當把串列中的值補在`list_less`和`list_greater`的尾部,使用函式`list_move_tail`。
* GGGG為`list_for_each_entry_safe`。
* HHHH為`list_move_tail`。
#### IIII,JJJJ,KKKK = ?
* IIII為`&stack[top--]`。
* JJJJ為`&stack[top--]`。
* KKKK為`XXX`。
---
### 延伸問題
- [ ] 1. 解釋程式碼運作原理,指出設計和實作的缺失
- [ ] 2. 比較測驗 1 和測驗 2 的程式碼,設計實驗來分析二者效能,解釋為何非遞迴的實作未能總是比遞迴的實作快
- [ ] 3. 提出效能改進方案,特別是避免依賴 `MAX_DEPTH`
- [ ] 4. 引入 Introsort ,也就是 quick sort 和 heap sort (或其他可避免前者 $O(n^2)$ 最差時間複雜度的演算法)。加入 heapsort 的原因為,當 quicksort 迭代 $2∗log(n)$ 次後還排序完成,那就很可能會得到 $O(n^2)$ 時間複雜度。此時切換為 heap sort 能將其時間複雜度控制在 $O(nlogn)$
## 測驗3
### 答案解析
#### LLLL = ?
#### MMMM,PPPP = ?
---
### 延伸問題
- [ ] 1. 解釋程式碼運作原理,指出其實作缺陷並改進
- [ ] 2. 在 XOR linked list 的基礎,實作測驗 1 和 2 提及的快速排序演算法,注意要引入你改進效能的版本
- [ ] 3. 修改 `xorlist_destroy`,使其不急著釋放記憶體,而搭配 atexit(3) 並在程式即將結束執行時,才批次處理資源釋放