# 2024q1 Homework1 (lab0)
contributed by < `LindaTing0106` >
### Reviewed by `lintin528`
在有多個節點被移動的狀況,像是 `q_reverseK` ,我的做法通常會是直接在原本的鏈結串列中使用 `list_move` 去做移動,這樣可以避免 `cut` 跟 `splice` 的使用,還有一個額外的 `list_head` 結構 `buf` ,這樣的話除了交換的動作外會多花費一些計算量,但這麼做的缺點就是程式的可讀性不如你現在的做法,我認為這邊可以做一下取捨。
## 開發環境
```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
Byte Order: Little Endian
Address sizes: 46 bits physical, 48 bits virtual
CPU(s): 36
On-line CPU(s) list: 0-35
Thread(s) per core: 2
Core(s) per socket: 18
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Core(TM) i9-10980XE CPU @ 3.00GHz
Stepping: 7
CPU max MHz: 4800.0000
CPU min MHz: 1200.0000
BogoMIPS: 6000.00
Virtualization: VT-x
L1d cache: 576 KiB
L1i cache: 576 KiB
L2 cache: 18 MiB
L3 cache: 24.8 MiB
NUMA node0 CPU(s): 0-35
```
## 指定的佇列操作
### `q_new`
在使用 `malloc` 配置空間後,檢查是否有配置成功,如配置失敗則回傳 `NULL` ,如配置成功則讓頭尾指標都指向自己,並回傳自身。
### `q_free`
先檢查 `head` 是否為 NULL,利用 `list.h` 的巨集 `list_for_each_entry_safe` ,將鏈結內每個節點所配置的空間釋放掉。
```c
void q_free(struct list_head *l)
{
if (!l)
return;
element_t *entry, *safe;
list_for_each_entry_safe (entry, safe, l, list){
free(entry->value);
free(entry);
}
free(l);
}
```
### `q_insert_head`
利用 `strcpy` 將來源字串複製給要新增的節點。
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *newnode = malloc(sizeof(element_t));
if (!newnode)
return false;
newnode -> value = malloc((strlen(s) + 1) *sizeof(char));
if (!newnode->value){
free(newnode);
return false;
}
strcpy(newnode -> value,s);
list_add(&newnode->list,head);
return true;
}
```
### `q_remove_head`
利用 `list_first_entry` 找出節點對應在鏈結中的位置,再用 `bufsize` 和節點字串的長度,控制複製幾個字元。
最後使用 `list_del` 將此節點移除。
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || q_size(head) == 0)
return NULL;
element_t *remove = list_first_entry(head, element_t, list);
if (strlen (remove->value)>bufsize){
strncpy(sp, remove->value,bufsize);
*(sp+bufsize-1) = '\0';
}
else
strncpy(sp, remove->value,strlen (remove->value)+1);
list_del(&(remove->list));
return remove;
}
```
### `q_delete_mid`
使用計數的方式尋找鏈結中的中心點,將其移除後,再進行節點配置空間的釋放。
但因為用到 `q_size` 導致時間複雜度為 O(n) 。
```diff
bool q_delete_mid(struct list_head *head)
{
- if (!head || q_size(head) == 0)
+ if (!head || list_empty(head))
return false;
int num = q_size(head);
num = num/2 +1;
int count = 0;
struct list_head *del = head;
while(count != num)
{
del = del->next;
count++;
}
element_t *remove = list_entry(del, element_t, list);
list_del(del);
free(remove->value);
free(remove);
return true;
}
```
:::danger
使用 `diff` 或 `git diff` 命令產生程式碼變更,而非憑感覺標示。
:::
:::warning
這邊我認為可以使用快慢指標實作,他在執行時會有較好的時間局部性,因相鄰的記憶體較容易被連續造訪。
:notes: lintin528
:::
> 以改用快慢指標實作,我知道的是原本的做法會導致 `O(n^2)` ,改用快慢指標則可以降至`O(n)` ,但我的疑惑是這與時間局部性有關係嗎。
### `q_delete_dup`
使用 `find` 來定位要檢查的字串節點,然後通過 `findsame` 遍歷鏈結。如果找到節點字串相同,則將 `findsame` 找到的節點刪除並將 `del` 設為 1 ,表示隨後需要將 `find` 也刪除。
### `q_swap`
使用 `list_move` 將節點兩兩交換。
### `q_reverseK`
:::danger
留意[資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary)
:::
用迴圈的方式去搜索要反向的範圍到哪,呼叫 `list_cut_position` 先把要反向的部分移到 `buf` 中在呼叫寫好的函式<s>實現</s> 實作部分反向的功能。利用 `seark` 保存下次開始反向的起點。
```c
void q_reverseK(struct list_head *head, int k)
{
struct list_head *cur = head;
struct list_head *seark = cur->next;
struct list_head buf;
INIT_LIST_HEAD(&buf);
int count = 1;
while (seark!=head){
while (count!=k){
if (seark == head)
break;
seark = seark->next;
count ++;
}
if (seark == head)
break;
list_cut_position(&buf, cur, seark);
q_reverse(&buf);
seark = buf.prev;
list_splice(&buf,cur);
INIT_LIST_HEAD(&buf);
cur = seark;
seark = seark->next;
count = 1;
}
}
```
### `q_sort`
最開始使用氣泡排序法來處理排序問題。
```c
void q_sort(struct list_head *head, bool descend)
{
int times = q_size(head);
int num = 0;
struct list_head *cur,*nex;
while(num != times){
cur = head->next;
nex = cur->next;
while (nex != head && cur != head){
element_t *curele = list_entry(cur, element_t, list);
element_t *nexele = list_entry(nex, element_t, list);
if (strcmp(curele->value, nexele->value)>=0){
list_move(cur, cur->next);
nex = cur->next;
}
else{
cur = cur->next;
nex = nex->next;
}
}
num = num+1;
}
}
```
但因為時間複雜度太高,而後改為合併排序法。
找到串列的中間節點後,利用迭回方式不斷進行拆分,直到串列中只有一個節點,再對其進行合併。
```c
void q_sort(struct list_head *head, bool descend)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *mid, *left, *right;
left = head->next;
right = head->prev;
while (left != right && left->next != right){
left = left->next;
right = right->prev;
}
mid = left;
struct list_head leftside;
list_cut_position(&leftside, head, mid);
q_sort(&leftside, descend);
q_sort(head, descend);
q_merge_two(head, &leftside, descend);
}
```
### `q_descend`
用兩個指標比較左側的值是否小於右側,如果成立的話則將左邊節點刪除。
為了避免指標需要重新指到 `head->next` ,將指標指向頭,並且用 `next` 的值去進行比較及刪除。
```c
int q_descend(struct list_head *head)
{
struct list_head *one = head;
bool del = 0;
while (one->next != head){
struct list_head *com = one->next;
element_t *oneele = list_entry(one->next, element_t, list);
while(com->next != head){
element_t *comele = list_entry(com->next, element_t, list);
if (strcmp(oneele->value, comele->value)<0){
list_del(one->next);
free(oneele->value);
free(oneele);
del = 1;
break;
}
com = com->next;
}
if (!del){
one = one->next;
}
else
del = 0;
}
return q_size(head);
}
```
:::danger
改進漢語表達。
:::
### `q_merge_two`
合併兩個鏈結串列,建立一個 `head` 當作鏈結串列暫時的頭,比較兩個鍵結串列大小後,將比較完的結果移動到 `head` 的尾端,直到其中一個鍵結串列為空後,將另一個鏈結串列剩餘的節點都移動到 `head` 的尾端。最終再將整個鏈結串列(除了 `head` )接回第一個鏈結串列。
### `q_merge`
因為這裡的 `head` 是佇列的鏈結串列的頭,故我們可以使用這個指標去取得每個佇列,再透過呼叫 `q_merge_two()` 將兩兩佇列合併,一直合併到只剩一個佇列就可以回傳總共的節點數量了。
```c
int q_merge(struct list_head *head, bool descend)
{
queue_contex_t *queue = list_first_entry (head, queue_contex_t, chain);// find the first queue
int num = q_size(queue->q);
if (list_is_singular(head))
return num;
struct list_head * movelist = head->next->next;
queue_contex_t *moveque = list_entry (movelist , queue_contex_t, chain);
while(movelist != head){
num = num + q_size(moveque->q);
q_merge_two(queue->q, moveque->q, descend);
movelist = movelist->next;
moveque = list_entry (movelist , queue_contex_t, chain);
}
return num;
}
```
:::danger
HackMD 不是讓你張貼完整程式碼的地方,GitHub 才是!因此你在開發紀錄只該列出關鍵程式碼 (善用 diff 標示),可附上對應 GitHub commit 的超連結,列出程式碼是為了「檢討」和「便於他人參與討論」,不是用來「假裝自己有付出」
:::
:::warning
若 `q_merge` 中,已經得到兩個鏈結串列的頭且經過排序了,或許可以嘗試用 `q_merge_two` 直接將兩者合併。
:notes: lintin528
:::
> 我在上面的程式中,原本就有使用 q_merge_two 將佇列合併了,不太清楚同學的意思。
## 研讀論文〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉
此論文在介紹時提到了,要去評估實作是否為常數時間複雜度,是一件困難的事,但這就會造成偵測有無 time leaks 的困難度也導致安全性低下(由於 Timing attacks )。
已經有多個可以檢測可變時間程式碼的工具,但多受制於需要去建模硬體的行為。因此作者的貢獻為提出了 dudect ,可以在給予的平台上檢測程式碼是否為常數時間複雜度。
- dudect 利用測試執行時間上有無 Timging leakage ,其中輸入的資料分為固定與隨機,經過後處理後,透過統計測試來嘗試證明 "兩個時序分佈相等" 為假。
- 測試完後需要再接數據進行後處理,方便於後續的統計分析。
- 再經過統計分析試圖反駁 " 兩個時間分佈相等 " ,使用 Welch’s t-test ,測試兩個時序的平均值是否相等,如果沒有通過此測試的話,表示其有時序洩漏的問題。
測試 memcmp 的時間變化性,用此函式去比較一個固定的字串以及輸入字串,使用時間分布圖呈現時,可以發現兩者在圖上的分布明顯不同,固有時序洩漏存在。
### simulation 模式
在 `qtest.c` 中 `insert` 和 `remove` 的區塊中會去檢查 `simulation` 是否為 1 ,如果在 `insert` 中 `simulation` 為 1 時,會使用 `is_insert_tail_const` 和 `is_insert_head_const` 去檢查函式是否是常數時間內可以處理完的。
找尋 `is_insert_tail_const` 函式時,我使用 `vscode` 找查其定義,一到搜尋到 `constant.c` 中。
- 在 `measure` 中,可以看到其會將隨機生成的測資與固定測資放入串列中,並且去儲存他這兩個操作做完後對應的時間。
由於 `fixture.c` 有引用 `constant.c` ,在此接著看程式碼。
- 在 `doit` 中開始執行,一開始會先生成輸入,接著執行 `measure` ,執行完後會將兩個時間相減,之後將這筆資料放入統計中,檢查統計的數量是否已經足夠。
比較 `dudect.h` 與 `fixture.c` 的程式碼後,發現其中 `dudect_main` 對應於 `doit` ,其中差別在於 `dudect_main` 中並沒有使用 `differentiate` ,而是將算執行時間的功能直接整合於 `measure` 中,另一點差別在於 `doit` 中沒有去判斷第一筆測試,在 `dudect` 中作者將第一批次的測試丟棄,並執行 `prepare_percentiles` 作為暖身。並且在於作者的 `update_statistics` 中,統計的動作也是在第十筆後才開始,故更改程式碼。
```diff
+ for (size_t i = 10; i < N_MEASURES - 1; i++) {
- for (size_t i = 0; i < N_MEASURES; i++) {
```
```diff
+ bool first_time = percentiles[DUDECT_NUMBER_PERCENTILES - 1] == 0;
+
+ if (first_time) {
+ // throw away the first batch of measurements.
+ // this helps warming things up.
+ prepare_percentiles(exec_times, percentiles);
+ } else {
+ update_statistics(exec_times, classes, percentiles);
+ ret &= report();
+ }
```
並且加入在 fixture.c 中缺少的程式片段。
其中註解提到此處是為了設置不同的閾值來裁剪測試值,但還不是很理解為何需要這樣做。
```c
static int cmp(const int64_t *a, const int64_t *b) { return (int)(*a - *b); }
static int64_t percentile(int64_t *a_sorted, double which, size_t size) {
size_t array_position = (size_t)((double)size * (double)which);
assert(array_position < size);
return a_sorted[array_position];
}
static void prepare_percentiles(int64_t *exec_times, int64_t *percentiles) {
qsort(exec_times, N_MEASURES, sizeof(int64_t), (int (*)(const void *, const void *))cmp);
for (size_t i = 0; i < DUDECT_NUMBER_PERCENTILES; i++) {
percentiles[i] = percentile(
exec_times, 1 - (pow(0.5, 10 * (double)(i + 1) / DUDECT_NUMBER_PERCENTILES)),
N_MEASURES);
}
}
```
將程式碼更動完後, trace-17-complexity 的 insert 部分就通過了,但 remove 的部分出現非法寫入,一開始以為是使用 strncpy 時的參數設置有誤,後來使用 make valgrind。
```
==930078== Invalid write of size 1
==930078== at 0x484F010: strncpy (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==930078== by 0x10F903: strncpy (string_fortified.h:95)
==930078== by 0x10F903: q_remove_head (queue.c:82)
==930078== by 0x110563: measure (constant.c:121)
==930078== by 0x110B95: doit (fixture.c:166)
==930078== by 0x110B95: test_const (fixture.c:207)
==930078== by 0x110CCE: is_remove_head_const (fixture.c:220)
==930078== by 0x10C4D2: queue_remove (qtest.c:302)
==930078== by 0x10C91C: do_rh (qtest.c:410)
==930078== by 0x10E031: interpret_cmda (console.c:181)
==930078== by 0x10E5E6: interpret_cmd (console.c:201)
==930078== by 0x10E9E7: cmd_select (console.c:610)
==930078== by 0x10F2D3: run_console (console.c:705)
==930078== by 0x10D423: main (qtest.c:1258)
==930078== Address 0x0 is not stack'd, malloc'd or (recently) free'd
```
看到 Address 0x0 發現應該是對 NULL 進行到寫入的動作,所以在將字串複製到 sp 前,需要對 sp 先做檢查。
## 確認分析 Linux 核心的 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 實作並評估其效益
`void list_sort` 中使用合併排序法來做排序,其中它會讓要合併的 list 保持在 2 : 1 ,如果有兩個已經排序好的子串列,他們的節點數量都為 $2^k$ ,在當我們有另一個數量為 $2^k$ 的元素後,才會再將剛剛的兩個子串列合併起來,其大小變為 $2^{k+1}$ ,使最終合併 : 未合併的比例維持在 2 : 1 。在合併排序中使用此種方式,就可以將 $3 \times 2^k$ 個元素放入快取中,避免 Cache Thrashing 。
:::warning
為何此舉可以「避免 Cache Thrashing」?
:::
:::info
想法為:在 count: 5 -> 6 的階段初, pending list 應為: 1-1-2-2 ,為了保持合併 : 未合併的元素為 2 : 1 的狀態,應該要將兩個 $2^2$ 合併為 $2^3$ ,如此就可以維持此比例。
但若不維持此規則,我們先將後面兩個 $2^0$ 進行合併,如此一來在 count: 6 -> 7 時,才會合併兩個 $2^2$ ,從而導致在後面的回合中,若有元素要進行合併,在其快取失誤時,會先取代掉剛才兩個 $2^0$ 在快取中的位置,而在之後若又需要與其合併時,又需要再將其呼叫至快取處,從而導致 Cache Thrashing 發生。
未保持 2 : 1:
```graphviz
digraph count56 {
#rankdir = LR;
node[shape=box];
{rank="same"; 4; 1 ;3 ;5 ; 2 ; 7;};
1->4;
3->5;
2->7;
node[shape="record"];
node1 [label= "<0>1|<1>4|<2>3|<3>5|<4>2|<5>7"];
}
```
```graphviz
digraph count78 {
#rankdir = LR;
node[shape=box];
{rank="same"; 4; 1 ;3 ;5 ; 2 ; 7; 9; 6;};
1->3->4->5;
2->7;
6->9;
node[shape="record"];
node1 [label= "<0>1|<1>4|<2>3|<3>5|<4>6|<5>9"];
}
```
```graphviz
digraph count89 {
#rankdir = LR;
node[shape=box];
{rank="same"; 4; 1 ;3 ;5 ; 2 ; 7; 9; 6; 8;};
1->3->4->5;
2->6->7->9;
node[shape="record"];
node1 [label= "<0>2|<1>7|<2>3|<3>5|<4>6|<5>9"];
}
```
:::
其中合併的動作是由 count 所控制的, count 是已經排好的子串列中節點的個數。每次當 count 增加,我們會將 bit k 設為 1 ,並將其其他位數字設為 0 。也就是說,每次合併只會在 count 為 $n \times 2^k$ ($n$ 為奇數) 時發生。
在程式碼中, `void list_sort` 會先將要進行排序的雙向鏈結串列變為單向,一開始 `*pending` 指向 `NULL` ,當 `list` 不為 `NULL` 時進到迴圈,迴圈內會檢查是否要做合併,並且每次將 `list` 的一個元素給到 `pending` 中, `pending` 中是用 prev 來串接節點。
最剛開始時,此時 `pending` 中沒有元素, `bit` 為 0 。
```graphviz
digraph all_list {
#rankdir = LR;
node[shape=box];
{rank="same"; 4; 1 ;3 ;5 ; 2 ; 7;}
4 -> 1 -> 3 -> 5 -> 2 -> 7;
node[shape=none];
{rank="same"; 4; 1 ;3 ;5 ; 2 ; 7;NULL}
7->NULL;
pending->NULL;
list->4;
}
```
將 `list` 的元素給予 `pending` , `count` 為 1 ,所以還是不會做合併的動作。
```graphviz
digraph all_list {
rankdir = LR;
node[shape=box];
4;
1 -> 3 -> 5 -> 2 -> 7;
node[shape=none];
//{rank="same"; 4; 1 ;3 ;5 ; 2 ; 7;NULL}
7->NULL;
pending->4;
list->1;
4->NULL;
}
```
此時 `count` 為 2'b10 了,故進到可以合併的條件式中。
```graphviz
digraph all_list {
rankdir = LR;
node[shape=box];
4,1;
3 -> 5 -> 2 -> 7;
node[shape=none];
//{rank="same"; 4; 1 ;3 ;5 ; 2 ; 7;NULL}
7->NULL;
pending->1;
list->3;
1->4;
4->NULL;
a->1;b->4;
}
```
開始做合併後,就用 `next` 來連接節點了,原本在 `void list_sort` 中的指標 `a` 和 `b` 會變成 `b` 和 `a` 的順序傳入 `struct list_head *merge` 中。
排序後變為圖中樣子 (箭頭表示 next )。
```graphviz
digraph all_list {
rankdir = LR;
node [shape= "record"];
node[shape=box];
4,1;
node[shape=none];
//{rank="same"; 4; 1 ;3
head ->1;
1->4;
4->NULL;
}
```
則利用以下表格即可推算出當 `count` 數值為何時,會不會做合併、要合併的串列,與做完合併後串列的模樣。
| 開始迭代時的 Count | 是否進行合併 | 開始迭代時的 pending list | 合併操作後 pending list |
|:-------------------------:|:-----:|:--------------------------------:|:------------------------------:|
| 0 | No | NULL | X |
| 1 | No | 1 | X |
| 2 | Yes | 1-1 | 2 |
| 3 | No | 1-2 | X |
| 4 | Yes | 1-1-2 | 2-2 |
| 5 | Yes | 1-2-2 | 1-4 |
| 6 | Yes | 1-1-4 | 2-4 |
| 7 | No | 1-2-4 | X |
| 8 | Yes | 1-1-2-4 | 2-2-4 |
| 9 | Yes | 1-2-2-4 | 1-4-4 |
| 10 | Yes | 1-1-4-4 | 2-4-4 |
| 11 | Yes | 1-2-4-4 | 1-2-8 |
| 12 | Yes | 1-1-2-8 | 2-2-8 |
| 13 | Yes | 1-2-2-8 | 1-4-8 |
| 14 | Yes | 1-1-4-8 | 2-4-8 |
| 15 | No | 1-2-4-8 | X |
| 16 | Yes | 1-1-2-4-8 | 2-2-4-8 |
> 取自 [DokiDokiPB](https://hackmd.io/@DokiDokiPB/SJfES914O)
### 嘗試 lib/list_sort.c 引入到 lab0-c 專案
將 Linux 的 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 引入 qtest.c 中,途中會遇到很多衝突,以下是我有遇到且解決掉的方式。
- `int count` 的加入:在 `list_sort.c` 中有出現很多傳遞 `priv` 的地方,觀察 timsort 程式碼發現其 priv 是用來計算比較次數的,故加入此變數。
- `u8` 改為 `uint8_t`:編譯程式時,總會出現這個未定義過的型態,故我將其改為了型態相同的 `uint8_t` 。
- `likely` 、 `unlikely` 的定義:由於此函式是為了告訴編譯器,哪個分支更有可能發生或不太可能發生,可以幫助編譯器,但由於無法直接引入 <linux/compiler.h> ,故需要自行定義其行為。
- 加入 `int compare` 函式。
使用 `perf stat --repeat 5 -e instructions,cycles ./qtest -f traces/trace-sort.cmd` 比較在 `queue.c` 中實作的 `q_sort` 和 `list_sort` 間的效能差異。
可以看出 `list_sort` 的 `cycles` 數和 `instructions` 數都少於我們實作的 `q_sort` 。
| | instructions | cycles |
|:---------:|:------------:|:----------:|
| q_sort | 47,802,626 | 30,013,600 |
| list_sort | 46,498,077 | 29,716,110 |
### 嘗試 timsort 引入到 lab0-c 專案
將 timsort 放入 lab0-c 並加以測試其效能,在我測試出來的結果可以看到在指令數量上跟 q_sort 差不多,但其 CYCLE 明顯少於 q_sort 。
| | instructions | cycles |
|:---------:|:------------:|:----------:|
| q_sort | 47,802,626 | 30,013,600 |
| timsort | 47,693,459 | 29,732,870 |
| list_sort | 46,498,077 | 29,716,110 |
> 參考 [vax-r](https://hackmd.io/@vax-r/linux2024-homework1#%E6%AF%94%E8%BC%83-timsort-%E8%88%87-list_sort) 對於兩種排序的比較方式
再利用作業二提供的程式來比較 list_sort 和 timsort 之間的比較次數。
一開始先利用原本程式中亂數產生的方式,來看通常情況下他們兩者之間的比較次數。
| 亂數串列 | timsort | list_sort |
|:--------------:|:-------:|:---------:|
| 第一次比較次數 | 9480 | 8709 |
| 第二次比較次數 | 9409 | 8728 |
| 第三次比較次數 | 9603 | 8707 |
| 第四次比較次數 | 9570 | 8700 |
| 第五次比較次數 | 9444 | 8723 |
如果在串列中的元素都為`單調遞增`時,資料會呈現。
| 單調遞增串列 | timsort | list_sort |
|:--------------:|:-------:|:---------:|
| 第一次比較次數 | 999 | 5036 |
| 第二次比較次數 | 999 | 5036 |
| 第三次比較次數 | 999 | 5036 |
| 第四次比較次數 | 999 | 5036 |
| 第五次比較次數 | 999 | 5036 |
如果在串列中的元素都為`單調遞減`時,資料會呈現。
| 單調遞減串列 | timsort | list_sort |
|:--------------:|:-------:|:---------:|
| 第一次比較次數 | 999 | 4940 |
| 第二次比較次數 | 999 | 4940 |
| 第三次比較次數 | 999 | 4940 |
| 第四次比較次數 | 999 | 4940 |
| 第五次比較次數 | 999 | 4940 |
可以看觀察出,如果在一般串列內容為亂數的情況下, list_sort 的表現通常是優於 timsort 的,但如果在串列有排序好的情況下, timsort 的表現會優於 list_sort 。
## 在 qtest 提供新的命令 shuffle
一開始會想要在 `queue.c` 中新增洗牌指令,但由於 `qtest.c` 中使用的是 `queue.h` 的函式庫,如果直接將其寫入 `queue.c` 的話需要在 `qtest.c` 中也引用 `queue.c` ,或是將指令宣告寫入 `queue.h` ,但我之前在寫作業時有更動過 `queue.h` 檔,導致其在 github 時會執行失敗,所以我便直接在 `qtest.c` 新增 `q_shuffle` 的函式。
實作細節參考 Fisher–Yates shuffle 演算法。
比較特別的地方在於使用測試程式時,一開始會找不到引用的函式庫,根據 `driver.py` 判斷是由於缺少了 `#!/usr/bin/env python3` 環境的設置。補上後又跑出了
`usr/bin/env: 'python3\r': No such file or directory`的錯誤,此錯誤是由於 Linux 會吃到此行指令的回車鍵,解決方法為在 vim 中輸入 `:set ff=unix` 後保存並且退出。
可以看到各個排列的分布都差不多。
![shuffle](https://hackmd.io/_uploads/Hy8Ee6BCa.png)
```
Expectation: 41666
Observation: {'1234': 41727, '1243': 41735, '1324': 41928, '1342': 41563, '1423': 41972, '1432': 41933, '2134': 41232, '2143': 41571, '2314': 41819, '2341': 41784, '2413': 41516, '2431': 41691, '3124': 41746, '3142': 41903, '3214': 41407, '3241': 41363, '3412': 41763, '3421': 41531, '4123': 41590, '4132': 41537, '4213': 41755, '4231': 41353, '4312': 41756, '4321': 41825}
chi square sum: 22.111073777180437
```
## tic-tac-toe
將 [jserv/ttt](https://github.com/jserv/ttt) 專案的程式碼部分整合進 lab0-c ,故將需要編譯的檔案額外寫入 Makefile 中。
並在 qtest 中額外加入 do_ttt 指令。
### 將 mcts 演算法改為定點數計算
使條件式判斷使用的演算法為 mcts , 故在程式碼中添加 `#define USE_MCTS` 。
```c
#ifdef USE_RL
#include "agents/reinforcement_learning.h"
#elif defined(USE_MCTS)
#include "agents/mcts.h"
#else
#include "agents/negamax.h"
#endif
```
```diff
@@ -8,6 +8,8 @@
#include <string.h>
#include <time.h>
+#define USE_MCTS
```
並且將程式碼中使用 `double` 型態的變數改為 `unsigned long`。這裡我設的 `FRACTIONAL_BITS` 大小為 `20` 。由於在原本的 `uct_score` 中使用到了 `double` 型態的 `sqrt` 和 `log` ,在這邊我創了兩個 `unsigned long` 的函式 `fixed_sqrt` 和 `fixed_taylor_series_log` 來取代浮點數運算的函式。
fixed_sqrt 中使用逼近的方式找尋某數的平方根。
> log(1 + x) = x - x^2/2 + x^3/3 - x^4/4 + x^5/5 - ...
fixed_taylor_series_log 則是使用泰勒展開式求得某數的對數。
:::warning
但實作下來造成運算變慢,還需要釐清是哪個函式導致。
:::
> [commit 7bca141](https://github.com/LindaTing0106/lab0-c/commit/7bca1410c0bb410abba49b847fa004c93da9c0aa)
### 引入「電腦 vs. 電腦」的對弈模式
在閱讀 [並行程式設計: 排程器原理](https://hackmd.io/@sysprog/concurrency/%2F%40sysprog%2Fconcurrency-sched) 後,因為 [coro](https://github.com/sysprog21/concurrent-programs/tree/master/coro) 對我來說比較容易理解,所以我選擇使用他作為引入程式的協同式多工。
一開始我先將需要被共享的資源宣告為全域變數,使的每個 task 都可以對其直接進行存取。
```c
static char table[N_GRIDS];
static int AI1_w = 0;
static int AI2_w = 0;
```
在進入排程指令前,先將 `table` 初始化。
之後程式就會依序進到 "AI_1 -> 鍵盤與印出事件處理 -> AI_2 -> 鍵盤與印出事件處理" 此流程,但這邊會有個問題是因為使用協同式分工,每個工作一定都是等上一個工作完成後,才會開始去做,那這樣就導致如果 AI_1 處理太久,使用者就能感覺到有延遲的問題發生。
這邊還在想是不是只能使用搶占式分工去避免。
而鍵盤事件處理是參考[〈Build Your Own Text Editor〉](https://viewsourcecode.org/snaptoken/kilo/) ,實作出 CTRL + q 會跳出 ttt ,而 CTRL + p 可以暫停 / 開始執行程式。
中間遇到的問題為,跳出 ttt 後,在進入會發生 segmentation fault ,經檢查發現問題在排程時的 `static int i` ,將其改為 `int i` 之後即可排除錯誤。
> [commit 88cc8b0](https://github.com/LindaTing0106/lab0-c/commit/88cc8b027b30d2855fdb1d312dd7167e9a77b4e6)