# 2025q1 Homework1 (lab0)
contributed by < `kurtislin` >
## Reviewed by BennyWang1007
1. 倉庫應使用 rebase 而不是直接 merge,如 commit `221c30e`、`4b811c7`等。
2. Commit 連結皆指向不屬於任何分支的 commit,例如 [3066919](https://github.com/sysprog21/lab0-c/commit/3066919ac0617bb76fa6680f5539b3a8f3ea6d47),或許是 rebase 之類的操作導致。
3. 最後一部分應該為 `q_delete_dup`,標題錯誤。
4. `q_delete_dup` 可考慮改成只使用一層迴圈走訪鏈結串列,將與 `curr_elem` 重複的節點釋放,最後再將 `curr_elem` 釋放(若有重複),即可避免字串的複製,也不會產生懸空指標。
5. 注意排版以及句尾的句號。
6. Commit message 可以再更詳盡,例如 `q_reverseK`。
7. 缺乏的部分如 tiny web server 等,有空可以慢慢補上。
{%hackmd NrmQUGbRQWemgwPfhzXj6g %}
## 開發環境
```shell
$gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
$lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 46 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 24
On-line CPU(s) list: 0-23
Vendor ID: GenuineIntel
Model name: 13th Gen Intel(R) Core(TM) i7-13700
CPU family: 6
Model: 183
Thread(s) per core: 2
Core(s) per socket: 16
Socket(s): 1
Stepping: 1
CPU(s) scaling MHz: 55%
CPU max MHz: 5200.0000
CPU min MHz: 800.0000
BogoMIPS: 4224.00
```
## 用linked list實做queue
### q_new,q_free [Commit 732fa62](https://github.com/kurtislin/lab0-c/commit/732fa6268fcaec33fa0a854a34b82b344461d512)
`new` 和 `free` 是密切相關的操作
`q_new`
建立新的鍊結串列的節點 回傳初始化完成的queue
`q_free`
遍歷鍊結串列 釋放所有節點 最後還要釋放head
### q_insert_head , q_insert_tail [Commit aab9360](https://github.com/kurtislin/lab0-c/commit/aab93606010b3fe58c8fc874b6ed48a817722327)
`q_insert_head` 在佇列的頭部插入新元素
`q_insert_tail` 在佇列的尾部插入新元素
* 首先檢查輸入參數的有效性
* 為新元素分配記憶體
* 為字串值創建一個副本
* 使用 `list.h` 中定義的 `list_add` 函數將節點添加到頭部
* 返回操作結果
### q_size,q_remove_head,q_remove_tail [Commit 3066919](https://github.com/kurtislin/lab0-c/commit/3066919ac0617bb76fa6680f5539b3a8f3ea6d47)
`q_size` 在計算佇列裡的元素數量
`q_remove_head`,`q_remove_tail` 在移除佇列的頭部元素和尾部元素
在實作 `q_remove_head` 和 `q_remove_tail` 函數時,我們採用輸出參數模式,透過 sp 緩衝區參數和返回值同時提供元素指針與元素內容。這種設計讓單一函數調用能夠返回多種資訊,提升了函數的靈活性,使調用者可根據需求選擇獲取所需的資料形式。
### q_reverse [Commit c04198f](https://github.com/kurtislin/lab0-c/commit/c04198f5af66efa42886378fc6845161f9c46e49)
`q_reverse`將佇列所有元素反轉
### q_swap [Commit a46fa1d](https://github.com/kurtislin/lab0-c/commit/a46fa1d96caec6600d0eb6a452cf42be63dc44b1)
`q_swap` 將相鄰的節點交換
### q_delete_mid q_reverseK [Commit d3a52e3](https://github.com/kurtislin/lab0-c/commit/d3a52e3ce7b88315fb6d54977c970a591907aa02)
`q_delete_mid` 利用快慢指標實現
`q_reverseK` 利用`temporary list` 實現,在`q_reverseK` 函數中,我使用了 `struct list_head temp_head` 而不是指標 `struct list_head *temp_head` 來宣告臨時頭節點,主要有以下原因:
#### 堆疊(Stack)分配 vs. 堆(Heap)分配:
直接宣告 `struct list_head temp_head` 會在函數的堆疊上分配記憶體
使用指標 `struct list_head *temp_head` 則需要使用 `malloc` 從堆中分配記憶體,並在使用完後需要 `free`
堆疊分配更快速,也不需要擔心記憶體洩漏
### q_delete_dup [Commit a290040](https://github.com/kurtislin/lab0-c/commit/a29004075c0c873749788b72e1ba990cb751aee7)
```c
bool q_delete_dup(struct list_head *head)
{
if (!head || list_empty(head))
return false;
bool removed = false;
struct list_head *node = head->next;
while (node != head && node->next != head) {
element_t *curr_elem = list_entry(node, element_t, list);
element_t *next_elem = list_entry(node->next, element_t, list);
if (!strcmp(curr_elem->value, next_elem->value)) {
removed = true;
char *dup_val = curr_elem->value;
while (node != head) {
element_t *check_elem = list_entry(node, element_t, list);
if (!strcmp(check_elem->value, dup_val)) {
struct list_head *tmp = node->next;
list_del(node);
q_release_element(check_elem);
node = tmp;
}
else
break;
}
}
else
node = node->next;
}
return removed;
}
```
以上是我一開始的代碼但是不管怎麼測試都會剩下一個元素
測試資料中沒有要刪除的元素時也會有以下問題
```shell
l = [1 2 3 4]
cmd> dedup
ERROR: Calling delete duplicate on null queue
```
之後發現問題是出現在執行`q_release_element(check_elem)`時,釋放了元素的記憶體,包括 `check_elem->value`。但是`dup_val` 指針是指向 `curr_elem->value`,所以造成懸空指標,之後改成`char *dup_val = strdup(curr_elem->value);`就可以了
### q_sort 和 merge sort 實作 [Commit f32ba44](https://github.com/kurtislin/lab0-c/commit/f32ba44ca9fa33a7143a0b43b7286328e80b18f3)
`q_sort` 使用 merge sort 演算法來排序,整個實作分成三個主要的 helper functions:
#### merge_sort()
遞迴式的 merge sort 主體,採用分治法的概念。先把鏈結串列切成兩半,各自排序後再合併。時間複雜度是 O(n log n),對鏈結串列來說效率不錯。
#### split_list()
用快慢指標的技巧把鏈結串列分成兩半。慢指標走一步,快指標走兩步,當快指標到底時慢指標就在中間位置。這樣可以在一次遍歷中找到分割點。
#### merge_lists()
把兩個已排序的鏈結串列合併成一個。比較兩個串列的頭部元素,選較小(或較大)的放到結果中,重複到其中一個串列空了為止,剩下的直接接上去。
整個 merge sort 的設計很適合鏈結串列,因為不需要額外的記憶體空間,只要重新連接指標就行了。
### q_ascend, q_descend, q_merge [Commit 9c0d6d0](https://github.com/kurtislin/lab0-c/commit/9c0d6d03513683f5f66c08e2efb97a7cb6b74045)
**q_ascend()** 和 **q_descend()** 都用了單調棧的概念,從尾部開始往前掃描,只保留符合條件的節點。ascend 會移除右邊有更小值的節點,descend 則相反。
**q_merge()** 把多個佇列合併成一個,先把所有元素收集到第一個佇列裡,最後用 q_sort 排序。實作上比較直觀,就是先合併再排序。
## Valgrind 記憶體分析
使用 `make valgrind` 進行動態記憶體分析
### 測試結果
```
--- TOTAL 100/100
```
## 研讀 lib/list_sort.c 並比較實作的sort
### 閱讀 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) [Linux核心的鏈接串列排序](https://hackmd.io/@sysprog/linux2025-lab0/%2F%40sysprog%2Flinux2025-lab0-e)
**發現**:
1. 使用 bottom-up merge sort 而非 recursive
2. Power-of-two 合併策略:確保每次合併都是 2:1 平衡的 可避免極端情況 例如 1024個已排序好的元素 再去合併4個元素
3. 使用位元運算追蹤合併狀態
4. 設計考慮 cache-friendly 的合併順序
```c=226
if (likely(bits)) {
struct list_head *a = *tail, *b = a->prev;
a = merge(priv, cmp, b, a);
/* Install the merged result in place of the inputs */
a->prev = b->prev;
*tail = a;
}
```
`likely()` 是一個編譯器優化提示,用於分支預測優化。
`likely(bits)` 告訴編譯器這個條件很可能為真,從而進行以下優化:
1. **分支預測**:CPU 會傾向於預測條件為真,減少管線停頓
2. **程式碼布局**:將 `if` 區塊放在較近的記憶體位置,提高快取命中率
3. **管線優化**:減少分支預測錯誤時的效能懲罰
這種優化在高頻執行的程式碼路徑中特別有效。
### 比較原本的sort 和 list_sort.c
#### 在queue.c加入切換機制 [Commit 83a61cb](https://github.com/kurtislin/lab0-c/commit/83a61cbca932253cfe90a966d003c99414b3f471)
**實作內容**:
1. **新增 ksort 命令**:切換排序演算法
- `ksort 0`: 原版 merge sort
- `ksort 1`: Linux kernel list_sort
2. **新增 benchmark 命令**:
- 支援多種資料分布:random, sorted, reverse, partial
- 使用 `clock_gettime(CLOCK_MONOTONIC)`
- 自動計算加速比並報告結果
3. **修改檔案**:
- `queue.c`: 加入 `q_set_kernel_sort()` 和比較函數
- `qtest.c`: 實作 `do_ksort()` 和 `do_benchmark()` 命令
- `Makefile`: 加入 `list_sort.o` 編譯目標
- 新增 `list_sort.c/h`: Linux kernel 排序實作
### benchmark 命令使用範例
#### 基本語法
```bash
./qtest
> benchmark <size> [distribution_type]
```
#### 使用範例
```bash
# 測試 1000 個隨機元素
> benchmark 1000 random
# 測試 5000 個已排序元素
> benchmark 5000 sorted
# 測試 10000 個反向排序元素
> benchmark 10000 reverse
# 測試 2000 個部分排序元素(80% 已排序 + 20% 隨機)
> benchmark 2000 partial
```
#### 支援的分布類型
- `random` (預設): 隨機生成的元素
- `sorted`: 預先排序好的元素(升序)
- `reverse`: 反向排序的元素(降序)
- `partial`: 部分排序的元素(80% 已排序 + 20% 隨機)
#### 範例輸出
```
Benchmarking with 10000 elements (random distribution)
Original merge sort: 2.45 ms
Linux kernel list_sort: 1.89 ms
Speedup: 1.30x (Linux kernel is 29.6% faster)
```
#### 切換排序演算法
```bash
# 使用原版 merge sort
> ksort 0
# 使用 Linux kernel list_sort
> ksort 1
# 確認當前使用的演算法
> help ksort
```
這個commit我是用clock_gettime 去取得時間
但是執行 benchmark 10000 random 幾次後 發現每次的結果差異都不小(包含效能提升比例)
#### 測試結果
以下是用10000個測試資料的時候的結果
結果都是kernel sort更快
差異最小的是1.26倍 差異最大的是1.5倍
後續數量改為500k 和100k 的結果也差不多
```bash
cmd> benchmark 10000 random
=== Benchmark Results ===
Data size: 10000 elements
Distribution: random
Original merge sort: 7.343 ms
Linux kernel list_sort: 5.837 ms
Kernel sort is 1.26 times faster
cmd> benchmark 10000 random
=== Benchmark Results ===
Data size: 10000 elements
Distribution: random
Original merge sort: 8.219 ms
Linux kernel list_sort: 5.483 ms
Kernel sort is 1.50 times faster
```