---
# System prepended metadata

title: 2024q1 Homework1 (lab0)
tags: [linux2024]

---

# 2024q1 Homework1 (lab0)
contributed by < `lumynou5` >

## 開發環境與前置作業

```shell
$ gcc --version
gcc (GCC) 13.2.1 20230801

$ lscpu
Architecture:            x86_64
  CPU op-mode(s):        32-bit, 64-bit
  Address sizes:         48 bits physical, 48 bits virtual
  Byte Order:            Little Endian
CPU(s):                  8
  On-line CPU(s) list:   0-7
Vendor ID:               AuthenticAMD
  Model name:            AMD Ryzen 7 4700U with Radeon Graphics
    CPU family:          23
    Model:               96
    Thread(s) per core:  1
    Core(s) per socket:  8
    Socket(s):           1
    Stepping:            1
    Frequency boost:     enabled
    CPU(s) scaling MHz:  71%
    CPU max MHz:         2000.0000
    CPU min MHz:         1400.0000
    BogoMIPS:            3994.00
```

參見分支 [impl-old](https://github.com/lumynou5/lab0-c/tree/impl-old)。

### Cppcheck

Cppcheck 自 [2.11](https://github.com/danmar/cppcheck/releases/tag/2.11) 加入了 `--check-level` 選項，預設值為 `normal`，避免花費太多時間檢查長函式。而 `log2_lshift16`（`log2_lshift16.h:14`）正好不在 `normal` 等級的檢查範圍內，必須要使用 `--check-level=exhaustive`。且關於檢查等級的提示訊息儘管分類為 `information` 卻仍然會判定為錯誤，造成靜態檢查得到失敗的結果，因此我在 `scripts/pre-commit.hook` 中加入 `--check-level=exhaustive` 將 `log2_lshift16` 納入檢查範圍。
> commit [82fd395](https://github.com/lumynou5/lab0-c/commit/82fd3954a69c4ad689311f4739b8cf6a1f30e927)

抑制 `queue.c` 的 `constParameterPointer` 檢查，因為其會誤判。
> commit [6609844](https://github.com/lumynou5/lab0-c/commit/660984473407d55482a48ea1ba13e8acecacb21b)

## 指定的佇列操作

佇列結構：
使用沒有「container」、獨立存在的 `struct list_head` 作為鏈結串列的「開頭」：這是實作串列常見的手法，如此在長度為 0 時便不會出現分支。
環狀雙向串列。

:::danger
避免非必要的項目縮排 (即 `* ` 和 `- `)，以清晰、明確，且流暢的漢語書寫。
:::

### `q_new`

配置並初始化一個 `struct list_head`。如上所述，串列的開頭應該是一個沒資料的節點，這點可以從 `list_entry` 和 `list_last_entry` 等窺見一二。

### `q_free`
> commit [bc4ba52](https://github.com/lumynou5/lab0-c/commit/bc4ba5277fc8e797e81afd2d16f9332143a4cb6e)

「遍歷」在[《教育部重編國語辭典修訂本》的釋義](https://dict.revised.moe.edu.tw/dictView.jsp?ID=19003)為「各處都到過」，因此我認為在此處使用該詞能正確表達意思，這只是自然語言。而「走訪」意為「拜見、往訪」，並沒有「各處」的含意（反而看起來像「access」的中國翻譯「訪問」），若要使用「走訪」一詞，應該改成「走訪每個節點」而非「走訪整個串列」。

> 「走訪」本來就沒有「存取各處資源」的寓意，一如 "traverse" 沒有意味著要經過所有的地點或單位。「遍」與「所有」擺在一起更是造成理解的困難，到底前者抑或後者是贅詞？「遍歷」這詞還是留做 Ergodic 的翻譯就好。

> 不過也許沒有哪個是贅詞？在古文中也有「遍歷各山」的用法（見前文連結），說明以前「遍歷」一詞也是如此使用的。據《重編國語辭典修訂本》，「遍」為「沒有遺漏」之意，英語維基百科對「traversal」的解釋亦為「the process of visiting (checking and/or updating) each vertex in a graph」，儘管在程式中很可能不會真的造訪每一個節點，但這應該視為最佳化的結果，而最原始的模樣應該是沒有提早退出迴圈、沒有剪枝的。但確實，相同的翻譯有可能造成誤會。另外，「歷」作動詞是「經」的意思，不太符合「visit」的語意，也是「遍歷」不好的理由之一。不過如此一來要如何表達英語中如「tree traversal」等用法？我認為「樹的走訪」失去了「each」的含意。

利用 `list_for_each_entry_safe` 巨集可以簡單地走訪整個串列，最後記得 `free(head)`。

`q_free` 的註解特別提到，在 `head` 為 `NULL` 時沒有作用，因此應該加上檢查。

### `q_insert_head`, `q_insert_tail`

插入到開頭相當簡單：配置一個 `element_t`，使用 `strdup` 重製（duplicate）字串 `s` 並指派給 `element->value`，最後用 `list_add` 將新節點加到串列開頭。並且在 `head == NULL`、記憶體配置失敗時回傳 `false`。

`q_insert_tail` 原本的實作和 `q_insert_head` 幾乎一樣，只是改用 `list_add_tail`。但我接著就想到，「將節點加到開頭的上一個節點後面」等同於「將節點加到開頭的前面」，且重複的程式碼是一種「壞味道」，因此我將其改寫為：

```c
bool q_insert_tail(struct list_head *head, char *s)
{
    if (!head)
        return false;
    return q_insert_head(head->prev, s);
}
```

### `q_remove_head`, `q_remove_tail`
> commit [926938b](https://github.com/lumynou5/lab0-c/commit/926938b4c9939dcc01abb013436c0d0ff1981776)

利用 `list_first_entry` 取得串列的第一筆資料，也就是 `head->next` 的容器，然後用 `list_del` 將節點從串列中移除，並將字串用 `strncpy` 複製到 `sp` 中。`q_remove_tail` 使用和 `q_insert_tail` 相似的手法，把 `head->prev->prev` 當作開頭時的第一個節點便是 `head` 的最後一個節點。

根據手冊，`char *strncpy(char *dst, char const *src, size_t sz)` 會將參數 `src` 所指向的字串複製到 `dst` 所指向的以 null 填充（null-padded）的字元序列。但如果 `dst` 大小不夠，字串會截斷且**不會**在結尾插入 null 字元。（即 `sz` 是不包含 null terminator 在內的「字串長度」，而非緩衝區大小，從手冊提供的可能實作和 `strnlen` 的描述也能看得出來。）

這時我的實作長這樣：

```c
strncpy(sp, element->value, bufsize); // 這裡可以不需要減去 1。
sp[bufsize - 1] = '\0'; // 將最後一個 char 指派為 null terminator。
                        // 如果 sp 不夠大就必須要加上 null terminator，
                        // 如果 sp 夠大這行則不改變結果（該處本來就會是 null 字元），
                        // 因此可以無條件地執行這行，不需要分支。
```

接著我想到，`stpncpy` 的回傳值是指向字串之後的指標，因此可以改寫成：
```c
*stpncpy(sp, element->value, bufsize - 1) = '\0';
```

不但程式碼更精簡，且使用的指令也較少。

以下比較針對 `stpncpy` 引入的效能比較。
> 編譯選項：`-O3 -S`

原本的程式碼：
```
movq    %rbx, %rdx
movq    %rcx, %rdi
call    strncpy@PLT
movb    $0, -1(%rax,%rbx)
```

修改後：
```
subq    $1, %rdx
call    stpncpy@PLT
movb    $0, (%rax)
```

### `q_delete_mid`

利用「快慢指標」找到中間的節點。

### `q_delete_dup`

和 `q_delete_mid` 不同，註解只說當 `head` 為 `NULL` 時回傳 `false`，為空時不用，不知道是不是誤植。
:::warning
提交 pull request 吧！
:notes: jserv
:::
> [#174](https://github.com/sysprog21/lab0-c/pull/174)

### `q_swap`

目前的實作可讀性較差，有改進空間。

### `q_reverse`

使用 `list_move` 將每個節點移動至開頭便能原地反向之。

### `q_ascend`, `q_descend`

兩者幾乎一樣，惟前者產生升序、後者產生降序，只有「比較」不同，可以將這部分邏輯抽出來，改成傳入函式指標來比較。

```c
typedef bool element_cmp_t(element_t const *lhs, element_t const *rhs);

static inline int ascend_descend_impl(struct list_head *head, element_cmp_t *cmp)
{
    // Real implementation...
}

int q_ascend(struct list_head *head)
{
    return ascend_descend_impl(head, element_greater);
}
```

另外，透過 `diff` 比較檔案差異發現，`ascend_descend_impl` 是否加上 `inline`（或 `__always_inline`）在 `-O3` 下對產生的組合語言沒有影響。