# 2024q1 Homework1 (lab0)
contributed by < `LIAO-JIAN-PENG` >
### Reviewed by `lintin528`
在有多個節點被移動的狀況,像是 `q_reverseK` ,我的做法通常會是直接在原本的鏈結串列中使用 `list_move` 去做移動,這樣可以避免 `cut` 跟 `splice` 的使用,還有一個額外的 `list_head` 結構 `buf` ,這樣的話除了交換的動作外會多花費一些計算量,但這麼做的缺點就是程式的可讀性不如目前的做法,我認為這邊可以做一下取捨。
## 開發環境
```
$ 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): 12
On-line CPU(s) list: 0-11
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-10400 CPU @ 2.90GHz
CPU family: 6
Model: 165
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 3
CPU max MHz: 4300.0000
CPU min MHz: 800.0000
BogoMIPS: 5799.77
Virtualization: VT-x
L1d cache: 192 KiB (6 instances)
L1i cache: 192 KiB (6 instances)
L2 cache: 1.5 MiB (6 instances)
L3 cache: 12 MiB (1 instance)
NUMA node(s): 1
NUMA node0 CPU(s): 0-11
```
## 佇列實作
:::danger
「作」是古老的字,甲骨文時代就存在,最初的涵義是「起」,現代漢語裡仍然使用的「振作」、「一鼓作氣」、「槍聲大作」中的「作」,都是「[起](https://dict.revised.moe.edu.tw/dictView.jsp?ID=6299)」的意思。在這個意義上跟「做」不衝突,因為「做」無此含義。「做」是後造字,始於宋元時代,當「即使」、「播弄」、「做作」講。
依據中華民國教育部《重編國語辭典修訂本》:
* 「[作](https://dict.revised.moe.edu.tw/dictView.jsp?ID=9622)」: 創作。如:「寫作」、「作畫」、「作詩」。《論語.述而》:「述而不作,信而好古」
* 「[做](https://dict.revised.moe.edu.tw/dictView.jsp?ID=9643)」: 製造。如:「做衣服」、「做鞋子」
對照 Dictionary.com 對 [implement](https://www.dictionary.com/browse/implement) 一詞的解釋:
* to fulfill; perform; carry out:
* to put into effect according to or by means of a definite plan or procedure.
* Computers. to realize or instantiate (an element in a program), often under certain conditions as specified by the software involved.
* to fill out or supplement
「實作」會是符合上述 "implement" 語意的譯詞,取「起」以「創作」的意思,而「製造」本質上是固定輸入並通常產生固定輸出的行為。
$\to$ [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)
:::
目標:完成 `queue.c` 尚未完成的程式實作
### q_new
```diff
struct list_head *q_new()
{
struct list_head *new = malloc(sizeof(struct list_head));
+ if (!new)
+ return NULL;
INIT_LIST_HEAD(new);
return new;
}
```
根據 C 語言規格書[7.22.3],如果記憶體空間不足以配置,則函式應該回傳 NULL。
> If the space cannot be allocated, a null pointer is returned.
### q_swap
使用 `list.h` 的 `list_move` 可以精簡原本程式碼。
```diff
void q_swap(struct list_head *head)
{
- struct list_head *prev = head;
- struct list_head *curr = head->next;
-
- while (curr != head) {
- prev->next = curr->next;
-
- curr->prev = prev->next;
- curr->next = prev->next->next;
-
- prev->next->next = curr;
- prev->next->prev = prev;
- prev = curr;
- curr = curr->next;
+ struct list_head *li = head->next;
+ while (li != head && li->next != head) {
+ list_move(li, li->next);
+ li = li->next;
}
}
```
### q_reverse
使用 `list_move` 精簡程式碼。
```diff
/* Reverse elements in queue */
void q_reverse(struct list_head *head)
{
- struct list_head *prev = head;
- struct list_head *curr = head->next;
- while (curr != head) {
- curr->prev = curr->next;
- curr->next = prev;
- prev = curr;
- curr = curr->prev;
- }
- curr->prev = curr->next;
- curr->next = prev;
+ struct list_head *li;
+ list_for_each (li, head->prev)
+ list_move(head->prev, li);
}
```
### q_reverseK
:::danger
應使用 `git diff` 或 `diff -u` 命令來產生程式碼之間的變更列表,不要憑感覺填入,注意位移量。
:::
```diff
/* Reverse the nodes of the list k at a time */
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
if (!head)
return;
- int times = q_size(head) / k;
+ int times = q_size(head) / (k + !k);
struct list_head *tail;
LIST_HEAD(tmp);
LIST_HEAD(new_head);
for (int i=0; i < times; i++) {
int j = 0;
list_for_each(tail, head) {
if (j >= k)
break;
j++;
}
list_cut_position(&tmp, head, tail->prev);
q_reverse(&tmp);
list_splice_tail_init(&tmp, &new_head);
}
list_splice_init(&new_head, head);
}
```
當 k 為 0 時,k + !k 會變成 0 + !0,也就是 0 + 1,其結果為 1。這樣的修正將導致當 k 為 0 時,程式碼將會計算 q_size(head) / 1,而不是 q_size(head) / 0,這可以避免浮點例外的錯誤。
> 參考 [vax-r](https://hackmd.io/@vax-r/linux2024-homework1#q_reverseK)
:::warning
本頁面的目的就是「開發紀錄」,你應該針對子主題去闡述
:::
## 研讀 lib/list_sort.c
### merge_final
```c
do {
/*
* If the merge is highly unbalanced (e.g. the input is
* already sorted), this loop may run many iterations.
* Continue callbacks to the client even though no
* element comparison is needed, so the client's cmp()
* routine can invoke cond_resched() periodically.
*/
if (unlikely(!++count))
cmp(priv, b, b);
b->prev = tail;
tail = b;
b = b->next;
} while (b);
```
這段程式碼中的 `if (unlikely(!++count))` 在 `count` 變為 0 時會執行 `cmp()` 函式。這段程式碼用於處理合併過程中可能的高度不平衡情況。在這裡,我們設置了一個 8-bit 的 `count` 變數。當 `count` 的值增加到非常大,導致溢位並變為 0 時,即使在實際上不需要進行元素比較,仍然會執行 `cmp()` 函式。
client 代表呼叫這段程式碼的外部程式的部分。當程式碼運行到某個情況時,即使不需要進行元素比較,也會持續向 client 發送 callback,以便外部的 `cmp()` 函式可以週期性地呼叫 `cond_resched()` 函式。
>Long-running list_sort() calls: If the list passed in may be long, or the
>client's cmp() callback function is slow, the client's cmp() may
>periodically invoke cond_resched() to voluntarily yield the CPU. All
>inner loops of list_sort() call back to cmp().
`cond_resched()` 的主要目的之一是讓長時間執行的核心程式碼暫時讓出 CPU ,給其他更高優先權的工作機會執行。因此在核心中有可能長時間迴圈執行的程式碼,插入 `cond_resched()` 呼叫,避免獨占住 CPU 資源。<s>這樣做是為了讓客戶端的 `cmp()` 函式能夠定期呼叫`cond_resched()` </s> 。
:::danger
不要「[舉燭](https://dict.revised.moe.edu.tw/dictView.jsp?ID=158282)」!你將註解翻譯成中文字,然後就懂了嗎?排序的程式碼裡頭出現 "client", "cond_resched", "periodically" 等字樣,你難道不會懷疑自己根本沒懂註解和程式碼的關聯嗎?
使用 `git blame` 和 `git log` 去探索這段程式碼和註解背後的演化過程,推敲後寫下你的洞見和疑惑。真正限制你專業成長的因素,就是「不懂裝懂」。
:::
```c
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
```
`__builtin_expect()` 是 gcc 的內建函式 [GCC 手冊 __builtin_expect](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html#index-_005f_005fbuiltin_005fexpect),用來告訴編譯器我們期望的條件。編譯器會根據`__builtin_expect()` 提供的資訊,告訴CPU可能執行的分支方向。這樣 CPU 的 branch predictor 就可以根據這些提示,~~提前做出預測出下一道指令~~。這裡 `!!(x)` 的兩個 `!` 是透過兩次的 `NOT` 來確保值一定是 0 或 1。
Branch prediction 是現代 CPU 中一種重要的性能優化技術。由於 CPU 執行指令是按順序執行的,當遇到條件分支指令(比如 if-else 條件式)時,就需要決定接下來執行哪個分支。CPU 在執行到分支指令前,往往無法確定要執行哪個分支,因此需要等待分支條件被評估之後才能決定。採用 branch prediction 可以根據歷史執行記錄,提前預測和預取下一條指令的執行路徑,以避免 CPU 在執行分支指令時發生暫停。
`likely()` 巨集表示 x 為真的機率比較大,並告訴 complier 將 `x==true` 的條件對應程式碼放在判斷式後面,反之 `unlikely()` 則是 x 為假的機率比較大,並告訴 complier 將 `x==false` 的條件對應程式碼放在判斷式後面,以提高 branch prediction 的效率。
思考:cond_resched() 在沒有 kernel preemption 狀況下,效力是否等於 sched_yield()
:::warning
上面敘述不精準,對照看 GCC 手冊和計算機組織/結構教科書裡頭關於 branch prediction 的解說。
:::
## 課程教材發現與洞見
連結:[課程教材疑惑與洞見](https://hackmd.io/@Jpeng/linux2024-question)