# 2024q1 Homework2 (quiz1+2)
contributed by < [yy214123](https://github.com/yy214123) >
## 開發環境
```shell
$ 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): 20
On-line CPU(s) list: 0-19
Vendor ID: GenuineIntel
Model name: 13th Gen Intel(R) Core(TM) i5-13600KF
CPU family: 6
Model: 183
Thread(s) per core: 2
Core(s) per socket: 14
Socket(s): 1
Stepping: 1
CPU max MHz: 5100.0000
CPU min MHz: 800.0000
BogoMIPS: 6988.80
Virtualization: VT-x
L1d: 544 KiB (14 instances)
L1i: 704 KiB (14 instances)
L2: 20 MiB (8 instances)
L3: 24 MiB (1 instance)
NUMA node(s): 1
NUMA node0 CPU(s): 0-19
```
## 填寫 Google 表單時獲得的啟發
### Dangling Pointer
在 [基於 C 語言標準研究與系統程式安全議題](https://hackmd.io/@sysprog/c-std-security) 的主題(二)中討論了有關 Dangling Pointer 的議題,其中提到:**在使用指標時,安全起見應該要記得將不用的指標在 free() 後設為 NULL。** 重新審視了我在 [lab0-c](https://github.com/yy214123/lab0-c) 的程式後,發現部份函式都有釋放指標的行為,而釋放後我卻沒有將其指向 NULL,如此會有造成 Dangling Pointer 的風險。
對此我進行了以下修改:
```diff
free(head);
+ head = NULL;
free(new);
+ new = NULL
```
## 第一周測驗
### 測驗一
```clike
node_t *list_tail(node_t **left)
{
while ((*left) && (*left)->next)
left = &((*left)->next);
return *left;
}
int list_length(node_t **left)
{
int n = 0;
while (*left) {
++n;
left = &((*left)->next);
}
return n;
}
```
在這兩個函式中,我們處理的是一個鏈結串列結構,其中 node_t 是串列節點的結構定義。這些函式通過間接指標(即指向指針的指針)作為參數來操作鏈表。允許函式直接修改指向鏈表頭部的指針。
```list_tail``` 的目的是找到並回傳串列的最末節點。它透過迴圈遍歷串列,每次迭代都檢查當前節點的下一個節點是否存在。當下一個節點不存在時,當前節點即為串列的最末節點。
```list_length``` 的目的是計算串列中的節點數量。透過遍歷串列的每個節點,每遍歷一個節點就將計數器 n 增加 1。當遍歷完所有節點後,函式回傳計數器 n 的值即為串列的長度。
```clike
while (i >= 0) {
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);
}
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);
left = right = NULL;
i += 2;
} else {
if (L)
list_add(&result, L);
i--;
}
}
*list = result;
```
這邊花了很多時間來紙筆追蹤,一開始搞不懂他的邏輯,一直卡在 else 判別式的地方。後續仔細觀察 begin 各個索引指向的串列後,才逐漸理解整個架構的運作模式。
可將每三個 begin 索引看作一組處理單元,其中每組的中心點是一個基準值 (pivot),這個節點左側的串列包含所有小於基準值的節點,而右側則包含所有大於基準值的節點。演算法的執行流程傾向於先處理基準值右側的部分,也就是那些大於基準值的節點。對於大於基準值的每個部分,演算法繼續將其細分為更小的三元組(左、中、右),依此類推,直到只剩下單一節點。一旦某個大於基準值的部分被細分到只包含一個節點,該節點就會被加入到最終的 result 中 。
:::success
延伸問題:
1.解釋上述程式碼的運作原理,提出改進方案並予以實作。
:::
:::success
延伸問題:
2.使用 Linux 核心風格的 List API 改寫上述程式碼,並針對鏈結串列,提出可避免最差狀況的快速排序實作,應設計效能評比的測試程式。
:::
#### 考慮時間複雜度
在 **worst case** 情況下,會因為 pivot 的緣故,導致快速排序的時間複雜度為 $\mathcal{O}(n^2)$,可以透過選擇更好的 pivot 來避免此問題。
**解決方法一**:以 Middle of three 挑選 pivot
```graphviz
digraph G {
node [shape=record, height=.1];
array [label="<f0> 4|<f1> 7|<f2> 2|<f3> 5|<f4> 9|<f5> 1|<f6> 6", style=filled, color=lightgrey];
pivot [label="Pivot: 5", shape=box, style=filled, color=lightblue];
start [shape=none, label=""];
end [shape=none, label=""];
start -> array:f0 [label="First"];
array:f3 -> pivot [label="Middle of three\n(4, 5, 6)"];
end -> array:f6 [label="Last"];
}
```
**解決方法二**:以 Median of Medians 挑選 pivot
```graphviz
digraph G {
node [shape=record, height=.1];
subgraph cluster_0 {
label="Array";
color=lightgrey;
array [label="1|2|3|4|5|6|7|8|9|10|11|12|13|14|15", style=filled];
}
subgraph cluster_1 {
label="Groups of 5";
color=lightgrey;
group1 [label="1|2|3|4|5", style=filled];
group2 [label="6|7|8|9|10", style=filled];
group3 [label="11|12|13|14|15", style=filled];
}
subgraph cluster_2 {
label="Medians of Groups";
color=lightgrey;
median1 [label="3", shape=box, style=filled, color=lightblue];
median2 [label="8", shape=box, style=filled, color=lightblue];
median3 [label="13", shape=box, style=filled, color=lightblue];
}
subgraph cluster_3 {
label="Median of Medians";
color=lightgrey;
pivot [label="Pivot: 8", shape=box, style=filled, color=red];
}
array -> group1 [label="Divide"];
array -> group2;
array -> group3;
group1 -> median1 [label="Find median"];
group2 -> median2;
group3 -> median3;
median1 -> pivot [label="Choose median"];
median2 -> pivot;
median3 -> pivot;
}
```
#### 考慮空間複雜度
原先程式碼使用了額外的 ```begin```、```end```來追蹤子串列,又使用 ```left```、```right```來存分割後的結果,能以 in-place partitioning 的角度出發去改善。
### 測驗二
:::success
延伸問題:
1.解釋上述程式碼的運作原理,提出改進方案並予以實作。
:::
在 `timsort` 函式中,第一行宣告了:
```c
stk_size = 0;
```
接著去搜尋其定義方式:
```c
static size_t stk_size;
```
原先不懂 size_t 為何,在 `stddef.h` 中找到了其定義:
```c
typedef __SIZE_TYPE__ size_t;
```
這點在 [C99](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf) 之 [6.5.3.4] 第4點也有提及。
:::info
The value of the result is implementation-defined, and its type (an unsigned integer type)
is size_t, defined in <stddef.h> (and other headers).
:::
延伸問題中提到使用 Linux 核心風格的 List API 改寫,一開始判斷是否為 empty 的判斷式就能改進:
```diff
- if (head == head->prev)
+ if (!head || list_empty(head) || list_is_singular(head))
return;
```
首先是 `find_run` 這個函式,其功能為在串列中尋找連續遞增或遞減的序列(run),並根據這個序列是遞增還是遞減來相應地處理鏈表。如果序列是遞減的,函式會將其內的節點反轉,使之成為遞增序列,此處有可以改善的空間,上述反轉的操作可以搭配 `q_reverse` 來使程式碼更精簡。
以一個簡單的範例來追蹤程式碼:
```graphviz
digraph G {
rankdir =LR
node [shape=record];
Node0 [label="<f0> head"];
Node1 [label="<f0> 5"];
Node2 [label="<f0> 2"];
Node3 [label="<f0> 3"];
Node4 [label="<f0> 7"];
Node5 [label="<f0> 6"];
Node6 [label="<f0> 1"];
Node7 [label="<f0> head"];
// Node0 <-> Node1
Node0:f0 -> Node1:f0 [color="red"];
Node1:f0 -> Node0:f0 ;
// Node1 <-> Node2
Node1:f0 -> Node2:f0 [color="red"];
Node2:f0 -> Node1:f0 ;
// Node2 <-> Node3
Node2:f0 -> Node3:f0 [color="red"];
Node3:f0 -> Node2:f0 ;
// Node3 <-> Node4
Node3:f0 -> Node4:f0 [color="red"];
Node4:f0 -> Node3:f0 ;
// Node4 <-> Node5
Node4:f0 -> Node5:f0 [color="red"];
Node5:f0 -> Node4:f0 ;
// Node5 <-> Node6
Node5:f0 -> Node6:f0 [color="red"];
Node6:f0 -> Node5:f0 ;
// Node6 <-> Node7
Node6:f0 -> Node7:f0 [color="red"];
Node7:f0 -> Node6:f0 ;
}
```
待補
:::success
延伸問題:
2.將改進過的 Timsort 實作整合進 sysprog21/lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。
:::
> [commit ce2f8f4](https://github.com/sysprog21/lab0-c/commit/ce2f8f4888535b7e7010d4c52417b94cd4a9bc7c)
先前已完成透過 option 命令修改 sort_type 之值,根據其值選擇要使用 merge_sort 或 list_sort 進行佇列排序操作。此次更新加入 timsort 並對程式碼做了下方更動。
```diff
- add_param("sort_type", &sort_type, "0 for merge_sort, 1 for list_sort",NULL);
+ add_param("sort_type", &sort_type, "0 for merge_sort, 1 for list_sort, 2 for timsort",NULL);
```
```diff
/* Sort elements of queue in ascending order */
extern int sort_type;
void q_sort(struct list_head *head, bool descend)
{
switch (sort_type) {
case 0:
merge_sort(head);
break;
case 1:
list_sort(head, cmp);
break;
+ case 2:
+ timsort(head, cmp);
+ break;
default:
printf("Unknown sort type.\n");
break;
}
if (descend)
q_reverse(head);
}
```
詳細的效能評比,我將其更新於 [2024q1 Homework1 (lab0) - 嘗試將 Timsort 引入到 lab0-c](https://hackmd.io/yI-Rh8l7Q9uzWhQgBQbsnQ?view#%E5%98%97%E8%A9%A6%E5%B0%87-Timsort-%E5%BC%95%E5%85%A5%E5%88%B0-lab0-c) 中。
## 第二周測驗
[2024q1 第 2 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz2)
### 測驗 `1`
:::success
解釋程式碼的運作原理。
:::
```clike
static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)
{
if (h->first)
h->first->pprev = &n->next;
n->next = AAAA;
n->pprev = &h->first;
h->first = n;
}
```
**`AAAA` 應填入 `h->first`。**
假設目前串列中還沒有節點(不會執行 if 判別式),根據:
```clike
static inline void INIT_HLIST_HEAD(struct hlist_head *h)
{
h->first = NULL;
}
```
所以初始 first 會指向 NULL:
```graphviz
digraph G {
rankdir = LR;
splines = false;
node[shape = "record"]
list_head[label = "<m>hlist_head | <n>first"]
NULL[shape = plaintext, label = "NULL", group = list]
list_head:n -> NULL;
}
```
現在執行第一次的 `hlist_add_head`,因 `AAAA` 填入 `h->first` 所以:
```graphviz
digraph G {
rankdir = LR;
splines = false;
node[shape = "record"]
list_head[label = "<m>hlist_head | <n>first"]
NULL[shape = plaintext, label = "NULL", group = list]
list_head:n -> NULL;
}
```
```graphviz
digraph G {
rankdir = LR;
splines = false;
node[shape = "record"]
node_1[label = "<m>hlist_node_1 | {<p>pprev | <n>next}", group = list];
NULL[shape = plaintext, label = "NULL", group = list]
node_1:n -> NULL;
}
```
接著執行`n->pprev = &h->first;`
```graphviz
digraph G {
rankdir = LR;
splines = false;
node[shape = "record"]
list_head[label = "<m>hlist_head | <n>first"]
node_1[label = "<m>hlist_node_1 | {<p>pprev | <n>next}", group = list];
NULL[shape = plaintext, label = "NULL", group = list]
{rank = "min" list_head}
list_head -> node_1 [
weight = 10, style = invis
]
node_1:p -> list_head:n;
node_1:n -> NULL;
}
```
最後執行 `h->first = n;`
```graphviz
digraph G {
rankdir = LR;
splines = false;
node[shape = "record"]
list_head[label = "<m>hlist_head | <n>first"]
node_1[label = "<m>hlist_node_1 | {<p>pprev | <n>next}", group = list];
NULL[shape = plaintext, label = "NULL", group = list]
{rank = "min" list_head}
list_head -> node_1 [
weight = 10, style = invis
]
list_head:n -> node_1:m;
node_1:p -> list_head:n;
node_1:n -> NULL;
}
```
若再次執行 `hlist_add_head` 插入 `hist_node_2` 節點,這次會執行 if 判別式部份。
`h->first->pprev` 即 `hist_node_1` 的 pprev,將其指向 `hist_node_2` 的 next。
```graphviz
digraph G {
rankdir = LR;
splines = false;
node[shape = "record"]
list_head[label = "<m>hlist_head | <n>first"]
node_1[label = "<m>hlist_node_1 | {<p>pprev | <n>next}", group = list];
node_2[label = "<m>hlist_node_2 | {<p>pprev | <n>next}", ];
NULL[shape = plaintext, label = "NULL", group = list]
{rank = "min" list_head}
list_head -> node_1 [
weight = 10, style = invis
]
list_head:n -> node_1:m;
node_1:p -> node_2:n;
node_1:n -> NULL;
}
```
接著依序執行:
```clike
n->next = h->first;
n->pprev = &h->first;
h->first = n;
```
```graphviz
digraph G {
rankdir = LR;
splines = false;
node[shape = "record"]
list_head[label = "<m>hlist_head | <n>first"]
node_1[label = "<m>hlist_node_1 | {<p>pprev | <n>next}", group = list];
node_2[label = "<m>hlist_node_2 | {<p>pprev | <n>next}", ];
NULL[shape = plaintext, label = "NULL", group = list]
{rank = "min" list_head}
list_head -> node_2 -> node_1 [
weight = 10, style = invis
]
list_head:n -> node_2:m;
node_2:p -> list_head:n
node_2:n -> node_1:m
node_1:p -> node_2:n;
node_1:n -> NULL;
}
```
以相同範例,Input: preorder = [3,9,20,15,7], inorder = [9,3,15,20,7]。
首先呼叫 `buildTree`,參數代入上方範例的 preorder 及 inorder,兩者 size 均為 5,前 3 行程式碼如下:
```clike
struct hlist_head *in_heads = malloc(5 * sizeof(*in_heads));
for (int i = 0; i < 5; i++)
INIT_HLIST_HEAD(&in_heads[i]);
```
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
nullNode [label="NULL", shape=plaintext];
map [label="in_heads |<ht0>first |<ht1>first |<ht2>first |<ht3>first |<ht4>first"];
map:ht0 -> nullNode;
map:ht1 -> nullNode;
map:ht2 -> nullNode;
map:ht3 -> nullNode;
map:ht4 -> nullNode;
}
```
接著是
```clike
for (int i = 0; i < inorderSize; i++)
node_add(inorder[i], i, inorderSize, in_heads);
```
在 `node_add` 中會去計算 inorder 的各元素應放入哪格 in_heads 中:
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
map [label="in_heads |<ht0>first |<ht1>first |<ht2>first |<ht3>first |<ht4>first"];
node[shape=none]
null1 [label=NULL]
null2 [label=NULL]
null3 [label=NULL]
null4 [label=NULL]
null5 [label=NULL]
subgraph cluster_1 {
style=filled;
color=lightgrey;
node [shape=record];
hn1 [label="hlist_node | {<prev>pprev | <next>next}"];
label="idx:0;val:3"
}
subgraph cluster_2 {
style=filled;
color=lightgrey;
node [shape=record];
hn2 [label="hlist_node | {<prev>pprev | <next>next}"];
label="idx:1;val:9"
}
subgraph cluster_3 {
style=filled;
color=lightgrey;
node [shape=record];
hn3 [label="hlist_node | {<prev>pprev | <next>next}"];
label="idx:2;val:20"
}
subgraph cluster_4 {
style=filled;
color=lightgrey;
node [shape=record];
hn4 [label="hlist_node | {<prev>pprev | <next>next}"];
label="idx:3;val:15"
}
subgraph cluster_5 {
style=filled;
color=lightgrey;
node [shape=record];
hn5 [label="hlist_node | {<prev>pprev | <next>next}"];
label="idx:4;val:=7"
}
map:ht0 -> hn3;
hn3:prev:s -> map:ht0;
hn3:next -> hn4;
hn4:prev:s -> hn3:next
hn4:next -> null1;
map:ht1 -> hn5;
hn5:prev:s ->map:ht1;
hn5:next -> null2;
map:ht2 -> hn1;
hn1:prev:s ->map:ht2;
hn1:next -> null3;
map:ht3 -> hn2;
hn2:prev:s ->map:ht3;
hn2:next -> null4;
map:ht4 -> null5;
}
```
起初用上方的圖去進行後續的 dfs 遞迴,發現結果與預期的有落差,重新看過程式碼後發現我 idx 的編號是依照 prorder 的元素順序,正確來說在 `INIT_HLIST_HEAD` 及 `node_add` 是以 inorder 的元素順序才對,故上方的圖片應如下修正:
```graphviz
digraph G {
rankdir=LR;
node[shape=record];
map [label="in_heads |<ht0>first |<ht1>first |<ht2>first |<ht3>first |<ht4>first"];
node[shape=none]
null1 [label=NULL]
null2 [label=NULL]
null3 [label=NULL]
null4 [label=NULL]
null5 [label=NULL]
subgraph cluster_1 {
style=filled;
color=lightgrey;
node [shape=record];
hn1 [label="hlist_node | {<prev>pprev | <next>next}"];
label="idx:1;val:3"
}
subgraph cluster_2 {
style=filled;
color=lightgrey;
node [shape=record];
hn2 [label="hlist_node | {<prev>pprev | <next>next}"];
label="idx:0;val:9"
}
subgraph cluster_3 {
style=filled;
color=lightgrey;
node [shape=record];
hn3 [label="hlist_node | {<prev>pprev | <next>next}"];
label="idx:2;val:15"
}
subgraph cluster_4 {
style=filled;
color=lightgrey;
node [shape=record];
hn4 [label="hlist_node | {<prev>pprev | <next>next}"];
label="idx:3;val:20"
}
subgraph cluster_5 {
style=filled;
color=lightgrey;
node [shape=record];
hn5 [label="hlist_node | {<prev>pprev | <next>next}"];
label="idx:4;val:=7"
}
map:ht0 -> hn3;
hn3:prev:s -> map:ht0;
hn3:next -> hn4;
hn4:prev:s -> hn3:next
hn4:next -> null1;
map:ht1 -> hn5;
hn5:prev:s ->map:ht1;
hn5:next -> null2;
map:ht2 -> hn1;
hn1:prev:s ->map:ht2;
hn1:next -> null3;
map:ht3 -> hn2;
hn2:prev:s ->map:ht3;
hn2:next -> null4;
map:ht4 -> null5;
}
```
接著是 `dfs` 的部份,這邊我覺得蠻困難的,一開始對遞迴呼叫中傳入的參數運算很混亂,這邊以另一個例子來追蹤:
> preorder = [A, B, D, E, C, F, G]
> inorder = [D, B, E, A, F, C, G]
根據建構二元數的規則,我們可以得知根節點為 `A` ,先看第一個遞迴呼叫:
```clike
tn->left = dfs(preorder, pre_low + 1, pre_low + (idx - in_low), inorder,
in_low, idx - 1, in_heads, size);
```
* `pre_low + 1`:根節點 `A` 之左子樹其起始索引於 preorder 中的 `pre_low + 1`,即根節點 `A` 的下個位置。
* `idx - in_low`: `idx` 為 `A` 在 inorder 中的索引,經由 `idx - in_low` 可以計算出根節點 `A` 之左子樹節點總數(即 3 個)。
由上方兩點可知左子樹在 preorder 中從索引 `pre_low + 1`(即 1) 開始且長度為`idx - in_low` (即 3)。所以可以知道結束的索引位於 `pre_low + 1 + (idx - in_low) - 1`,將其化簡後可得 `pre_low + (idx - in_low)`。
```clike
tn->right = dfs(preorder, pre_high - (in_high - idx - 1), pre_high, inorder,
idx + 1, in_high, in_heads, size);
```
先將 `pre_high - (in_high - idx - 1)` 改為 `pre_high - (in_high - idx ) + 1`,我覺得這樣解釋比較清晰。
* `in_high - idx`: `idx` 為 `A` 在 inorder 中的索引,經由 `in_high - idx` 可以計算出根節點 `A` 之右子樹節點總數(即 3 個)。
所以由 `pre_high - (in_high - idx ) + 1` 可以得知右子樹在 preorder 的起始索引(即 4)。
綜合上述可以理解為每次呼叫遞迴時會傳入左右子樹的**起始索引**及**結束索引**。
:::success
#### 撰寫完整的測試程式。
:::
目前的設想是撰寫 preorder traversal 及 inorder traversal 的遞迴程式碼,倘若由 preorder 序列及 inorder 序列所構建的 BT 是正確的,對該 BT 進行 preorder traversal 及 inorder traversal 後會與原先的序列相同。
:::success
#### 指出其中可改進之處並實作。
:::
> [commit 5b568bb](https://github.com/yy214123/M02-quiz1-2/commit/5b568bbe9b5e4b89d0b68efff54511135f19f683)
此次提交先上傳原始的程式碼。
參考教材 [Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable),原始程式的雜湊函數採 Division method,應可用教材所提及的 Multiplication Method 來改進。
:::warning
我對於 Multiplication Method 的 bits 部份有疑惑,不知道該如何代入什麼值。
:::
> [commit 5333c09](https://github.com/yy214123/M02-quiz1-2/commit/5333c09b2b596a4cac5c5fb2689543a657358702)
> 此次更新加入了教材中的 Multiplication Method 雜湊函數。這個版本的程式碼目前有 bug 須修正
> [commit b2d2080](https://github.com/yy214123/M02-quiz1-2/commit/b2d2080061be7c49bb8a0eaadc445cd9631aaf2f)
> 此次更新修復了原先的 bug,逐步偵錯後發現是 `INIT_HLIST_HEAD(&in_heads[i])` 這部份的迭代次數設置有誤,原先的次數是 `inorderSize `應該配合 `bits` 的設計調整,否則會出現計算後的 `hash` 值找不到對應的 bucket。
原先提及對 Multiplication Method 的 bits 部份有疑惑,目前是以下方方式撰寫,但我不清楚是否正確:
```clike
int bits =ceil(log2(size));
int hash = (num < 0 ? hash_32(-num, bits) : hash_32(num, bits));
```
如果上方是正確的,那麼這部份也還有改進空間,可參考 [第 3 週測驗題](https://hackmd.io/@sysprog/linux2024-quiz3) 測驗 `1` 提及的方法改進。
### 測驗 `2`
:::success
解釋程式碼的運作原理。
:::
```clike
void hlist_del(struct hlist_node *n)
{
struct hlist_node *next = n->next, **pprev = n->pprev;
*pprev = next;
if (next)
EEEE = pprev;
}
```
`EEEE` 應填入 `next->pprev`。間接指標 `pprev` 會指向一個**指著自身節點的指標**,根據 if 判別條件,如果刪除的節點並非最末節點,將欲刪除節點之下一節點的 `pprev` 指向其自身。
```clike
void lRUCacheFree(LRUCache *obj)
{
struct list_head *pos, *n;
list_for_each_safe (pos, n, &obj->dhead) {
LRUNode *cache = list_entry(pos, LRUNode, FFFF);
list_del(GGGG);
free(cache);
}
free(obj);
}
```
這邊一開始填的答案都錯,因為我只看了 `lRUCacheCreate` 的程式架構,但實際上這邊的 `INIT_LIST_HEAD` 及 `INIT_HLIST_HEAD` 還未將節點彼此串接起來,應先理解 `lRUCachePut` 的運作邏輯後才回過頭來完成此部份。
仔細研究整個程式架構後,發現這個測驗蠻有趣的,本質上是兩個獨立的結構體,一個(hlist)利用雜湊函數的方式來降低搜尋節點的成本,一個(list)用雙向環狀鍊結串列來維護 LRU 的特性,將最近使用過的節點透過 `list_move` 來調整該節點之優先權。
理解整體架構後,回過頭看 `lRUCacheFree` 就清晰許多,可以看到:
`list_for_each_safe (pos, n, &obj->dhead)`
而 dhead 是維護 LRU 的雙向環狀鍊結串列的頭部,故 `FFFF` 應填入 `link`,而 `GGGG` 應該填入 `&cache->link`。
:::success
撰寫完整的測試程式。
:::
:::success
指出其中可改進之處並實作。
:::
雜湊函數也有改進空間,原始程式碼採 Division method,同測驗 `1` 可將其改為 Multiplication Method。
### 測驗 `3`
:::success
解釋程式碼的運作原理。
:::
一開始對 `small_const_nbits`中的`__builtin_constant_p` 感到疑惑,翻閱 [GCC 官方文件](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)及重新觀看[第 2 週課程 [34:07]](https://www.youtube.com/watch?v=WYcexo6T3UI&t=609s)後,才了解其功能。此函式會在編譯時期檢查某個表示式是否為常數。