# 2024q1 Homework2 (quiz1+2)
**contributed by < `RRbell1027` >**
## 第 1 週測驗題
### 測驗 1
首先看到鏈結串列結構體:
```c
typedef struct __node {
struct __node *left, *right;
struct __node *next;
long value;
} node_t;
```
* 由於測驗一中沒有用到 `left` 和 `right` ,所以暫且忽略。
```graphviz
digraph example {
rankdir=LR;
node[shape=none, label=<<table border="0" cellspacing="0">
<tr>
<td port="value" border="1">value</td>
<td port="next" border="1">next</td>
</tr>
</table>>];
a:next -> b;
b:next -> c;
c:next -> null;
}
```
#### AAAA:
分析鏈結串列的操作函式:
```c
node_t *list_tail(node_t **left)
{
while ((*left) && (*left)->next)
left = &(AAAA);
return *left;
}
```
* 從函數名稱,推測是回傳**鏈結串列的最後一個節點的指標**。
* 常見的演算法為:利用 `next` 指標判斷當前節點是否為最後一個節點,進而用 `while` 迴圈從第一個指標訪問到最後一個指標。
* 因此,迴圈中指派給 `left` 的值為**指向下個節點的指標**的地址 `&((*left)->next)`。
```graphviz
digraph example {
rankdir=LR;
node[shape=none, label=<<table border="0" cellspacing="0">
<tr><td port="value" border="1">value</td>
<td port="next" border="1">next</td></tr>
</table>>];
headptr[shape=rect, label="head"]
left[shape=rect, label="left"]
null[label="null"]
left -> headptr [constraint=false];
headptr -> a;
a:next -> b;
b:next -> c;
c:next -> null;
}
```
```graphviz
digraph example {
rankdir=LR;
node[shape=plaintext, label=<<table border="0" cellspacing="0">
<tr><td port="value" border="1">value</td>
<td port="next" border="1">next</td></tr>
</table>>];
headptr[shape=rect, label="head"]
left[shape=rect, label="left"]
null[label="null"]
left -> a:next [constraint=false];
headptr -> a;
a:next -> b;
b:next -> c;
c:next -> null;
}
```
#### BBBB:
分析以下鏈結串列操作函式:
```c
int list_length(node_t **left)
{
int n = 0;
while (*left) {
++n;
left = &(BBBB);
}
return n;
}
```
* 從函式名稱推測功能為回傳鏈結串列長度。
* 每訪問一個節點計數器 `n` 加一。
* 與上題相同,left 要被指派**指向下一個節點的指標**的地址 `&((*left)->next)`。
#### CCCC, DDDD, EEEE:
* 首先分析非遞迴快速排序法[Optimized QuickSort — C Implementation (Non-Recursive)](https://alienryderflex.com/quicksort/):
* 函數利用 `begin` 和 `end` 兩個堆疊儲存遞迴呼叫時的引數,分別代表著該次呼叫要處理的陣列的第一個索引和最後一個索引。
```diff
- sort(arr, beg, l);
- sort(arr, r, end);
+ beg[i+1]=L+1; end[i+1]=end[i]; end[i++]=L; /* beg[i] doesn't change */
```
* 接者分析鏈結串列的改寫:
```c
list_add(n->value > value ? &right : &left, n);
```
* 鏈結串列並非像陣列一樣通過元素互換將元素二分到陣列前後。而是將鏈結串列**拆分成兩個鏈結串列**,在各自做排序。
```graphviz
digraph {
rankdir=LR;
node[shape=rect];
pivot[shape=none]
3->2->4->5->1;
pivot->3[constraint=false]
}
```
* 變成:
```graphviz
digraph {
node[shape=none];
{node[shape=rect]; rank="same"; 2->1; 3; 4->5;}
pivot->3;
left->2;
right->4
}
```
* 結合上述分析,`DDDD` 和 `EEEE` 為 `left` 和 `right` 鏈結串列的最後一個節點的指標。
```graphviz
digraph {
node[shape=none];
{node[shape=rect]; rank="same"; 2->1; 3; 4->5;}
pivot->3;
left->2;
right->4;
DDDD->1;
EEEE->5;
}
```
```c
begin[i] = left;
end[i] = list_tail(&left);
begin[i + 1] = pivot;
end[i + 1] = pivot;
begin[i + 2] = right;
end[i + 2] = lsit_tail(&right);
```
* 而 `CCCC` 則是 p 的下一個節點的指標:
```c
while (p) {
node_t *n = p;
p = p->next;
list_add(n->value > value ? &right : &left, n);
}
```
#### list_head API 實作版:
* 改寫 `node_t` 結構體,讓 `list.h` 支援 `node_t`:
```diff
typedef struct __node {
struct __node *left, *right;
- struct __node *next;
+ struct list_head *list;
long value;
} node_t;
```
* 刪除測驗 1 程式碼中的 `end` 堆疊:
* 這個堆疊只用來存放鏈結串列尾節點,大可以在下一次的迭代中尋找。
* 利用 `list_is_singular` 和 `list_empty` 取代 `begin[i] == end[i]` 的判斷。
```diff
- begin[i] = left;
- end[i] = list_tail(&left);
- begin[i + 1] = pivot;
- end[i + 1] = pivot;
- begin[i + 2] = right;
- end[i + 2] = list_tail(&right);
+ stk[i] = left;
+ stk[i+1] = mid;
+ stk[i+2] = right;
```
* 利用 `list_for_each_entry_safe` 取代 `while(p)` 迴圈。
```diff
- while (p) {
- node_t *n = p;
- p = p->next;
+ node_t *node, *safe;
+ list_for_each_entry_safe (node, safe, li, list) {
```
* 快速排列排列完整程式碼:
```c
void q_sort(struct list_head *head, bool descend)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
/* q_size returns the size of @head */
int i = 0, size = q_size(head);
LIST_HEAD(result);
struct list_head **stk = malloc(size * sizeof(struct list_head *));
stk[i] = head;
while (i >= 0) {
struct list_head *li = stk[i];
if (list_empty(li)) {
i--;
continue;
}
if (list_is_singular(li)) {
list_add(li->next, &result);
i--;
continue;
}
struct list_head *left = malloc(sizeof(struct list_head));
INIT_LIST_HEAD(left);
struct list_head *mid = malloc(sizeof(struct list_head));
INIT_LIST_HEAD(mid);
struct list_head *right = malloc(sizeof(struct list_head));
INIT_LIST_HEAD(right);
node_t *pivot = list_first_entry(li, node_t, list);
list_del(&pivot->list);
list_add(&pivot->list, mid);
long value = pivot->value;
node_t *node, *safe;
list_for_each_entry_safe (node, safe, li, list) {
list_del(&node->list);
list_add(&node->list,
((value > node->value) ^ descend) ? left : right);
}
stk[i] = left;
stk[i+1] = mid;
stk[i+2] = right;
i += 2;
}
list_splice(&result, head);
}
```
---
### 測驗 2
#### AAAA:
```c
struct list_head *head;
struct list_head **tail = &head; /* AAAA */
/* merge two list */
return head;
```
* 因為中間合併演算法中沒有 `head` 只有 `tail` ,判斷 `tail` 被指派了 `head` 或 `head->next` 的地址。因為 `head` 尚未被分配空間,因此不可能是 `head->next`。
#### BBBB, CCCC:
* 這裡的合併邏輯類似 galloping mode ,比較兩鏈結串列的頭指標,將較小的(假設升冪排列)元素放進暫存鏈結串列中:
```graphviz
digraph {
rankdir = LR
{
node[shape=rect]; 1; 4; 5;
3[fillcolor=gray, style=filled];
2[fillcolor=gray, style=filled];
};
{node[shape=none]; a; b; tmp;};
tmp->1;
a->2->5;
b->3->4;
}
```
* 例如上圖,$2 < 3$,所以將 $2$ 加入到 `tmp` 鏈結串列的尾端。
```graphviz
digraph {
rankdir = LR
{
node[shape=rect]; 1; 4; 5;
3[fillcolor=gray, style=filled];
2[fillcolor=gray, style=filled];
};
{node[shape=none]; a; b; tmp;};
tmp->1->2;
a->5;
b->3->4;
}
```
* 接著解析程式碼。推測出 `tail` 是 tmp 的末節點的 `next` 指標。
* BBBB 可以是 `&(*tail)->next` 或 `&a->next` ,根據表單答案選後者。
```c
if (cmp(priv, a, b) <= 0) {
*tail = a;
tail = &a->next; /* BBBB */
a = a->next;
```
* CCCC 同理:
```c
*tail = b;
tail = &b->next; /* CCCC */
b = b->next;
```
#### DDDD, EEEE:
* 註釋寫的很清楚,這兩行程式碼是為了將鏈結串列變回環狀鏈結串列。
* 也就是頭接尾,尾接頭。
```c
/* The final links to make a circular doubly-linked list */
tail->next /* DDDD */ = head;
head->prev /* EEEE */ = tail;
```
#### FFFF:
* `merge_force_collapse` 會確保剩下 $1$ 或 $2$ 個鏈結串列。
* 若剩下 $1$ 個,放回 `head`。 $2$ 個則還要再合併一次。
```c
if (stk_size <= 1 /* FFFF */) {
build_prev_link(head, head, stk0);
return;
}
merge_final(priv, cmp, head, stk1, stk0);
```
#### 分析 Timsort 程式碼:
* 程式碼利用排序時不需要用到 `prev` 的特性,把 `prev` 指標發揮的淋漓盡致。
* 先將單一鏈結串列拆分成數個已排序的鏈結串列,並利用頭節點的 `prev` 將其串在一起。
* 每個排序好的鏈結串列的大小會存放在第二個節點的`prev`。
* 例如:
```graphviz
digraph {
rankdir=LR;
node[shape=rect];
4->5->6->2->3->1 [label=next];
1->3->2->6->5->4 [label=prev];
}
```
* 在不經過 `merge_collapse` 下,將變成:
```graphviz
digraph {
rankdir=LR;
node[shape=rect];
4->5->6 [label=next]; 2->3[label=next];
1->2->4[constraint=false, label=prev];
{
node[shape=none];
size1[label=3];
size3[label=2];
null1[label=null];
null2[label=null];
null3[label=null];
}
5->size1[label=prev, constraint=false];
3->size3[label=prev, constraint=false];
6->null1[label=next];
3->null2[label=next];
1->null3[label=next];
}
```
* 過程中,將滿足條件的鏈結串列先合併。
* 合併結束後,呼叫 `build_prev_link` 將 `prev` 指標恢復正常。
---
## 第 2 周測驗題
### 測驗 1
#### AAAA:
* 看完[Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable)後觀察函式:
```graphviz
digraph {
rankdir=LR
splines=line;
node[shape=record];
h[label="h|<p>first"];
first_node[label="<h>first_node|{<p>pprev|<n>next}"];
n[label="<h>n|{<p>pprev|<n>next}"];
other[label="..."]
h:p->first_node:h;
first_node:p->h:p;
first_node:n->other;
}
```
* 函式回傳後,期望結果為:
```graphviz
digraph {
rankdir=LR
splines=line;
node[shape=record];
h[label="h|<p>first"];
first_node[label="<h>first_node|{<p>pprev|<n>next}"];
n[label="<h>n|{<p>pprev|<n>next}"];
other[label="..."]
h:p->n:h;
n:p->h:p;
n:n->first_node:h;
first_node:p->n:n;
first_node:n->other;
}
```
* 所以,結果為:
```c
if (h->first)
h->first->pprev = &n->next;
n->next = h->first; /* AAAA */
n->pprev = &h->first;
h->first = n;
```
#### BBBB:
```c
int hash = (num < 0 ? -num : num) % size;
hlist_for_each (p, BBBB) {
```
* 由這兩行可以看出,雜湊函式採用求餘法,其中 `size` 是雜湊表的長度, `num` 是鍵值,碰撞採用單向鏈結串列處理。
* 所以 `BBBB` 是該鍵值對應到的鏈結串列頭節點,也就是 `&heads[hash]` 。
#### CCCC:
```c
hlist_for_each (p, BBBB) {
struct order_node *on = CCCC(p, struct order_node, node);
```
* 從參數判斷這是 `container_of` 巨集的重新定義,往前翻找可以找到 `list_entry` 。
#### DDDD:
```c
int hash = (val < 0 ? -val : val) % size;
hlist_add_head(&on->node, DDDD);
```
* 同 `BBBB` 。
#### 程式碼邏輯:
* 從前序排列的第一項可以得到子樹的樹根,第二項為右子樹樹根。
* 從中序排列可以得到左子樹和右子樹的節點和節點數。
* 藉由左子樹節點數,可以回推左子樹樹根。
```graphviz
digraph {
pre[shape=none, label=<<table border="0" cellspacing="0">
<tr>
<td>preorder: </td>
<td border="1" bgcolor="lightblue">3</td>
<td border="1" bgcolor="green">9</td>
<td border="1" bgcolor="green">5</td>
<td border="1" bgcolor="red">20</td>
<td border="1" bgcolor="red">15</td>
<td border="1" bgcolor="red">7</td>
</tr>
</table>>];
in[shape=none, label=<<table border="0" cellspacing="0">
<tr>
<td>inorder: </td>
<td border="1" bgcolor="green">9</td>
<td border="1" bgcolor="green">5</td>
<td border="1" bgcolor="lightblue">3</td>
<td border="1" bgcolor="red">15</td>
<td border="1" bgcolor="red">20</td>
<td border="1" bgcolor="red">7</td>
</tr>
</table>>];
}
```
```graphviz
digraph {
3->9->5;
3->20->15
20->7
}
```
* 利用雜湊表紀錄中序排列,以降低演算法搜尋中序時的時間複雜度。
### 測驗 2
#### EEEE:
```c
void hlist_del(struct hlist_node *n)
{
struct hlist_node *next = n->next, **pprev = n->pprev;
*pprev = next;
if (next)
EEEE = pprev;
}
```
* 從函式名稱推測功能為刪除節點,例如:
```graphviz
digraph {
rankdir=LR
splines=line;
node[shape=record];
prev[label="<h>prev_node|{<p>pprev|<n>next}"];
n[label="<h>n|{<p>pprev|<n>next}"];
next[label="<h>next_node|{<p>pprev|<n>next}"];
prev:n->n:h;
n:p->prev:n;
n:n->next:h;
next:p->n:n;
}
```
* 變成
```graphviz
digraph {
rankdir=LR
splines=line;
node[shape=record];
prev[label="<h>prev_node|{<p>pprev|<n>next}"];
n[label="<h>n|{<p>pprev|<n>next}"];
next[label="<h>next_node|{<p>pprev|<n>next}"];
prev:n->next:h;
next:p->prev:n;
}
```
所以 `EEEE` 為 `next->pprev` 。
#### FFFF:
```c
struct list_head *pos, *n;
LRUNode *cache = list_entry(pos, LRUNode, FFFF);
```
* LRUNode 的定義
```c
typedef struct {
int key;
int value;
struct hlist_node node;
struct list_head link;
} LRUNode;
```
* 唯一符合資料型態為 `list_head` 的子物件為 `link` 。
#### GGGG:
```c
struct list_head *pos, *n;
list_for_each_safe (pos, n, &obj->dhead) {
LRUNode *cache = list_entry(pos, LRUNode, link /*FFFF */);
list_del(GGGG);
free(cache);
}
free(obj);
```
* 程式碼會先把當前節點從鏈結串列中刪除再釋放記憶體空間。
* `GGGG` 是當前節點的指標(地址),也就是 `pos` 或 `&cache->link` 。
#### HHHH:
```c
struct hlist_node *pos;
LRUNode *cache = list_entry(pos, LRUNode, HHHH);
```
* 同 `FFFF` ,找出 `LRUNode` 中資料型態為 `hlist_node` 的子物件,也就是 `node` 。
#### IIII:
```c
list_move(IIII, &obj->dhead);
```
* 程式碼目的是將最新使用過的雜湊值放到鏈結串列的頭節點,如此一來,尾節點就會是最久沒用過的節點。
* 因此,`IIII` 就是 `lRUCacheGet` 函式找到的節點的 `list_head` 指標(地址): `&cache->link` 。
#### JJJJ:
```c
struct hlist_node *pos;
LRUNode *c = list_entry(pos, LRUNode, JJJJ);
```
* 同 `HHHH` 。
#### KKKK:
* 同 `IIII` ,`KKKK` 為 `&c->link` 。
#### 程式碼邏輯:
* LRUCache 目標是將最久沒使用的鍵值移除快取。
* 利用佇列紀錄鍵值最新的使用順序(在此,使用佇列這個名詞是因為 FIFO 的特性),雜湊表降低檢索的時間複雜度。
* 一個節點同時擁有佇列和雜湊表的鏈結串列。
* 例如:黑色為雜湊表的鏈結串列邊,紅色為佇列的邊:
```graphviz
digraph {
rankdir=LR
hlist[shape=record, label="<t>|<b>"];
hlist:t->2->4;
hlist:b->1;
2->1->4[color=red]
}
```
* 此圖鍵值的最新使用順序可以用佇列看出:$4$->$1$->$2$ 。優先移除 $4$ 的節點。
#### Linux Kernel 對 LRU Cache 的實作:
* 觀察到 [linux/include/linux/lru_cache.h](https://github.com/torvalds/linux/blob/master/include/linux/lru_cache.h#L164) 函式庫對 LRU Cache 的實作。
```c
struct lru_cache {
/* the least recently used item is kept at lru->prev */
struct list_head lru;
struct list_head free;
struct list_head in_use;
```
* `lru_cache` 將資源分成了三類(註釋取自 `struct lc_element`):
```c
list is on one of three lists:
in_use: currently in use (refcnt > 0, lc_number != LC_FREE)
lru: unused but ready to be reused or recycled
(lc_refcnt == 0, lc_number != LC_FREE),
free: unused but ready to be recycled
(lc_refcnt == 0, lc_number == LC_FREE),
```
* 提供了以下幾個主要的 API:
* lc_create: 創建 LRU Cache ,並將所有資源放入 `free` 。
* lc_get: 請求特定資源,如果雜湊表中沒有,回收一個資源並分派出去,放入 `in_use`。
* lc_put: 資源使用完畢但是未來可能用到,將資源放入 `lru` 。
* lc_del: 資源使用完畢且不會再用到,將資源 free() 並放入 `free` 。
* 其中, `lc_get` 在分派新資源時,會調用 `lc_prepare_for_change` ,優先選擇 `free` 中的資源,若 `free` 為空,從 `lru` 選擇最後一個(最久沒被用過的)資源。
```c
if (!list_empty(&lc->free))
n = lc->free.next;
else if (!list_empty(&lc->lru))
n = lc->lru.prev;
```
* 舉個例子:DFTL (Demand-based Flash Translation Layer)
* 開機時,電腦會將**邏輯地址與物理地址的對照表 (FTL)** 從固態硬碟加載到記憶體中,加速電腦對固態硬碟資料的讀取。
* 然而,隨著固態硬碟容量的增加, FTL 的大小以達到 GB 級別,對記憶體造成很大的負擔。
* 因此,如今普遍只存放部分 FTL ,並利用 LRU Cache 機制更新記憶體上 FTL 內容。
* 若短時間內對同一筆資料做多次讀取,依舊可以保持原先的效率。
### 測驗 3
#### AAAA:
* 根據註釋,`__ffs` 函式功能是找出字元串從右數來的第一個 $1$ 。
* 程式碼邏輯類似二分搜尋法,利用字元串遮罩將字元串切半,判斷目標在字元串的左半還是右半 。
* 假設字串最大長度為 $8$
```graphviz
digraph {
a[shape=record, label="{x|x|x|x|x|x|x|x}"];
and[shape=none, label="&"]
b[shape=record, label="{0|0|0|0|1|1|1|1}"];
equal[shape=none, label="="]
c[shape=record, label="{0|0|0|0|x|x|x|x}"];
}
```
* 這個遮罩下,如果沒有 $1$ 在右半,字元串會變成 $0$ 。
* 類似的邏輯常用在子網路 ip 的區分上,利用子網路遮罩判斷兩個 ip 是否在同一個子網路底下。
```c
#if BITS_PER_LONG == 64
if ((word & AAAA) == 0) {
num += 32;
word >>= 32;
```
* 回到測驗程式碼,字串長度為 $64$ ,因此 `AAAA` 為 `0xffffffff` 。
#### BBBB:
* 從函式名稱推測為將特定字元清零。
```c
unsigned long mask = BIT_MASK(nr);
unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr);
*p &= BBBB;
```
* `BIT_MASK` 會創造所有字元為 $0$ ,只有目標位元為 $1$ 的字元串。
* 目標是讓目標字元和 $0$ 做 AND 清零,其餘和 $1$ 做 AND 保持不變。
* 所以 `BBBB` 為 `~mask` 。
#### CCCC:
* 函式功能為找出字元串第 n 個 $1$ 的位置。
* 程式碼為了減少負擔,會在鎖定目標在長度 `BITS_PER_LONG` 的範圍中之後,再使用 `fns` 函式的 `while` 迴圈尋找。
* 方法為:先尋找前 `BITS_PER_LONG` 位元中有幾個 $1$ ,如果少於要求,再往後搜尋。
* 例如,假設一次搜索範圍為 $4$ 位元, n 為 $3$ :
```graphviz
digraph {
rankdir=LR;
bit[shape=none, label=<<table border="0" cellspacing="0">
<tr>
<td border="1">0</td>
<td border="1">1</td>
<td border="1">1</td>
<td border="1">0</td>
<td border="1">0</td>
<td border="1">1</td>
<td border="1">0</td>
<td border="1">1</td>
<td bgcolor="gray" border="1">1</td>
<td bgcolor="gray" border="1">0</td>
<td bgcolor="gray" border="1">1</td>
<td bgcolor="gray" border="1">0</td>
</tr>
</table>>];
}
```
* 這裡只找到 $2$ 個 $1$ ,所以向後搜尋:
```graphviz
digraph {
rankdir=LR;
bit[shape=none, label=<<table border="0" cellspacing="0">
<tr>
<td border="1">0</td>
<td border="1">1</td>
<td border="1">1</td>
<td border="1">0</td>
<td bgcolor="gray" border="1">0</td>
<td bgcolor="gray" border="1">1</td>
<td bgcolor="gray" border="1">0</td>
<td bgcolor="gray" border="1">1</td>
<td border="1">1</td>
<td border="1">0</td>
<td border="1">1</td>
<td border="1">0</td>
</tr>
</table>>];
}
```
* 累積已經有 $4$ 個 $1$ 了,所以確認目標在這 $4$ 個位元內。
```c
if (sz CCCC BITS_PER_LONG) \
tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz);
```
* 這兩行指令發生在最後一次範圍搜索時:
* 如果 `size` 不是 `BITS_PER_LONG` 的倍數,就用 `fns` 函式
* 如果 `size` 是 `BITS_PER_LONG` 的倍數,表示上一次的搜索是最後一次,於是將 `tmp` 全部設成 $1$,`fns` 回傳 $0$ ,減少運算次數。
* 因此, `CCCC` 為 `%` 。