# 2024q1 Homework1 (lab0)
contributed by < `otteryc` >
## 開發環境
```shell
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ 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): 8
On-line CPU(s) list: 0-7
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
CPU family: 6
Model: 158
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
Stepping: 9
CPU max MHz: 4500.0000
CPU min MHz: 800.0000
BogoMIPS: 8400.00
```
## 針對佇列操作的程式碼實作
### `q_insert_head`
依 `queue.h` 檔案中之指示,本函式當明確地<s>分配</s> 配置空間,並將傳入字串 `s` 存入所新增於佇列的物件中。
:::warning
allocate 翻譯為「配置」,而非「分配」,這樣才可跟 dispatch 的翻譯區隔,二者都頻繁在作業系統領域出現。
:::
惟查,關於 `s` 字串之大小,作業說明以及程式碼中,並無明確說明。為妥適配置記憶體,減少程式開銷,吾人查詢呼叫 `q_insert_head` 以及 `q_insert_tail` 之程式碼區段:
:::warning
改進你的漢語表達,文言文擬古不能掩蓋行文不流暢的現象,以清晰且無語病的白話文書寫。
:::
巨集 `dut_insert_head` ,所插入的字串係由 `get_random_string` 產生,而 random_string 則是由 `prepare_inputs`產生,揆諸前開函式,可知 random_string 長度固定為 8 個字元,而且是 well null-terminated。
```c
static char random_string[N_MEASURES][8];
static char *get_random_string(void)
{
random_string_iter = (random_string_iter + 1) % N_MEASURES;
return random_string[random_string_iter];
}
void prepare_inputs(uint8_t *input_data, uint8_t *classes)
{
randombytes(input_data, N_MEASURES * CHUNK_SIZE);
for (size_t i = 0; i < N_MEASURES; i++) {
classes[i] = randombit();
if (classes[i] == 0)
memset(input_data + (size_t) i * CHUNK_SIZE, 0, CHUNK_SIZE);
}
for (size_t i = 0; i < N_MEASURES; ++i) {
/* Generate random string */
randombytes((uint8_t *) random_string[i], 7);
random_string[i][7] = 0;
}
}
```
次查,在 `linenoice.c` 中,規定 console 中每行指令之上限為 4096
```c
#define LINENOISE_MAX_LINE 4096
```
:::danger
減少非必要的項目縮排 (即 `* `),使用清晰、明確,且流暢的漢語書寫。
:::
本於上述觀察,吾人獲悉佇列中單一元素(即 `element_t`)中所指向的字串,大小最多為 4096 個位元。嘗試將之作為單一元素所指向的字串的長度上限,惟此舉會導致 `make test` 時,超過記憶體限制,只能以實作上需要兩次<s>遍歷</s> 走訪的 `strdup(3)` 替代之。
:::warning
留意[資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)
:::
```diff
bool q_insert_head(struct list_head *head, char *s)
{
if (!head || !s)
return false;
element_t *n = malloc(sizeof(element_t));
if (!n)
return false;
- n->value = malloc(MAX_STRING_LEN);
+ n->value = strdup(s);
if (!n->value) {
free(n);
return false;
}
- strncpy(n->value, s, MAX_STRING_LEN);
list_add(&n->list, head);
return true;
}
```
### `q_insert_tail`
本函式與 `q_insert_head` 的實作幾近相同,差異者係在插入新物件時,所使用之函式應改為 `list_add_tail` ,僅此而已。
TODO: 兩函式的實作或許可改以 macro 實作 ,俾降低程式碼維護成本。
:::warning
「或許」不該簡稱為「或」,否則會造成歧義。
:::
### `q_reverse`
<s>依題旨,本題</s> 本函式不得涉及記憶體管理的操作,本函式的想法是用兩個指標,`fast` 指標儲存被操作的物件的下一個物件的記憶體位置,`slow` 指標則負責將 `list_head` 結構體中分別指往前後的指標反向。並且,當 `slow` 指向 `head` 時,就代表反向完成。
```c
void q_reverse(struct list_head *head)
{
if (!head)
return;
struct list_head *fast = head->next, *slow = head, *tmp;
do {
tmp = slow->next;
slow->next = slow->prev;
slow->prev = tmp;
} while (slow = fast, fast = fast->next, slow != head);
}
```
### `q_delete_mid`
:::danger
避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景。
:::
<s>本題</s>由於這份作業的 queue 是由 doubly circular linked-list 實作,除了針對 singly linked-list 的快慢指標來尋找在佇列正中央的節點以外,還可以透過兩個指標分別從 head 跟 tail 開始互為反向地走訪,當兩個指標相遇時,就是佇列中點,並刪除之。
```c
struct list_head *right = head->next, *left = head->prev;
while (true) {
if (right == left)
break;
right = right->next;
if (right == left)
break;
left = left->prev;
}
```
上述程式碼,值得注意的是,讓 right 提早 left 移動一步,如此就可以解決當佇列中元素個數為偶數的情形,像是下圖情形,應被從佇列中刪除的是標示為 2 號的元素。
```graphviz
digraph delete_mid {
rankdir=LR
Head [label=H,shape=circle]
0 [shape=circle]
1 [shape=circle]
2 [color=Red, shape=circle]
3 [shape=circle]
Head->0
0->1
1->2
2->3
3->Head
}
```
:::warning
不該言及「本題」,個別函式之間存在關聯,應看待為整體的創作,只是依據工程開發的角度,逐項分析和探討。
:::
### `q_sort`
這一個函式依照指示用 merge sort 完成,在進行 conquer 的時候所用的函式可以獨立出來,供 `q_merge` 函式呼叫。
```c
void q_sort(struct list_head *head, bool descend)
{
if (!head || list_is_singular(head) || list_empty(head))
return;
struct list_head *right = head->next, *left = head->prev;
while (true) {
if (right == left)
break;
left = left->prev;
if (right == left)
break;
right = right->next;
}
LIST_HEAD(mid);
list_cut_position(&mid, head, right);
q_sort(head, descend);
q_sort(&mid, descend);
merge_sort_conquer(head, &mid, descend);
}
```
這裡一樣要尋找佇列中點,不過應該注意的是,當佇列內的節點個數是偶數時,應該要取的節點與上一個函式不同。
```graphviz
digraph delete_mid {
rankdir=LR
Head [label=H,shape=circle]
0 [shape=circle, color=red]
1 [shape=circle]
Head->0
0->1
1->Head
}
```
在這個情況下,應該要取編號為 0 的節點作為中點進行分出兩個子佇列,否則將無法有效的分治。
```diff
static void merge_sort_conquer(struct list_head *dest,
struct list_head *victim,
bool descend)
{
int mask = descend ? 0 : INT_MIN;
LIST_HEAD(result);
struct list_head *insert;
while (!list_empty(dest) && !list_empty(victim)) {
element_t *e_victim = list_first_entry(victim, element_t, list),
*e_dest = list_first_entry(dest, element_t, list);
insert = strcmp(e_victim->value, e_dest->value) & mask ? victim->next
: dest->next;
list_move_tail(insert, &result);
}
insert = list_empty(dest) ? victim : dest;
- list_splice_tail(insert, &result);
+ list_splice_tail_init(insert, &result)
list_splice(&result, dest);
return;
}
```
另外,在 conquer 的部分,透過 `strcmp` 函式回傳的值來判斷兩個節點之間的大小,根據 man strcmp(3):
```
strcmp() returns an integer indicating the result of the comparison, as follows:
• 0, if the s1 and s2 are equal;
• a negative value if s1 is less than s2;
• a positive value if s1 is greater than s2.
```
此外,在排序時,兩個節點的內容相等時,孰先孰後並不重要。所以,在判斷要依升冪還是降冪排列時,只要觀查 strcmp 回傳值的 sign bit 就好,可以有效降低進行分支的次數。
另外, `list_splice_tail` 在搬離所有節點之後,不會妥善的處理來源佇列的 head ,如果這時候再去存取來源佇列的 head ,會造成未定義的結果,所以應該改用 `list_splice_tail`
### `q_merge`
這個函式所要處理的是把多個以排序佇列合而為一,僅需一一走訪,並使用前述 merge sort 的 conquer 函式把每個佇列連接在一起即可。
```c=
int q_merge(struct list_head *head, bool descend)
{
if (!head || list_empty(head))
return 0;
LIST_HEAD(result);
queue_contex_t *iter;
list_for_each_entry (iter, head, chain) {
// cppcheck-suppress uninitvar
merge_sort_conquer(&result, iter->q, descend);
INIT_LIST_HEAD(iter->q);
}
list_splice(&result, list_first_entry(head, queue_contex_t, chain)->q);
return q_size(list_first_entry(head, queue_contex_t, chain)->q);
}
```
另外,在第 9 行處, cppcheck 似乎會將 `iter->q` 認為未初始化的變數,由於在 `list_for_each_entry` 巨集的定義中,有確實的初始化 iter 變數,所以透過 suppress 選項 <s>指令</s> 使 cppcheck 暫不檢查此類潛在錯誤。