[Linux2025q1: Quiz 2](https://hackmd.io/@sysprog/linux2025-quiz2)
## 測驗 1
測驗 1 的重點在快速排序,故先回顧何為快速排序。
:::success
快速排序基於分治 (Divide and Conquer),係透過選定一個基準值 (pivot),將資料分成左右兩部分(陣列、鏈結串列...),再以遞迴呼叫 (recursive calls) 或[迭代 (iterate)](https://hackmd.io/@sysprog/linux2025-quiz1#%E6%B8%AC%E9%A9%97-3) 形式依序對子部分進行相同操作。直至最後子部分的長度為 0 或 1。
接著進行合併操作,根據區分資料的方式 (位置交換或是移動到新開記憶體)會有不同方式,前者是就地 (in-place) 操作,因此不需要再進行合併,後者則需依照
註: 有關迭代式鏈結串列的快排實作,可以參見 [Linux2025q1: Quiz1 Write-Up](https://hackmd.io/@dennis40816/linux2025q1-quiz1-writeup)。
:::
現在我們來看關鍵程式:
```c
static void list_quicksort(struct list_head *head);
{
struct list_head list_less, list_greater;
struct listitem *pivot;
struct listitem *item = NULL, *is = NULL;
if (list_empty(head) || list_is_singular(head))
return;
INIT_LIST_HEAD(&list_less);
INIT_LIST_HEAD(&list_greater);
/* Section A */
pivot = AAAA(head, struct listitem, list);
BBBB(&pivot->list);
list_for_each_entry_safe (item, is, head, list) {
if (cmpint(&item->i, &pivot->i) < 0)
list_move_tail(&item->list, &list_less);
else
CCCC(&item->list, &list_greater);
}
/* Section B */
list_quicksort(&list_less);
list_quicksort(&list_greater);
/* Section C */
DDDD(&pivot->list, head);
EEEE(&list_less, head);
FFFF(&list_greater, head);
}
```
:::spoiler 可能選項
- `list_add`
- `list_add_tail`
- `list_del`
- `list_for_each_entry_reverse`
- `list_for_each_entry`
- `list_first_entry`
- `list_move`
- `list_move_tail`
- `list_splice`
- `list_splice_tail`
- `list_cut_position`
:::
整段程式採遞迴呼叫,大致可分為 A、B、C 三個部分,其中 B 是遞迴呼叫部分,後面不贅述。
:::info
Section A
:::
**AAAA**
參考值通常由頭、尾、中間或隨機選擇,而根據 `pivot` 型別,可以判斷 ++AAAA++ 要返回的是元素的指針,選項中只有 `list_first_entry` 有關。故選 `list_first_entry`。
**BBBB**
在鏈結串列中選完 `pivot` 後通常會將其移除鏈結串列中。因此推斷 ++BBBB++ 應該是移出或移動操作,因為參數只有一個,故選 `list_del`。
**CCCC**
位於 `list_for_each_entry_safe` 中,++CCCC++ 對應將鏈結串列中的一個元素 ++移動到++ `list_greater` 這個鏈結串列的頭或尾的操作。且移動操作應該使元素保持原本的順序,故選 `list_move_tail`。
:::info
Section B
:::
**DDDD - FFFF**
++DDDD++ - ++FFFF++ 可以一起觀察,首先當做完上述的操作,head 應該是一個指向自己的 `list_head`,我們現在要將結果添加回去。針對單個元素有以下選擇:
- `list_add`
- `list_add_tail`
- `list_move`
- `list_move_tail`
由於 `pivot` 現在是獨立的元素(沒有 `head`),所以應該使用 `list_add` 或 `list_add_tail`,雖然意思沒有差,但前者短,故 ++DDDD++ 選 `list_add`。
++EEEE++ 和 ++FFFF++ 是將排序好的兩個鏈結串列串接到 `head` 上,由於是按順序,所以 `list_less` 接到 `head` 的頭部(在 `pivot` 前),而 `list_greater` 應該接到 `pivot` 的後面,所以要連到 `head` 尾部。移動整個 `list` 是 `list_splice` 相關的函式。故:
- ++EEEE++: `list_splice`
- ++FFFF++: `list_splice_tail`
::::info
:::spoiler 延伸閱讀 🧐
看完主要程式,我們來看看輔助程式。
`getnum` 作為獲得隨機數的函式,使用了三個線性[同餘產生器 (Linear Congruential Generators, LCG)](https://link.springer.com/chapter/10.1007/978-1-4615-2317-8_3)。 LCG 的公式如下:
$$
N_{j+1} \equiv (A \cdot N_j + B) \mod{M}
$$
此處的 $A$ 、 $B$ 選擇了特殊的數字,這些數值在 [Wichmann–Hill](https://en.wikipedia.org/wiki/Wichmann%E2%80%93Hill),由 Brian Wichmann 和 David Hill 於 1982 年所提出的。將這三個 LCG 的結果進行 ++相加++ 後作為隨機數,該數列的週期為 $6.95 \times 10^{12}$。
:question: 考題中則對三個 LCG 進行異或操作得到隨機數,這個的週期又是多少呢?
:::
::::
## 測驗 2
[平方根](https://hackmd.io/@sysprog/sqrt)是日常運算重要的一環,以下測驗為對其的探討:
首先討論計算前導零 (leading zero) 的實作 `cl2`,其使用遞迴手法呼叫。
### clz2
`clz2` 的處理邏輯如下:
1. 將當前的數字以位元為單位,切成一半。
2. 若 upper 是 0,下一次檢查 lower 部分,且紀錄 ==upper 位元數 (16 >> c)== ^[1]^。
3. 直到剩下 ==2 個== 位元,回傳結果,結束遞迴呼叫。
以下程式實現以上邏輯:
```c
static const int mask[] = {0, 8, 12, GGGG};
static const int magic[] = {HHHH, 1, 0, IIII};
unsigned clz2(uint32_t x, int c)
{
if (!x && !c)
return 32;
uint32_t upper = (x >> (16 >> c));
uint32_t lower = (x & (0xFFFF >> mask[c]));
if (c == JJJJ)
return upper ? magic[upper] : KKKK + magic[lower];
return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + LLLL);
}
```
讓我們一行行解析:
```c
if (!x && !c)
return 32;
```
若程式剛開始 ( `c` 是 `0` ) ,且 `x` 也是 `0`,直接返回全部都是 0 的前導零個數。
```c
uint32_t upper = (x >> (16 >> c));
uint32_t lower = (x & (0xFFFF >> mask[c]));
```
創立兩個變數 `upper` 和 `lower`,`upper` 是將 `x` 右移 `(16 >> c)`,對於第一輪來說:
```c=
uint32_t upper = (x >> (16 >> 0))
uint32_t lower = (x & (0xFFFF >> 0));
```
第 1 行將目前的數字左半部分右移,完全覆蓋掉右部分,以此得到 `upper`。
第 2 行就是僅保留下半部分。
若進一步思考在下一步,可以發現 `mask[c]` 在下一步 ( `c=1` )變成 `8`,再下一步會變成 `12`,這個規律是甚麼呢?
是**上次的位移量加上 `(16 >> c)`** ^[2]^ 。
因此我們可以推斷出 `mask[3]` 應該為 12 + 2 = 14。
```c
if (c == JJJJ)
return upper ? magic[upper] : KKKK + magic[lower];
return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + LLLL);
```
接下來最為精華的部分,首先是看到 `if` 分支,進入後回傳一個 ==固定的值== ( `magic` 必定是數值,沒有函式再被呼叫),這也代表進入 `if` 後,流程就剩下一系列的返回階段了。而題目說明了結束條件:
>遞迴呼叫 clz2() 函式,直到僅剩 2 位元(bits)需要檢查 leading zero,然後回傳結果。
說明當 `c` 與 `JJJJ` 相等剛好剩下 2 bits,結合上述提到:
>若 upper 是 0,下一次檢查 lower 部分,且紀錄 ==upper 位元數 (16 >> c)== ^[1]^。
得知所求即 `(16 >> c) == 3`。故得知 `JJJJ`^[1]^。
**magic**
接著,我們先看 `if` 分支內,此時要返回 `upper`、`lower` ++各剩下兩個 bits 時的前導零數量++。
如果 `upper` 非 `0`,直接返回 `magic[upper]`,此時 `upper` 只可能有三種可能 ^[3]^ :
- `0b01`: 有一個前導 `0`,對應到 `magic[1]` ➝ `1`
- `0b10`: 沒有前導 `0`,對應到 `magic[2]` ➝ `0`
- `0b11`: 沒有前導 `0`,對應到 `magic[3]` ➝ `0` ➝ `IIII`
那 `magic[0]` 呢? 我們知道該項只跟==三元運算符後半有關==,讓我們來思考甚麼時候會用 `magic[0]`。
只有一種,就是 `upper` 是 `0` (才會進入三元運算符的後半),而 `lower` 也是 `0` (`magic` 的 index),此時前導零數量為 `4` (剩下各兩 bits,都是 `0`) ^[4]^。
但應該要注意 `magic[0]` 不直接填 4 喔!!! 因為還有一項 `KKKK`,這項是常數代表的是 `upper` ==固定貢獻== 的前導零數量,也就是 `2` (才會進入後半部嘛!)。因此也能知道 `magic[0]` 也是 `2`。
:::warning
:warning: `KKKK` 也可以做為常數加入 `magic` 陣列嗎?
不行! 因為 `upper` 非零時也會用到。
:::
**recursive function calls**
最後一部分,是遞迴呼叫 `clz2` 的過程:
```c
return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + LLLL);
```
當 `upper` 非零,計算 `upper` 的 `clz2` 並返回 ( `c + 1` 是要再把 `upper` 切一半),若 `upper` 是零,則計算 `lower` 的 `clz2` 並加上 `upper` 全零的領導零數量( `16 >> c` )並返回。
由於 `lower` 也要切一半,所以 `LLLL` 同樣也是 `1` ^[5]^。
**Summary of clz2**
從上述可統整出以下資訊,++數字對應上方中的 ^[x]^++ :
1. `(16 >> c)` 是本輪切割完後的 bit 有多長。例如 `c` 是 `0` 時,則當輪分割完剩 16 bits。題目中提到==若位元剩兩位時==,結束繼續呼叫,對應到的是 `if (c == JJJJ)` 的判斷。
至此可以推斷 `JJJJ` 是當位元只剩下兩位時, `c` 的數值。僅 `c` 是 `3`,`(16 >> c)` 是 `2`,代表分割完就剩下兩位。故 `JJJJ` 是 `3`。
1. `mask` 陣列裡面放的數字,是 $mask[i] = mask[i-1] + (16 \gg c)$,其中 `mask[0]` 是 0。故 `GGGG` (`mask[3]`) 為 `14`。
1. 在進入 `if` 分支,若 `upper` 非 `0`,則只會有三種可能: `1`、`2`、`3`,對應的前導零數量為 `1`、`0`、`0`,其中最後一種可能對應到 `IIII`,故 `IIII` 為 `0`。
1. 只有當 `upper` 是 `0`,才可能會用到 `magic[0]`,且此時 `lower` 也是 `0`,所有 bits 都是 `0`,故知 `KKKK` 加上 `HHHH` 是 `4`。此處,`KKKK` 代表由全零的 `upper` 貢獻的前導 `0`,故 `KKKK` 是 `2`,因此 `HHHH` 也是 `2`。
1. 由於 `lower` 在遞迴中也要切一半,故 `LLLL` 是 `1`。
結束了嗎? 當然沒有,繼續看下去。
### sqrti
可參考[〈2024-02-25 問答簡記〉](https://hackmd.io/l4--gpsZQBiEI5LKS1lDvg#%E6%AA%A2%E8%A8%8E%E7%AC%AC%E4%BA%8C%E9%80%B1%E6%B8%AC%E9%A9%97%E9%A1%8C) 的第二週測驗題。
在一開始,利用剛剛實作的 `clz2` 實作 `clz64`:
```c
#define clz32(x) clz2(x, 0)
static inline int clz64(uint64_t x)
{
/* If the high 32 bits are nonzero, count within them.
* Otherwise, count in the low 32 bits and add 32.
*/
return (x >> 32) ? clz32((uint32_t) (x >> 32)) : clz32((uint32_t) x) + 32;
}
```
`sqrti` 實作如下:
```c
uint64_t sqrti(uint64_t x)
{
uint64_t m, y = 0;
if (x <= 1)
return x;
int total_bits = 64;
/* clz64(x) returns the count of leading zeros in x.
* (total_bits - 1 - clz64(x)) gives the index of the highest set bit.
* Rounding that index down to an even number ensures our starting m is a
* power of 4.
*/
int shift = (total_bits - 1 - clz64(x)) & MMMM;
m = 1ULL << shift;
while (m) {
uint64_t b = y + m;
y >>= NNNN;
if (x >= b) {
x -= b;
y += m;
}
m >>= PPPP;
}
return y;
}
```
說明指出:
>(total_bits - 1 - clz64(x)) gives the index of the highest set bit. Rounding that index down to an even number ensures our starting m is a power of 4.
該要求表明 `PPPP` 要是偶數 (0, 2, 4, 6),如何用 `&` 強制一個數字變成偶數呢?就是確保最低位是 `0`,因此 `PPPP` 的最簡表示為 `~1`。
剩下的 `NNNN` 和 `MMMM` 竟然要求是負數,暫時看不懂? :question:
**Answers**
- **GGGG**: 14
- **HHHH**: 2
- **IIII**: 0
- **JJJJ**: 3
- **KKKK**: 2
- **LLLL**: 1
- **MMMM**:
- **NNNN**:
- **PPPP**: ~1
## 測驗 3
參考 [〈Linux 核心的 hash table 實作〉](https://hackmd.io/@sysprog/linux-hashtable)
了解完 Linux 中雙向鏈結串列的原理,沒想到在雜湊表的 Linux 實作上,也有它的身影!