# 2024q1 Homework1 (lab0)
contributed by < [`56han`](https://github.com/56han/lab0-c) >
### Reviewed by `allenliao666`
- commit 內文中句首應該要大寫,例如 commit [f88e90a](https://github.com/56han/lab0-c/commit/f88e90a36da8f52cdd43e308fc442b3fa46a5cba)
- `q_ascend` 的內文及註解有誤,應該是若右側節點小於當前節點才需標記當前節點為需要刪除。
- `q_ascend` 和 `q_descend` 有不需雙層迴圈即可完成的實作方法,可以嘗試看看。
- 開發紀錄寫得很清楚,閱讀起來很舒服。
:::danger
1. 詳閱作業規範
2. 留意各式細節,唯有重視小處並步步為營,方可挑戰原始程式碼規模超越三千七百萬行的 Linux 核心
:::
### Reviewed by `Ackerman666`
`q_delete_mid` 在雙向佇列中,可從頭尾開始往中間找尋中間點
如此只需走訪一次串列就行 可參考 [`YangYeh-PD`](https://hackmd.io/@YangYeh/linux2024-homework1#q_delete_mid) 的解說
## 開發環境
```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): 12
On-line CPU(s) list: 0-11
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz
CPU family: 6
Model: 158
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 10
CPU max MHz: 4600.0000
CPU min MHz: 800.0000
BogoMIPS: 6399.96
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 node0 CPU(s): 0-11
```
## 雙向鏈結串列結構
```graphviz
digraph G {
node [shape=record];
// 定义节点
nodeA [label="{<prev> prev | value: 'A' | <next> next}"];
nodeB [label="{<prev> prev | value: 'B' | <next> next}"];
nodeC [label="{<prev> prev | value: 'C' | <next> next}"];
// 定义相同排名,将节点放在同一行
// { rank=same; nodeA -> nodeB -> nodeC [dir=none]; }
// 定义双向链接
nodeA:next -> nodeB:prev [dir=both];
nodeB:next -> nodeC:prev [dir=both];
nodeA:prev -> nodeC:next [dir=both];
}
```
## 指定的佇列操作
### `q_new`
定義一個**函式** `q_new`,用於動態**配置**,並初始化一個空的雙向鏈結串列節點,如果配置失敗則返回 `NULL`。
```c
struct list_head *q_new()
{
struct list_head *head = malloc(sizeof(struct list_head));
if (!head)
return NULL;
INIT_LIST_HEAD(head);
return head;
}
```
### `q_free`
原本寫法如下,但寫完 `q_insert_head` 之後,反而分數變少。
我認為是配置給 `element_t` 結構體中 `value` 欄位的記憶體沒有被釋放。此外,直接釋放 `struct list_head` 結構體的 pointer 也可能破壞 list 的結構,因為它沒有按照 list 的邏輯結構來逐一走訪每個節點,並釋放每個元素。
:::danger
避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景。
$\to$ [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)
:::
```diff
void q_free(struct list_head *head)
{
if (!head) {
return;
}
+ element_t *n, *s;
+ list_for_each_entry_safe (n, s, head, list)
+ q_release_element(n);
- struct list_head *current = head->next;
- while (current != head) {
- struct list_head *tmp = current;
- current = current->next;
- free(tmp);
- }
free(head);
}
```
### `q_insert_head`
用於在雙向鏈結串列的頭部插入一個新元素,新元素中包含了一個字串`s`。如果插入成功返回 `true`,否則返回 `false`。
```c
/* Insert an element at head of queue */
element_t *new_element = malloc(sizeof(element_t));
if (!head || !new_element) {
return false;
}
new_element->value = strdup(s);
if (!new_element->value) {
free(new_element);
return false;
}
list_add(&new_element->list, head);
return true;
```
### `q_insert_tail`
將`q_insert_tail`中的差異在於插入節點的位置不同,因此只須修改最後一行。
```diff
- list_add(&new_element->list, head);
+ list_add_tail(&new_element->list, head);
```
觀摩 [164253](https://hackmd.io/@mGrArnQ0STevIEXX7Ss2rg/linux2024-homework1),可以在 `q_insert_tail` 中直接呼叫 `q_insert_head`,並將第一個參數改為 `head->prev` 即可。
```c
bool q_insert_tail(struct list_head *head, char *s)
{
return q_insert_head(head->prev, s);
}
```
### `q_remove_head`
Delete 與 Remove 的概念可以參考 [2024 年 Linux 核心設計/實作課程作業 —— lab0 (A)](https://hackmd.io/@sysprog/linux2024-lab0/%2F%40sysprog%2Flinux2024-lab0-a) 提及的內容,主要的差異為目標節點還是否存在於記憶體空間。
* "Remove" 這動作對於 linked list 來說,是斷開節點和節點的關聯,也就是說原本若是 A $\to$ B $\to$ C,在 remove(B) 操作完成後,就會變成 A $\to$ C 的節點關係。特別注意到 B 這個節點其實還存在於記憶體中,只是我們無法從節點間的關係看出來。於是 "remove" 可解讀為 "take away" (將某物帶走)。
* "Delete" 則有 "erase" 的涵義,若用於 linked list,則指某個節點將被「抹除」,對應的記憶體空間可能會被釋放 (release) 或回收 (reclaim),並非只是指標的變化。
從雙向鏈結串列的 head 移除一個元素。將移除的元素中的字串複製到 `sp` 指向的緩衝區中,並確保字串以空字串終止。
```c
/* Remove an element from head of queue */
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (head == NULL || list_empty(head)) {
return NULL;
}
element_t *n = list_first_entry(head, element_t, list);
list_del(head->next);
if (sp != NULL) {
strncpy(sp, n->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
return n;
}
```
### `q_remove_tail`
將 `q_remove_tail` 中的差異在於插入節點的位置不同,因此改從 tail 獲取最後一個元素,並刪除最後一個元素。
```diff
- element_t *n = list_first_entry(head, element_t, list);
- list_del(head->next);
+ element_t *n = list_last_entry(head, element_t, list);
+ list_del(head->prev);
```
參考 [164253](https://hackmd.io/@mGrArnQ0STevIEXX7Ss2rg/linux2024-homework1),
和 `q_insert_tail` 一樣 可以簡單的用 `q_remove_head` 完成
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
return q_remove_head(head->prev, sp, bufsize);
}
```
### `q_delete_mid`
參考 [你所不知道的 C 語言: linked list 和非連續記憶體](<https://hackmd.io/@sysprog/c-linked-list>)
方法:**使用快慢指標**
```c
/* Delete the middle node in queue */
bool q_delete_mid(struct list_head *head)
{
// https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
struct list_head *slow = head->next;
if (!head)
return false;
for (struct list_head *fast = head->next; fast != head && fast->next != head; fast = fast->next->next) {
slow = slow->next;
}
list_del(slow);
q_release_element(list_entry(slow,element_t,list));
return true;
}
```
### `q_delete_dup`
原本誤以為題目的意思是遇到重複,只要留下一個元素,並把後面連續重複的元素都刪除。
後來依題意改成刪除所有連續重複的元素。
```diff
/* Delete all nodes that have duplicate string */
bool q_delete_dup(struct list_head *head)
{
// https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
if (!head)
return false;
struct list_head *node;
bool flag = false;
for(node = head->next ; node->next!=head ;){
- element_t *prev = list_entry(node->prev, element_t, list);
element_t *current = list_entry(node, element_t, list);
element_t *next = list_entry(node->next, element_t, list);
if (strcmp(current->value, next->value) == 0) {
list_del(&next->list);
q_release_element(next);
flag = true;
} else {
if(flag){
- list_del(&prev->list);
- q_release_element(prev);
// 刪除當前節點,並重置標誌
+ struct list_head *temp = node->next; // 保存下一個節點的地址
+ list_del(node);
+ q_release_element(current);
+ node = temp; // 更新node為下一個節點
+ flag = false;
}
+ else{
node = node->next;
+ }
- flag = false;
}
}
// 處理最後有重複的節點
+ if (flag) {
+ element_t *c = list_entry(node, element_t, list);
+ list_del(node);
+ q_release_element(c);
+ }
return true;
}
```
觀摩 [96121503](https://hackmd.io/@jill96121503/linux2024-homework1),自我檢討,其中:`for loop` 可以善用 list.h 的 `list_for_each_entry_safe` 完成,使程式碼更加簡潔。
### `q_swap`
原本想法兩兩互換,後來參考 [`laneser`](https://hackmd.io/cYPKQL0_SWeekMGR9HuvVQ?view) 同學方法: **先移除一個節點,再加入在交換的位置上**。
```c
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
if (!head)
return;
struct list_head *node;
for (node = head->next; (node->next != head) && (node != head);
node = node->next) {
struct list_head *next = node->next;
list_del(node);
list_add(node, next);
}
```
觀摩 [yutingshih](https://hackmd.io/@yutingshih/linux2024-homework1),發現可以善用 list.h 中的 `list_move_tail()` 撰寫。
>`list_move(node, head)`:將 node 移動到 head 的後面,成為鏈結串列的第一個節點。
>`list_move_tail(node, head)`:將 node 移動到 head 的前面,成為鏈結串列的最後一個節點。
:::danger
改進你的漢語表達。
:::
```c
list_for_each(node, head) {
if (node->next == head)
break;
list_move_tail(node->next, node);
}
```
### `q_reverse`
利用 list.h 中的`list_move` ,將鏈結串列中的結點一個一個往前移。
```c
struct list_head *current, *s;
list_for_each_safe (current, s, head){
list_move(current, head);
}
```
### `q_reverseK`
逐一走訪鏈結串列,每當計數達到 `k` 時,就將當前到達的位置之前的部分切割出來,翻轉,然後接回原來的位置,直到逐一走訪完整個鏈結串列。
```c
/* 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 || list_empty(head))
return;
struct list_head *it, *safe, *cut;
int count = k;
cut = head;
list_for_each_safe (it, safe, head) {
if (--count)
continue;
LIST_HEAD(tmp);
count = k;
list_cut_position(&tmp, cut, it);
q_reverse(&tmp);
list_splice(&tmp, cut);
cut = safe->prev;
}
}
```
### `q_ascend`
透過外層循環逐一走訪整個鏈結串列,對於每個節點,使用內層循環 **比較其右側所有節點的值。**
如果發現右側有節點的值大於當前節點的值,則標記當前節點為需要刪除。
逐一走訪完成後, `list_del` 將需要刪除的節點從鏈結串列中移除並 `q_release_element` 釋放相關資源。
:::danger
程式碼註解總是用美式英語書寫!
:::
```c
/* Remove every node which has a node with a strictly less value anywhere to
* the right side of it */
int q_ascend(struct list_head *head)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
if (!head || list_empty(head))
return 0;
struct list_head *cur = head->next;
while (cur != head) { // 外層循環逐一走訪整個 linked list
struct list_head *next_for_comparison = cur->next;
bool should_delete = false;
while (next_for_comparison !=
head) { // 外層循環逐一走訪當前節點右側的所有節點
element_t *cur_element = list_entry(cur, element_t, list);
element_t *next_element =
list_entry(next_for_comparison, element_t, list);
if (strcmp(cur_element->value, next_element->value) > 0) {
should_delete = true;
break; // 找到一個嚴格更大的值,當前節點需要被刪除
}
next_for_comparison = next_for_comparison->next;
}
struct list_head *next = cur->next; // 保存當前節點的下一個節點
if (should_delete) {
list_del(cur); // 從 linked list 中刪除當前節點
element_t *to_delete = list_entry(cur, element_t, list);
q_release_element(to_delete); // 釋放節點資源
}
cur = next; // 移動到下一個節點
}
return q_size(head);
}
```
:::danger
程式碼列出是為了討論和檢討,抽離出關鍵程式碼來討論。
:::
### `q_descend`
和`q_ascend` 相同,修改判斷式即可。
```diff
- if (strcmp(cur_element->value, next_element->value) > 0)
+ if (strcmp(cur_element->value, next_element->value) < 0)
```
### `q_merge_two`
合併兩個雙向鏈結串列 `first` 和 `second`。使用一個臨時鏈結串列 `tmp` 來存放合併後的結果。
並逐一比較 `first` 和 `second` 鏈結串列中的元素,按照元素值的**升序**將元素移動到 `tmp` 鏈結串列中。
當其中一個鏈結串列被完全移動完畢後,善用 list.h中的`list_splice`,將剩餘的元素也加入到 `tmp` 中,並最終使用`list_splice_tail_init`將 `tmp` 鏈結串列中的所有元素移動回 first 鏈結串列中。
```c
static int q_merge_two(struct list_head *first, struct list_head *second)
{
if (!first || !second)
return 0;
int count = 0;
LIST_HEAD(tmp);
while (!list_empty(first) && !list_empty(second)) {
element_t *f = list_first_entry(first, element_t, list);
element_t *s = list_first_entry(second, element_t, list);
if (strcmp(f->value, s->value) <= 0)
list_move_tail(&f->list, &tmp); //f 接到 tmp 後面
else
list_move_tail(&s->list, &tmp);
count++;
}
count += q_size(first) + q_size(second);
list_splice(&tmp, first);
list_splice_tail_init(second, first);
return count;
}
```
### `q_sort`
以 **Divide and conquer** 實作 Merge Sort 對鏈結串列進行排序。我使用的方法是先以**升序**作排列,最後判斷 descend 值,判斷是否需要反轉(reverse)。
:::danger
你如何確保目前的測試程式已涵蓋排序演算法的最差狀況?
:::
```c
/* Sort elements of queue in ascending/descending order */
void q_sort(struct list_head *head, bool descend)
{
/* Try to use merge sort*/
if (!head || list_empty(head) || list_is_singular(head))
return;
/* Find middle point */
struct list_head *mid, *left, *right;
left = right = head;
do {
left = left->next;
right = right->prev;
} while (left != right && left->next != right);
mid = left;
/* Divide into two part */
LIST_HEAD(second);
list_cut_position(&second, mid, head->prev);
/* Conquer */
q_sort(head, false);
q_sort(&second, false);
/* Merge */
q_merge_two(head, &second);
if (descend == true) {
q_reverse(head);
}
}
```
:::warning
TODO: 思考如何避免遞迴
:::
### `q_merge`
處理了將多個排序好的子佇列合併成一個大佇列,並根據指定順序進行排序的任務。
這些子佇列使用 `queue_contex_t` 結構表示,並藉由 `chain` 成員鏈接在一個主要的鏈結串列 head 中。合併後的佇列給可以根據 `descend` 參數以升序或降序進行排序。
```c
/* Merge all the queues into one sorted queue, which is in ascending/descending
* order */
int q_merge(struct list_head *head, bool descend)
{
// https://leetcode.com/problems/merge-k-sorted-lists/
if (!head || list_empty(head))
return 0;
if (list_is_singular(head))
return list_first_entry(head, queue_contex_t, chain)->size;
LIST_HEAD(tmp);
queue_contex_t *cur, *safe;
queue_contex_t *first = list_first_entry(head, queue_contex_t, chain);
list_for_each_entry_safe (cur, safe, head, chain) {
list_splice_init(cur->q, &tmp);
}
int size = q_size(&tmp);
list_splice_init(&tmp, first->q);
q_sort(first->q, descend);
return size;
}
```
---
## Valgrind + 自動測試程式
閱讀 [The Valgrind Quick Start Guide](https://valgrind.org/docs/manual/quick-start.html#quick-start.intro) 得知,Valgrind 工具套件提供了許多 debugging 和分析工具,可協助我的程式更快、更正確,其中最受歡迎的是 Memcheck。它可以檢測 C 和 C++ 程式中常見的許多與記憶體相關的錯誤,這些錯誤可能導致崩潰和不可預測的行為。
程式將比正常運行速度慢得多,並且使用更多記憶體。Memcheck 將發出有關其檢測到的**記憶體錯誤和洩漏**的訊息。
### massif
它測量程式使用了多少 heap 記憶體,測量程式堆疊的大小。此外,某些空間洩漏是傳統洩漏檢查器(例如 Memcheck )無法偵測到的。
>這是因為記憶體實際上並沒有丟失,仍然有一個指向它的指標,但它沒有被使用。隨著時間過去,存在此類洩漏的程式會增加其使用的記憶體量,Massif 可以幫助識別這些洩漏。
```
$ valgrind --tool=massif ./qtest -f <trace file>
```
運行 trace-017-complexity
![image](https://hackmd.io/_uploads/r10vqBKR6.png)
`test_const` 的記憶體使用量穩定且持續增長,代表它沒有釋放記憶體。
:::warning
TODO:
為何在 "Peak of 1.7 MiB at snapshot #87" 記憶體使用會達到峰值。
:::
---
## 在 qtest 提供新的命令 shuffle
閱讀 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm) 演算法之後,實作洗牌(shuffle)。
首先在 `qtest.c` 中 `ADD_COMMAND`,在開始實做 `q_shuffle`。
原本直接將 `q_shuffle` 實作在 `queue.c` 中,並在 `queue.h` 新增 `q_shuffle` 的宣告 ,git commit 之後出現錯誤訊息如下:
```shell
[!] You are not allowed to change the header file queue.h or list.h
```
因此得知不能修改 `queue.h` 檔,於是我多寫了兩個檔案,分別是:`shuffle.h` 和 `shuffle.c`。
> commit [e26bd21](https://github.com/56han/lab0-c/commit/e26bd2185f27fa7c4c8a06ff55eb8a006db09878)
commit `shuffle.c` 時,出現了以下錯誤
```shell
Fail to pass static analysis.
```
是因為 [cppcheck](http://cppcheck.net/manual.html) 檢查到了非標準代碼,如果想要過濾掉某些錯誤,可以加上 `cppcheck-suppress aaaa` 。因此我加入註解 `// cppcheck-suppress nullPointer` 以忽略警告。
```c
element_t *oldnode =
list_entry(old, element_t, list); // cppcheck-suppress nullPointer
```
### 統計方法驗證 `shuffle`
```
Expectation: 41666
Observation: {'1234': 41617, '1243': 41491, '1324': 41902, '1342': 42053, '1423': 41613, '1432': 41631, '2134': 41597, '2143': 41646, '2314': 41615, '2341': 41697, '2413': 41729, '2431': 41872, '3124': 41441, '3142': 41696, '3214': 41700, '3241': 41560, '3412': 41635, '3421': 41660, '4123': 41502, '4132': 41584, '4213': 41759, '4231': 41908, '4312': 41402, '4321': 41690}
chi square sum: 12.808332933326932
```
從圖表來看 shuffle 的頻率大致符合 Uniform distribution。
![Figure_1](https://hackmd.io/_uploads/HyXU-GD0p.png)
### 論文〈Dude, is my code constant time?〉
Dudect 是一種用於評估軟體實作是否符合常數時間執行的工具,對於密碼學實作的安全性非常重要。常數時間執行能夠防範定時攻擊 (timing attack),這是一種利用密碼學運算的執行時間差異,來推測密鑰或機密資訊的側錯分析攻擊手法。
它的貢獻在於以很小的實作成本,提供高度的安全性檢測能力。使用的 t-test 是用於檢測算法執行時間是否存在統計學上的顯著差異,這對於偵測側通道攻擊非常重要。
#### 程式實作的原理
[dudect.h](https://github.com/oreparaz/dudect/blob/master/src/dudect.h) 過程就是一個迭代的統計檢測,透過不斷收集新的測量數據、更新統計數據、設置合理的閾值過濾極端值,最終得出被測試函數是否存在 Timing leakage 風險。
其中 `prepare_percentiles()` 函式,是先**將執行時間進行排序**,接著從排序後的 `ctx->exec_times` 執行時間資料中給定百分位數對應的值。
此函式目的在於計算出執行時間閾值,用於後續過濾極端值。透過設置不同的閾值,可以有選擇地過濾掉測量值中的極值,從而獲得更加準確的統計結果。
```c
static void prepare_percentiles(dudect_ctx_t *ctx) {
qsort(ctx->exec_times, ctx->config->number_measurements, sizeof(int64_t), (int (*)(const void *, const void *))cmp);
for (size_t i = 0; i < DUDECT_NUMBER_PERCENTILES; i++) {
ctx->percentiles[i] = percentile(
ctx->exec_times, 1 - (pow(0.5, 10 * (double)(i + 1) / DUDECT_NUMBER_PERCENTILES)),
ctx->config->number_measurements);
}
}
```
另外 `update_statistics` 中以捨棄前 10 筆測量,以避免一開始測量的不穩定性。`difference` 儲存每次迭代的執行時間差異,對每個有效的 `difference` 執行 t-test,檢測算法執行時間是否存在統計學上的顯著差異。
迴圈走訪多個剪裁閾值,只對那些低於特定閾值的 `difference` 執行 t-test。這可以幫助識別在不同的執行時間閾值下,算法的行為是否存在顯著的統計差異。
```c
static void update_statistics(dudect_ctx_t *ctx) {
for (size_t i = 10 /* discard the first few measurements */; i < (ctx->config->number_measurements-1); i++) {
int64_t difference = ctx->exec_times[i];
if (difference < 0) {
continue; // the cpu cycle counter overflowed, just throw away the measurement
}
// t-test on the execution time
t_push(ctx->ttest_ctxs[0], difference, ctx->classes[i]);
// t-test on cropped execution times, for several cropping thresholds.
for (size_t crop_index = 0; crop_index < DUDECT_NUMBER_PERCENTILES; crop_index++) {
if (difference < ctx->percentiles[crop_index]) {
t_push(ctx->ttest_ctxs[crop_index + 1], difference, ctx->classes[i]);
}
}
// ... 以下省略
}
}
```
#### `lab0-c` 加入具備 `percentile` 的處理
> commit [e91a53f](https://github.com/56han/lab0-c/commit/e91a53f4484791b30d30d114e25292fb1343d6ea)
在 `dudect/fixture.c` 中,我發現 `doit` 這個函式實作與 `dudect.h` 主要實作功能類似,但沒有實作 `percentile` 這個部份,造成極端值沒有被去除,導致判斷結果出錯。
將 [dudect.h](https://github.com/oreparaz/dudect/blob/master/src/dudect.h) 處理 `percentile` 的部分加入 `lab0-c` 的 `fixture.c` 後便可以正確分析 `insert_head`, `insert_tail`, `remove_head`, `remove_tail` 的執行時間。
## 研讀並嘗試引入 Linux 核心原始程式碼的 `lib/list_sort.c`
安裝 perf 及設定系統權限,預設會使 perf 權限不足:
```shell
$ sudo su # As Root
$ sysctl -w kernel.perf_event_paranoid=-1
$ echo 0 > /proc/sys/kernel/kptr_restrict
$ exit
```
### 嘗試引入 `lib/list_sort.c`
將 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 和 [linux/list_sort.h](https://github.com/torvalds/linux/blob/master/include/linux/list_sort.h) 引入本專案中
### 實驗
隨機產生 500000 個字串加入到鏈結串列中,使用不同的 perf 效能分析工具分析兩種不同的排序方式效能上的差異。
traces/trace-sort.cmd:
```
option fail 0
option malloc 0
new
ih RAND 500000
time
sort/lsort
time
```
執行以下指令,以比較執行時間
```shell
perf stat --repeat 5 -e instructions,cycles ./qtest -f traces/trace-sort.cmd
```
q_sort 執行時間表:
| 測試次數 | 執行時間(秒) |
| -------- | -------- |
| 1 | 0.9123 |
| 2 | 0.9287 |
| 3 | 0.9295 |
| 4 | 0.9474 |
| 5 | 0.9541 |
linux 的 list_sort 執行時間表:
| 測試次數 | 執行時間(秒) |
| -------- | -------- |
| 1 | 0.45526 |
| 2 | 0.4762 |
| 3 | 0.46526 |
| 4 | 0.45756 |
| 5 | 0.45429 |
### list_sort 優點
* 使用單向鏈結串列,可以節省空間。
* 使用了 Bottom-Up 合併演算法,與傳統的 Top-Down 合併排序演算法相比,可以減少分割的步驟,節省了時間。
* 透過限制大小比率最多為2:1,可以減少在最終合併步驟中所需的比較次數,也可以避免極端不平衡的情況,從而改善排序的最壞情況效能。
## 使用 address sanitizer
``` shell
make clean
make SANITIZER=1
```
之後執行 `make test` ,沒有出現任何記憶體相關錯誤訊息。
## 整合 tic-tac-toe 遊戲
### 將 jserv/ttt 專案的程式碼部分整合進 lab0-c
將 [jserv/ttt](https://github.com/jserv/ttt/blob/main/zobrist.c) 專案的 `zobrist.[ch]` 會用到 `hlist.h` 相關的程式碼抽出,成為 `hlist.h`,並確保後者可與 `lab0-c` 既有的 `list.h` 並存。
>commit [53ad444](https://github.com/56han/lab0-c/commit/53ad44486cb7971c7b0b24b91b3ca3596e602fa7)
修改 `qtest` 程式,使其新增 `ttt` 命令,其作用是執行與人類對弈的井字遊戲,從 [jserv/ttt](https://github.com/jserv/ttt/tree/main) 的 `main.c` 檔案中提取與遊戲相關的程式碼至專案中的 `console.c`。當輸入指令 `ttt` 後,將繼續執行原始 `main.c` 中的內容,以啟動井字遊戲。
>commit [4876b95](https://github.com/56han/lab0-c/commit/4876b9567332fc00d738a540d47fb3ab8e9e6780)
### Monte Carlo tree search (MCTS) 演算法
此方法依賴平衡探索和利用的智慧搜尋樹。執行隨機採樣並儲存操作統計數據,以便在每次後續迭代中做出更明智的選擇。
#### Monte Carlo tree search 的每個迴圈包括四個步驟:
* 選擇(Selection):從根節點 R 開始,連續向下選擇子節點至葉子節點 L 。
* 擴充(Expansion):除非任意一方的輸贏使得遊戲在 L 結束,否則建立一個或多個子節點並選取其中一個節點 C 。
* 仿真(Simulation):再從節點 C 開始,用隨機策略進行遊戲。
* 反向傳播(Backpropagation):使用隨機遊戲的結果,更新從 C 到 R 的路徑上的節點資訊。
每一個節點的內容代表**勝利次數/遊戲次數**
![MCTS_(English).svg](https://hackmd.io/_uploads/HkKme7hAp.png)