# 2024q1 Homework1 (lab0) contributed by < `dockyu` > #### Reviewed by `gawei1206` 在 `q_ascend`, `q_descend`, `q_merge` 這三個函式中的回傳值都有要求,請你看一下 `queue.h` 中的敘述 #### Reviewed by `fatcatorange` 部份開發流程可以更多的描述作法,或附上對應的 commit 連結 #### Reviewed by `eleanorLYJ` 是否考慮用亂數以外不同的資料分布的資料作測試? ## 開發環境 ```bash $ 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: 46 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 20 On-line CPU(s) list: 0-19 Vendor ID: GenuineIntel Model name: 13th Gen Intel(R) Core(TM) i5-13500 CPU family: 6 Model: 191 Thread(s) per core: 2 Core(s) per socket: 14 Socket(s): 1 Stepping: 2 CPU max MHz: 4800.0000 CPU min MHz: 800.0000 BogoMIPS: 4992.00 ``` ## 針對佇列操作的程式碼實作 ### `q_new` :::danger 避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景。 ::: 不能在 function 裡使用 `LIST_HEAD(head)` ,會在返回時被釋放掉 >能否說明上述場景發生在什麼地方,並更詳細的在 commit message 或筆記中描述作法 >[name=fatcatorange] ### `q_free` 不能直接釋放掉 object element_t ,因為 element_t 裡有一個 attribute 是指標,指到的 object 不會被釋放 呼叫`queue.h` 的 `q_release_element`,`q_release_element`被用來釋放掉**整個** element :::danger 避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景。 > 收到 ::: ```c static inline void q_release_element(element_t *e) { test_free(e->value); test_free(e); } ``` ### q_insert_head 如果直接將 parameter s assign 給新的 element_t 的 value 會出錯 ```c element_t *elem = malloc(sizeof(element_t)); elem->value = s; ``` ``` cmd> new l = [] cmd> ih hello ERROR: Need to allocate and copy string for new queue element l = [hello] cmd> ih fuc ERROR: Need to allocate and copy string for new queue element l = [fuc �c�\] cmd> ih ddd ERROR: Need to allocate and copy string for new queue element l = [ddd �c�\ ddd] cmd> ih sss ERROR: Need to allocate and copy string for new queue element l = [sss �c�\ sss �c�\] cmd> ih jfjd ERROR: Need to allocate and copy string for new queue element l = [jfjd �c�\ jfjd �c�\ jfjd] Error limit exceeded. Stopping command execution ``` 改為使用`strdup`,會自動複製字串到新的記憶體空間(保證足夠?) ```c char *s_cpy = strdup(s); if (!s_cpy) { free(elem); return false; /* copy s fault */ } elem->value = s_cpy; ``` ### q_swap 兩兩交換可以選擇 1. 和後一個 swap 2. 和前一個 swap 有兩個地方會遇到問題 1. 在第1個向前swap 2. 在最後一個向後交換 接著我又發現 1. 在第1個向前 swap -> 在奇數個 node 向前交換 2. 在最後一個向後交換 -> 只會在奇數個 node 的 queue 出現,也就是在奇數個 node 向後交換 所以問題永遠都出在奇數,剛好這個問題必須間隔一個再做交換,所以選擇在偶數向前交換可以讓會出問題的 case 不存在 ```c struct list_head *li, *safe; bool even = false; struct list_head *swap, *left, *right; list_for_each_safe (li, safe, head) { if (even) { swap = li->prev; left = swap->prev; right = li->next; /* 交換 li 以及 swap */ } even = !even; } ``` ### q_sort split & compare 參考 [25077667](https://github.com/25077667/lab0-c/blob/dev/queue.c) 一開始想用 indirect pointer 指到 merge 的 list 最尾端 ,再把下一個接在尾部 為了方便 split 我先將 list 從環狀變成一條,每次 split 都將 mid 的 next 設為 NUL ,但是這樣就不能用 list.h 提供的 function 後來參考[25077667](https://github.com/25077667/lab0-c/blob/dev/queue.c)的發現 只要將新建的鏈結串列 merge 到原本的鏈結串列上就可以了,但是必須嚴格限制哪一個是新的哪一個是舊的,以免新的鏈結串列的 head object 在 function return 被釋放 merge 永遠比較**新建的鏈結串列的第一個**以及 走在舊的 list 上的 li ,直到其中一個佇列走完 ```c while (!list_empty(head_from) && li != head_dest) { struct list_head *first = head_from->next; if (comp_func(li, first)) ``` 只有兩種情況 1. 新的佇列走完 2. 舊的佇列走完 -> 新的佇列現存的都比舊的佇列還大 不管是哪種情況,都可以直接將新的接在舊的後面,完成這一次的 merge :::danger 改進你的漢語表達。 > yes teacher ::: >應為 NULL 而不是 NUL >[name=fatcatorange] ### 新增 shuffle 命令 在 `qtest.c` 中新增命令 ```c ADD_COMMAND(shuffle, "Shuffle the nodes of the queue by Fisher–Yates shuffle algorithm", ""); ``` 編譯並執行,可以看到多出了一個命令 ```bash shuffle | Shuffle the nodes of the queue by Fisher–Yates shuffle algorithm ``` 因為 shuffle 對佇列的處理和 descend 幾乎一樣,都不接受參數並且只對兩個元素以上的佇列有效,所以我直接複製 do_descend 函式的內容到 do_shuffle 函式,並刪除判斷結果的正確性程式碼,留下 q_show 因為我希望 shuffle 結束後可以顯示佇列 ```diff - bool ok = true; - - cnt = current->size; - if (current->size) { - for (struct list_head *cur_l = current->q->next; - cur_l != current->q && --cnt; cur_l = cur_l->next) { - element_t *item, *next_item; - item = list_entry(cur_l, element_t, list); - next_item = list_entry(cur_l->next, element_t, list); - if (strcmp(item->value, next_item->value) < 0) { - report(1, - "ERROR: At least one node violated the ordering rule"); - ok = false; - break; - } - } - } q_show(3); - return ok && !error_check(); + return !error_check(); ``` 在 `queue.h` 中宣告函式原型 ```c int q_shuffle(struct list_head *head); ``` 在 `queue.c` 中實作該函式 ```c int q_shuffle(struct list_head *head) { return 0; } ``` 在 `qtest.c` 中將原本複製過來的函式換為 q_shuffle ```diff - current->size = q_descend(current->q); + q_shuffle(current->q); ``` 此時編譯後就可以使用 shuffle 命令了 ```bash cmd> shuffle Warning: Calling ascend on null queue cmd> new l = [] cmd> shuffle Warning: Calling ascend on empty queue l = [] cmd> ih hello l = [hello] cmd> shuffle Warning: Calling ascend on single node ``` 可以正常執行不會報錯 ```bash cmd> show Current queue ID: 0 l = [dd t h] cmd> shuffle l = [dd t h] ``` --- ##### 在 git commit 時遇到問題 ```bash /usr/bin/sha1sum: WARNING: 1 computed checksum did NOT match [!] You are not allowed to change the header file queue.h or list.h ``` 但是如果把這行刪掉編譯就會出錯 ```bash $ make test CC qtest.o qtest.c: In function ‘do_shuffle’: qtest.c:900:9: error: implicit declaration of function ‘q_shuffle’; did you mean ‘do_shuffle’? [-Werror=implicit-function-declaration] 900 | q_shuffle(current->q); | ^~~~~~~~~ | do_shuffle cc1: all warnings being treated as errors make: *** [Makefile:54: qtest.o] Error 1 ``` <s>參考[這位同學的作業](https://github.com/wilicw/lab0-c/blob/260ecfe7eff6e31ebb0c23d6dbd89cae245f4832/random.h#L26),發現 `qtest.c` 有引入 `random.h` 而裡面有一個 random_shuffle function </s>,跟我的問題不相關 參考 [millaker](https://github.com/millaker/lab0-c),發現它沒有在 `queue.h` 宣告函式原型,而是直接在 `qtest.c` 宣告函式原型,並在 `queue.c` 實作,我還不知道這是怎麼運作的,但是確實是可以編譯成功, shuffle 命令也可以使用 :::danger 注意看作業說明,學員不該變更 `queue.h`。 ::: ###### qtest.c ```diff #include "queue.h" + void q_shuffle(struct list_head *); ``` ###### queue.c ```c void q_shuffle(struct list_head *head) { return; } ``` 使用 `random.c` 中的 randombytes function 產生隨機數 k ```c randombytes((uint8_t *)&rand_val, sizeof(rand_val)); ``` 將 k 個移至佇列尾部 ```c k = get_random_range_1_to_k(size--); list_for_each(li, head) if (--k) break; list_move_tail(li, head); ``` #### 執行結果 ```bash cmd> show Current queue ID: 0 l = [lk sss nnn dd jjj ccc] cmd> shuffle l = [dd lk nnn jjj ccc sss] ``` --- #### shuffle 分析 測試程式發現只有不到10個結果 ```bash python3 ./scripts/shuffle_test.py Expectation: 41666 Observation: {'1234': 0, '1243': 0, '1324': 0, '1342': 0, '1423': 0, '1432': 0, '2134': 2, '2143': 0, '2314': 0, '2341': 0, '2413': 0, '2431': 0, '3124': 0, '3142': 1, '3214': 0, '3241': 0, '3412': 0, '3421': 0, '4123': 0, '4132': 1, '4213': 0, '4231': 0, '4312': 1, '4321': 0} chi square sum: 999974.0001680028 ``` 直接手動洗牌也只能做5次,<s>猜測試洗牌過程中有產生垃圾,被檢測到就不給做了</s> ```bash cmd> ih RAND 5 l = [riurm pjaksxwxh egydoac kctcx yhsrqxjo] cmd> shuffle l = [kctcx egydoac riurm yhsrqxjo pjaksxwxh] cmd> shuffle l = [yhsrqxjo egydoac riurm pjaksxwxh kctcx] cmd> shuffle l = [egydoac riurm pjaksxwxh kctcx yhsrqxjo] cmd> shuffle l = [riurm pjaksxwxh kctcx yhsrqxjo egydoac] cmd> shuffle l = [egydoac pjaksxwxh yhsrqxjo kctcx riurm] Error limit exceeded. Stopping command execution ``` 發現問題出在 do_shuffle 的最後的 error_check ,因為 error_check 回傳的是有沒有錯誤發生 ```diff - return error_check(); + return !error_check(); ``` ![Figure_1](https://hackmd.io/_uploads/SycN9ZMaT.png) ```text Expectation: 41666 Observation: {'1234': 41968, '1243': 41450, '1324': 41543, '1342': 41899, '1423': 41534, '1432': 41809, '2134': 41628, '2143': 41982, '2314': 41785, '2341': 41732, '2413': 41121, '2431': 41815, '3124': 41598, '3142': 41673, '3214': 41664, '3241': 41704, '3412': 41508, '3421': 41849, '4123': 41445, '4132': 41683, '4213': 42126, '4231': 41409, '4312': 41605, '4321': 41470} chi square sum: 26.824845197523164 ``` 所有可能發生的次數差不多 ### Valgrind 分析記憶體問題 會出現很多下面的 Invalid read ```bash ==156179== Invalid read of size 8 ``` 發現是下面這行引起的 ```c memcpy(sp, del_elem->value, bufsize); ``` ### 效能分析 :::danger 使用 perf 時一直跳出下面這個錯誤 ```bash ┌─Error:─────────────────────────────┐ │The cycles:P event is not supported.│ │ │ │ │ │Press any key... │ └────────────────────────────────────┘ ``` 參考 [ perf top error: "The cycles:P event is not supported" on 13th gen / raptor lake ](https://bugs.launchpad.net/ubuntu/+source/linux/+bug/2043320) ,發現太新的 CPU perf 沒辦法正確執行 > 太好了,你找到可以貢獻程式碼到 Linux 核心的機會。 :notes: jserv ::: --- 撰寫 shell script 流程可以參考下圖,因為 qtest 可以用 `-f` 參數指定一組命令作為輸入,所以可以將自己寫的 sort 以及 linux 核心的 list_sort 命令分別寫在不同的 cmd 檔案,用 shell script 使用 perf 測量效能,我寫了多個 cmd 檔案,並且迭代地用 perf 測量效能並產生對應的報告最後得到的結果如下 當隨機產生 1000000 個 node 時,效能並沒有差很多 :::warning 使用清晰的標註方式 ::: - [ ] <s>我的</s> sort ``` Performance counter stats for './qtest -f ./perf_test/test-sort-1000000.cmd' (5 runs): 1,1153,7322 cache-misses # 52.34% of all cache refs ( +- 2.05% ) 7,3736,1882 branches ( +- 0.03% ) 49,1900,3385 instructions # 0.74 insn per cycle ( +- 0.02% ) 3 context-switches ( +- 19.44% ) 2,1309,0573 cache-references ( +- 0.42% ) 66,7067,1349 cycles ( +- 1.00% ) 1.4348 +- 0.0183 seconds time elapsed ( +- 1.28% ) ``` - [ ] list_sort ``` Performance counter stats for './qtest -f ./perf_test/test-linux_sort-1000000.cmd' (5 runs): 9417,4523 cache-misses # 55.08% of all cache refs ( +- 1.44% ) 6,9026,4605 branches ( +- 0.01% ) 47,5297,0306 instructions # 0.79 insn per cycle ( +- 0.01% ) 4 context-switches ( +- 15.81% ) 1,7098,2842 cache-references ( +- 0.14% ) 60,2361,4972 cycles ( +- 0.83% ) 1.2957 +- 0.0135 seconds time elapsed ( +- 1.05% ) ``` ```graphviz digraph G { // 設定圖形的整體方向為從左到右 rankdir=LR; // 定義節點形狀 node [shape=ellipse]; sh [label="sh"]; perf [label="perf"]; qtest [label="qtest"]; // 特別為qtest設定長方形形狀 report [shape=box, label="report_file"]; // 特別為report設定長方形形狀 // 定義上方的cmd節點 cmd1 [shape=box,label="cmd"]; cmd2 [shape=box,label="cmd"]; // 建立節點間的連接 sh -> perf -> qtest -> report; // cmd節點指向qtest cmd1 -> qtest; cmd2 -> qtest; // 確保sh, perf, qtest, 和 report水平排列 { rank=same; sh; perf; qtest; report; } } ``` --- ## ttt ### hlist 程式碼閱讀 [linux/list.h L992](https://github.com/torvalds/linux/blob/480e035fc4c714fb5536e64ab9db04fedc89e910/include/linux/list.h#L932)開始是與 hlist 相關的程式碼 ```c /* * Double linked lists with a single pointer list head. * Mostly useful for hash tables where the two pointer list head is * too wasteful. * You lose the ability to access the tail in O(1). */ ``` :::danger 不理解就說不理解,沒有「不太理解」這回事。 不要將自己的專業成長交給大語言模型。 為何你不做實驗來確認呢? ::: 最一開始就有一段註解,說明在使用雜湊表時,兩個指標的 list head 太過浪費,所以 hlist 的 head 只有一個指標,也就是單鏈,起初我<s>不太理解</s> 一個鏈結串列多一個指標為什麼會被認為是浪費,閱讀[Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable)並且<s>詢問 ChatGPT</s> 後得出結論, hlist 的意思是 hash list ,也就是雜湊表的一個 bucket 對應到一個 hlist ,而雜湊表預期 key 很多,但是每一個 bucket 裡存放的節點很少(才可以在接近常數時間被搜尋到),所以為了一個 bucket 多使用一個指標是"**浪費**"的 h_list 的結構定義在[linux/types.h L197](https://github.com/torvalds/linux/blob/480e035fc4c714fb5536e64ab9db04fedc89e910/include/linux/types.h#L197) ```c struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; }; ``` pprev 使用間接指標讓整個鏈結串列不存在空指標,所以不用考慮特殊情況,讓程式碼更加簡潔 單獨的一個 hlist 鏈結串列長相如下(參考[Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable)) ```graphviz digraph G { rankdir = LR; splines = false; node[shape = "record"] list_head[label = "<m>list_head | <n>first"] node_1[label = "<m>hlist_node_1 | {<p>pprev | <n>next}", group = list]; node_2[label = "<m>hlist_node_2 | {<p>pprev | <n>next}", group = list]; node_3[label = "<m>hlist_node_3 | {<p>pprev | <n>next}", group = list]; NULL[shape = plaintext, label = "NULL", group = list] {rank = "min" list_head} list_head -> node_1 -> node_2 -> node_3[ weight = 10, style = invis ] list_head:n -> node_1:m; node_1:p -> list_head:n; node_1:n -> node_2:m; node_2:p -> node_1:n; node_2:n -> node_3:m; node_3:p -> node_2:n; node_3:n -> NULL; } ``` --- ## 整合 hlist 相關功能 1. 建立 hlist.h 2. 因為我的 [lab0-c](https://github.com/dockyu/lab0-c) 專案沒有 types.h ,所以要手動將 hlist 的資料結構放進 hlist.h [commit](https://github.com/dockyu/lab0-c/commit/74cbaa8d799e3873f708d852acf7161ef2e7b926) 3. 將 [linux/list.h L992](https://github.com/torvalds/linux/blob/480e035fc4c714fb5536e64ab9db04fedc89e910/include/linux/list.h#L932)開始是與 hlist 相關的程式碼搬進 hlist.h [commit](https://github.com/dockyu/lab0-c/commit/81e058ac9f5b632a1f8983e6c7e9f2d9a5ab17bd) --- ##### git hook 不認識 hlist git commit 時因為有提到 hlist 這個字,被 git hook 擋住了 ```bash $ git commit Following files need to be cleaned up: hlist.h Relocate hlist settings to hlist.h [line 1] - Possible misspelled word(s): hlist How to Write a Git Commit Message: https://chris.beams.io/posts/git-commit/ Proceed with commit? [e/n/?] ``` 發現錯誤來自 commit-msg.hook ```bash <lab0-c path>/scripts$ grep -r "Possible misspelled word(s):" commit-msg.hook: add_warning LINE_NUMBER "Possible misspelled word(s): $MISSPELLED_WORDS" ``` 再進一步找到是因為字典檔 `aspell-pws` 沒有 hlist 這個字 ``` ASPELL=$(which aspell) if [ $? -ne 0 ]; then echo "Aspell not installed - unable to check spelling" else LINE_NUMBER=1 MISSPELLED_WORDS=$(echo "$COMMIT_MSG_LINES[LINE_NUMBER]" | $ASPELL --lang=en --list --home-dir=scripts --personal=aspell-pws) if [ -n "$MISSPELLED_WORDS" ]; then add_warning LINE_NUMBER "Possible misspelled word(s): $MISSPELLED_WORDS" fi fi ``` 將 hlist 加入字典檔 ```bash $ echo "hlist" >> aspell-pws ``` :::danger 你本來就不該在標題中書寫 `hlist`,後者只是輔助 hash table 的操作,是 List API 的一部分。 > 但是在 linux kernel 中也有一些 commit 會在標題提及 hlist ,例如 [dentry: switch the lists of children to hlist](https://github.com/torvalds/linux/commit/da549bdd15c295c24b2ee7ffe7ad0f3877fa8a87) ,如果我只是單純的建立一個 hlist.h 檔案,並在裡面定義了 hlist_head hlist_node 型別,該怎麼撰寫標題比較合適? > > 可書寫 "Add header in preparation for hash table",本次作業的重點是強化學員的英語書寫,包含用精簡的詞彙來表達你的動機和舉措。Linux 核心的開發者可能會更動到 List API 的實作,因此提及其內部行為是合理的,但以本系列作業來說,要求學員撰寫「符合情境」且「簡單明確」(例如該避免一堆 "Update queue.c" 的標題) 是當初設定的重要練習。 :notes: jserv ::: --- ### 測試 hlist.h 是否可用 為了確定 hlist.h 不會在編譯時產生錯誤,我建了一個 hlist.c 引入 hlist.h,並嘗試編譯遇到了一些錯誤 ##### 無法識別 bool ```bash hlist.h: At top level: hlist.h:133:15: error: unknown type name ‘bool’ 133 | static inline bool hlist_fake(struct hlist_node *h) | ^~~~ hlist.h:146:15: error: unknown type name ‘bool’ 146 | static inline bool | ^~~~ ``` 這個錯誤是因為無法識別 bool 型別,引入 <stdbool.h> 就可以了 --- ##### `WRITE_ONCE` `READ_ONCE` 未定義 ```bash hlist.h: In function ‘hlist_unhashed_lockless’: hlist.h:89:17: error: implicit declaration of function ‘READ_ONCE’ [-Werror=implicit-function-declaration] 89 | return !READ_ONCE(h->pprev); | ^~~~~~~~~ hlist.h: In function ‘__hlist_del’: hlist.h:106:9: error: implicit declaration of function ‘WRITE_ONCE’ [-Werror=implicit-function-declaration] 106 | WRITE_ONCE(*pprev, next); | ^~~~~~~~~~ ``` 在 Linux Kernel 中,READ_ONCE 和 WRITE_ONCE 是用於防止編譯器優化可能會改變變數訪問順序的巨集,我在 [linux/tools/virtio/linux/compiler.h](https://github.com/torvalds/linux/blob/480e035fc4c714fb5536e64ab9db04fedc89e910/tools/virtio/linux/compiler.h) 中找到這兩個巨集的定義,將這個整個檔案加入 hlist.h 就可以解決這個錯誤 --- ##### `LIST_POISON1` 以及 `LIST_POISON2` 未定義 ```bash hlist.h: In function ‘hlist_del’: hlist.h:132:19: error: ‘LIST_POISON1’ undeclared (first use in this function) 132 | n->next = LIST_POISON1; | ^~~~~~~~~~~~ hlist.h:132:19: note: each undeclared identifier is reported only once for each function it appears in hlist.h:133:20: error: ‘LIST_POISON2’ undeclared (first use in this function) 133 | n->pprev = LIST_POISON2; | ``` <s>LIST_POISON1 以及 LIST_POISON2 是為了避免 dangling pointer 所以要將指標指到一個明現不合法的地址,參考 [linux/scripts/mod/list.h L25](https://github.com/torvalds/linux/blob/480e035fc4c714fb5536e64ab9db04fedc89e910/scripts/mod/list.h#L25) 的作法就可以解決問題 雖然目前專案沒有其他地方用到 LIST_POISON ,但是為了避免之後重複定義,我在 LIST_POISON 的定義前後加上 Include Guards ```c #ifndef HLIST_H #define HLIST_H #define LIST_POISON1 ((void *) 0x100) #define LIST_POISON2 ((void *) 0x122) #endif ``` </s> :::danger `LIST_POISON1` 和 `LIST_POISON2` 的數值很關鍵,這是 Linux 核心特別保留給例外處理的地址,你移植到使用者空間時,需要改用其他手法。 $\to$ [課堂問答](https://hackmd.io/@sysprog/H1QORp-h6) > 改為特定地址 ```c #ifndef LIST_POISONING #define LIST_POISONING #define LIST_POISON1 ((void *) 0x00100100) #define LIST_POISON2 ((void *) 0x00200200) #endif ``` ::: --- zobrist.h 裡有一行 ```c #include "list.h" ``` 因為我已經將 linux 裡的 list.h 拆成 lab0-c/list.h 和 hlist.h 所以我在下面有新增一行 ```diff + #include "hlist.h" ``` 編譯 zobrist.c 時遇到問題 ```bash zobrist.c: In function ‘zobrist_get’: zobrist.c:38:42: error: macro "hlist_for_each_entry" passed 4 arguments, but takes just 3 38 | zobrist_entry_t) | ^ In file included from zobrist.h:7, from zobrist.c:5: hlist.h:303: note: macro "hlist_for_each_entry" defined here 303 | #define hlist_for_each_entry(pos, head, member) \ | zobrist.c:37:5: error: ‘hlist_for_each_entry’ undeclared (first use in this function) 37 | hlist_for_each_entry (entry, &hash_table[hash_key], ht_list, | ^~~~~~~~~~~~~~~~~~~~ zobrist.c:37:5: note: each undeclared identifier is reported only once for each function it appears in zobrist.c:37:25: error: expected ‘;’ before ‘{’ token 37 | hlist_for_each_entry (entry, &hash_table[hash_key], ht_list, | ^ | ; ...... 40 | { | ~ zobrist.c:33:22: error: unused variable ‘entry’ [-Werror=unused-variable] 33 | zobrist_entry_t *entry = NULL; | ^~~~~ zobrist.c:45:1: error: control reaches end of non-void function [-Werror=return-type] 45 | } | ^ cc1: all warnings being treated as errors make: *** [Makefile:60: zobrist.o] Error 1 ``` 查看 ttt/list.h 後修改 `hlist_entry_safe` 和 `hlist_for_each_entry` ```c #ifdef __HAVE_TYPEOF #define hlist_entry_safe(ptr, type, member) \ ({ \ typeof(ptr) ____ptr = (ptr); \ ____ptr ? hlist_entry(____ptr, type, member) : NULL; \ }) #else #define hlist_entry_safe(ptr, type, member) \ (ptr) ? hlist_entry(ptr, type, member) : NULL #endif #ifdef __HAVE_TYPEOF #define hlist_for_each_entry(pos, head, member) \ for (pos = hlist_entry_safe((head)->first, typeof(*(pos)), member); pos; \ pos = hlist_entry_safe((pos)->member.next, typeof(*(pos)), member)) #else #define hlist_for_each_entry(pos, head, member, type) \ for (pos = hlist_entry_safe((head)->first, type, member); pos; \ pos = hlist_entry_safe((pos)->member.next, type, member)) #endif ``` 並且根本不需要 lab0-c/list.h 因為 zobrist.c 只有用到 hlist_head --- 我為 ttt 的功能建立一個 TTT_OBJS 方便管理 ```makefile TTT_OBJS := game.o mt19937-64.o zobrist.o agents/negamax.o OBJS := qtest.o report.o console.o harness.o queue.o list_sort.o \ random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \ shannon_entropy.o \ linenoise.o web.o \ $(TTT_OBJS) ``` 但是在 make 的時候一直<s>出錯</s> 顯示找不到依賴檔 `.agents/negamax.o.d` :::danger 是誰「出錯」?主詞講清楚。 > 已修改 ::: ```bash agents/negamax.c:101:1: fatal error: opening dependency file .agents/negamax.o.d: No such file or directory 101 | } | ^ ``` :::danger directory 的翻譯是「目錄」,而非「資料夾」(folder) > 已修改 ::: 這是因為 Makefile 的規則直接在依賴名稱前面加上 `.` 想要建立一個隱藏檔案,但是我的 negamax.c 是放在 agents <s>資料夾</s> 目錄 裡的,直接加上的方法太過暴力,所以我改成判斷檔案的目錄,並在同一個目錄裡建立依賴檔,當然 clean 的規則也要一起修改 ```diff - deps := $(OBJS:%.o=.%.o.d) + deps := $(OBJS:%.o=$(dir %).$(notdir %).d) %.o: %.c - @mkdir -p .$(DUT_DIR) - $(VECHO) " CC\t$@\n" - $(Q)$(CC) -o $@ $(CFLAGS) -c -MMD -MF .$@.d $< + $(VECHO) " CC\t$@\n" + @mkdir -p $(@D) + $(Q)$(CC) $(CFLAGS) -c $< -o $@ -MMD -MF $(@D)/.$(@F:.o=.d) clean: rm -f $(OBJS) $(deps) *~ qtest /tmp/qtest.* rm -rf .$(DUT_DIR) rm -rf *.dSYM + find . -type f -name ".*.d" -delete (cd traces; rm -f *~) ``` ttt 命令已經可以在 qtest 中使用 ```bash $ ./qtest cmd> ttt 1 | 2 | 3 | 4 | ---+------------- A B C D X> a2 1 | 2 | × 3 | ○ 4 | ---+------------- A B C D ``` --- git commit 時被 git hook 抓到不符合風格的程式碼,還有一個語法錯誤 ```bash $git commit hlist.h:121:13: error: Syntax Error: AST broken, 'h' doesn't have a parent. [internalAstError] return !READ_ONCE(h->pprev); ^ game.c:74:28: style: Parameter 'table' can be declared with const [constParameter] int *available_moves(char *table) ^ mt19937-64.c:83:9: style: The scope of the variable 'i' can be reduced. [variableScope] int i; ^ mt19937-64.c:85:21: style: The scope of the variable 'mag01' can be reduced. [variableScope] static uint64_t mag01[2] = {0ULL, MATRIX_A}; ^ mt19937-64.c:85:21: style: Variable 'mag01' can be declared with const [constVariable] static uint64_t mag01[2] = {0ULL, MATRIX_A}; ^ Fail to pass static analysis. ``` 我不知道為什麼會引起 AST broken ,我會去找出原因 如果將 table 的型別改成 `const char *` 確實可以通過檢查,但是編譯卻會失敗 mt19937-64.c 內的程式碼是我覺得不應該更動 <s>所以我決定先用 `--no-verify` flag 不檢查就提交 ```bash $ git commit --no-verify ``` </s> 改為用 suppress 繞過特定檢查 ```hook --suppress=internalAstError:hlist.h \ --suppress=constParameter:game.c \ --suppress=variableScope:mt19937-64.c \ --suppress=constVariable:mt19937-64.c" ``` --- #### ttt 電腦對電腦 因為目前只有兩個模式 1. 人對電腦 2. 電腦對電腦,我覺得用枚舉來表示能夠增加可讀性 ```c enum game_mode { PVE, EVE }; ``` 並且增加一個 ai_2 , 在 EVE 的情況下用 ai 和 ai_2 對戰 ```diff char turn = 'X'; char ai = 'O'; + char ai_2 = 'X'; ``` ### 定點數 參考 [Fixedpoint versions of ln, pow, exp, sqrt and division](https://gist.github.com/Madsy/1088393) 的寫法 參考 [chmike/fpsqrt](https://github.com/chmike/fpsqrt/blob/master/fpsqrt.c) 的 sqrt 一開始改成用定點數之後 AI 直接變成智障,一直下最前面的格子 嘗試在正常遊戲過程中將每次計算 uct 時的 n_total n_visits score 紀錄在 log 檔裡,用這些資料測試定點數版本的 uct 和原版差多少,然後在慢慢修正錯誤的地方 發現第一個錯誤是 sqrt 的部份,接受整數型別的參數但是我卻勿把定點數傳入造成最後結果差了 190 倍 現在的 uct 看起來比較正常,但還是有明顯誤差,**甚至有些大小關係跟浮點數版本的相反** ###### 擷取其中幾筆 ```bash n_total: 308, n_visits: 11, score: 5.000000000000000 fp n_total: 20185088, fp n_visits: 720896, fp score: 327680 UCT score: 1.475249292187409 fp UCT score: 1.996109008789062 n_total: 308, n_visits: 21, score: 15.000000000000000 fp n_total: 20185088, fp n_visits: 1376256, fp score: 983040 UCT score: 1.453016916317026 fp UCT score: 1.725845336914062 n_total: 308, n_visits: 13, score: 7.000000000000000 fp n_total: 20185088, fp n_visits: 851968, fp score: 458752 UCT score: 1.477372510154364 fp UCT score: 1.918090820312500 ``` 找到第二個錯誤,原本的 uct 計算中使用到 EXPLORATION_FACTOR ,這是一個浮點數 $\sqrt[]{2}$ ,我直接將它傳入定點數的 sqrt function 但是它接受的是定點數所以會出錯 我將 uct 的部分從專案抽離,建立一個專門用來測試定點數計算的專案 [fixed-point-test](https://github.com/dockyu/fixed-point-test) ,下面的數據是在此 [commit](https://github.com/dockyu/fixed-point-test/commit/1f374ce80caee131151c13eb208213acb0c79269) 測試的結果,和浮點數運算結果的差距已經非常小了,但是目前我的定點樹相關的程式碼極度雜亂,所以下一步就是要修改程式碼,等到程式碼變整齊之後再整合進 ttt ```bash n_total: 308, n_visits: 13, score: 7.000000000000000 fp n_total: 20185088, fp n_visits: 851968, fp score: 458752 score / n_visits Float: 0.538461538461538 Fixed Point: 0.538452148437500 log(n_total) Float: 5.73010 Fixed Point: 5.73010 log(n_total) / n_visits Float: 0.44078 Fixed Point: 0.44077 sqrt(log(n_total) / n_visits) Float: 0.66391 Fixed Point: 0.66389 EXPLORATION_FACTOR Float: 1.41421 Fixed Point: 1.41420 EXPLORATION_FACTOR * sqrt(log(n_total) / n_visits) Float: 0.93891 Fixed Point: 0.93887 score / n_visits + EXPLORATION_FACTOR * sqrt(log(n_total) / n_visits) Float: 1.47737 Fixed Point: 1.47733 ``` 此 [commit](https://github.com/dockyu/lab0-c/commit/3f51788aea7d00092cd66676e795a4158a7dc332) 中,井字遊戲的 AI 已經可以用定點數取代浮點數運算,並且他的行為已經"不笨",可以阻止對手獲勝,並在我故意放水時讓自己獲勝 :::info 我發現兩個 AI 對戰一定會下成這樣,難道是所謂的必勝法? ```bash A B C D 1 | 2 | × 3 | ○ × 4 | × ○ × ○ ---+------------- A B C D X won! Moves: A4 -> D4 -> A2 -> A3 -> C4 -> B4 -> B3 ``` ::: ### git rebase ```bash $ git remote add upstream https://github.com/sysprog21/lab0-c.git $ git fetch upstream $ git merge upstream/master Auto-merging scripts/pre-commit.hook CONFLICT (content): Merge conflict in scripts/pre-commit.hook Automatic merge failed; fix conflicts and then commit the result. ``` ###### pre-commit.hook ``` <<<<<<< HEAD --suppress=internalAstError:hlist.h \ --suppress=constParameter:game.c \ --suppress=nullPointer:agents/mcts.c \ --suppress=variableScope:agents/mcts.c \ --suppress=variableScope:mt19937-64.c \ --suppress=constVariable:mt19937-64.c" ======= --suppress=checkLevelNormal:log2_lshift16.h" >>>>>>> upstream/master ``` 修改為 ``` --suppress=constParameterPointer:console.c \ --suppress=internalAstError:hlist.h \ --suppress=constParameter:game.c \ --suppress=nullPointer:agents/mcts.c \ --suppress=variableScope:agents/mcts.c \ --suppress=variableScope:mt19937-64.c \ --suppress=constVariable:mt19937-64.c \ --suppress=checkLevelNormal:log2_lshift16.h" CPPCHECK_OPTS="-I. --enable=all --error-exitcode=1 --force $CPPCHECK_suppresses $CPPCHECK_unmatched ." ``` ### 使用 coroutine 因為原本的 lab0 太多東西了,我將最新版本的 linenoise 以及 lab0 的命令界面實做的部份抽出,做了一個較輕量的命令行程式 [cli](https://github.com/dockyu/cli) (刪除檢查的部份)用來測試 coroutine 的功能 閱讀 [coro.c](https://github.com/sysprog21/concurrent-programs/tree/master/coro) 之後我總結出要用 coroutine 的方式撰寫程式需要做到以下幾點 1. 一個 task 對應到一個 function 2. function 開頭是設定任務的 context,並回到 scheduler 3. scheduler 負責初始化 task ,也就是執行所有 task 的 function 4. scheduler 初始化完成後就可以開始 coroutine 5. task switch 時 task 會暫時從排程中移出,直到做完一次再決定要不要加入排程(由 task 自己判斷) 我將 ttt 劃分為多個任務 1. ai1 下一次 ai 2. 判斷勝利1 (輸出遊戲過程) 3. ai2 下一次 4. 判斷勝利2 (輸出遊戲過程) 5. 畫棋盤狀態 6. 監聽鍵盤事件 不知道為什麼出錯了, task->name 是錯的,而且會一直變,最後變成 EVE ,但是 EVE 甚至不是一個字串 ```c enum game_mode { PVE, EVE }; ``` ```bash cmd> ttt EVE 5 Initial task1 Initial task2 Initial task3 Initial task4 Initial task5 Initial task6 ai_1_task resume, task name: H��]�] check_win_task resume, task name: EVE ai_2_task resume, task name: EVE ``` 如果有 printf 就可以正常執行,沒有 printf 就會出現下面的錯誤 ```c if (!list_empty(&tasklist)) { printf("task_switch 1\n"); struct task *t = list_first_entry(&tasklist, struct task, list); // printf("taskname=%s\n", t->task_name); list_del(&t->list); cur_task = t; longjmp(t->env, 1); } ``` ```bash *** longjmp causes uninitialized stack frame ***: terminated Aborted (core dumped) ``` 考關閉編譯器優化解決 ```diff - CFLAGS = -O1 -g -Wall -I. + CFLAGS = -O0 -g -Wall -I. ``` 第一次 coroutine ttt 會成功,但是當執行完一次 coro_ttt 之後,在執行第二次會 Segmentation fault commit : [d8209d](https://github.com/dockyu/cli/commit/d8209d1aa9f1c91d8e3571e30cf3b2daabd637c3) ```bash cmd> coro_ttt Segmentation fault (core dumped) ``` 找到問題,以下程式碼的 i 若是超出 tasks 的範圍會出現 segmentation fault ,起因是當我執行下一次程式時 i 不會被歸零 ```c tasks[i++](&arg); ``` ```diff +static int initial_task_index; void schedule(void) { - static int i; - + int i = initial_task_index; setjmp(sched); ``` #### 監聽鍵盤事件 ###### 修改終端機設定 1. c_lflag 中的 ECHO (預設是啟用)控制鍵盤的輸入要不要顯示在終端機,我希望 ctrl-P 不要顯示,所以要禁用 2. c_lflag 中的 ICANON (預設是啟用)稱為 canonical mode ,表示緩衝區要一行一行 (有換行)才被傳給程式,我希望按下 ctrl-P 馬上就有反應,不希望還要按 Enter ,所以要禁用 3. c_lflag 中的 IXON (預設是啟用)對 ctrl-Q 做暫停以及其他組合鍵,不會被傳給程式,因為我需要對 ctrl-P ctrl-Q 做自定義處理一定要傳給程式,所以要禁用 ```c raw.c_lflag &= ~(ECHO | ICANON); raw.c_iflag &= ~(IXON); ``` 設定 Non-Blocking Mode 在讀取時不會因為沒有數據可以讀而卡在那邊等待,當我沒有按任何組合鍵時, keyboard_listen 不應該卡住而是直接 task_switch ```c fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK); ``` ```c /* 目前設定 */ fcntl(STDIN_FILENO, F_GETFL) /* 目前設定 + Non-Blocking */ fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK /* 儲存設定 x */ fcntl(STDIN_FILENO, F_SETFL, <setting>); ```