contributed by < vestata
>
Vincent550102
list_for_each
,以 q_swap
為例。直觀的用for loop去travers鏈結串列,直到遇到 head 為止
建議更改為
直觀地用迴圈走訪整個環狀鏈結串列,直到遇到頭部為止
vestata
weihsinyeh
q_sort
中 graphviz 指標的線要整理,優點讓讀者清楚閱讀,好溝通。git rebase
之後重新記錄。已更正。vestata
無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利)
收到
vestata
首先先明白整個 queue 的結構與其鏈結串列的結構。所依先著手於詳閱 list.h
裡面的內容,還有 queue.h
裡面的結構。
發現 queue.h
裡面有關於每一個函式的詳細介紹。按照敘述完成程式。
避免過多的中英文混用,已有明確翻譯詞彙者,例如「鏈結串列」(linked list) 和「佇列」(queue),就使用該中文詞彙,英文則留給變數名稱、人名,或者缺乏通用翻譯詞彙的場景。
q_new
配置記憶體並將其 prev 跟 next 指向自己,可以直接使用 list.h
裡面的 INIT_LIST_HEAD
直接完成。
q_free
將 queue 所佔用空間釋放掉,我閱讀文件看到 list_for_each_entry_safe
這是我第一次看到這種 macro
(才疏學淺),研究了一下發現是跟 for loop
一樣的用法,並在 traverse 鏈結串列式時,有 safe
這個指標,確保不會再 current node 被 remove 時不會出錯。這邊使用 list_for_each_entry_safe
來完成。並直接使用 queue.h
中的 q_release_element
來釋放串列中的記憶體。
最後再把 head pointer
釋放掉。
改進你的漢語表達。
make test
18/100
q_insert_head
input為 char
的陣列 s
所以要
q_insert_head_tail
看文件內容說明,他需要將 char s
的內容複製到 element->value
中
更新完之後 q_insert_head
加上 q_insert_head
合併後 make test
為36/100
在 git commit
時發生問題,說明中提到 strcpy
不會確認 buffer 長度,容易對記憶體受傷害。
留意術語的使用:
改為使用 strndup
以下是期說明: strndup
會根據提供的字符串 s 的長度分配足夠的記憶體空間(包括空字符 字串結尾字元),並返回一個指向新分配 配置記憶體的指標。如果記憶體分配失敗, strndup
會返回 NULL
我在這邊原本是使用 tmp->value = strndup(s, strlen(s))
,後來執行程式一直報錯 Attempted to free unallocated block.
花了很多時間 debug ,最後是參照 vax-r的程式,改為使用 strdup
程式便正常執行了!
為何 strndup
會報錯而 strdup
得原因待補。
q_remove_head
選擇直接調用 list.h
裡面的 list_empty
跟 list_del
發現 make test
效果超差…
回去看文件發現原來要把東西複製進去 *sp
裡面,重新修正程式碼。因為有相對應的緩衝區容量,所以直接使用 strncpy
這次error是 Attempted to free unallocated block.
,但是我這一段程式碼沒有free任何東西,目前還不知道問題出在哪,先繼續往下作答。(此問題為 q_insert_tail
的問題,已修正)
到現在 make test
的機制還有點沒有用明白,但是最優先需要解決 Attempted to free unallocated block.
的問題,應該從 q_free
下手:
q_size
使用直觀的 list_for_each_safe
對該queue進行 traverse
,並宣告一 count
計數。
q_delete_mid
使用的是 jserv
上課所提過的 pointer to pointer
技巧去實作,並翻閱 2095. Delete the Middle Node of a Linked List Solved Medium Topics Companies
使用一個 element_t *tmp
去指向 list_entry
所找到的記憶體位置。
花了一點時間將它實踐出來。相對於使用 pointer
節省了一個指標的使用量。
使用 q_release_element
q_delete_dup
Implemented q_delete_dup using pointer-to-pointer technique, incorporating a jump pointer for skipping over duplicate entries. Node comparison is facilitated by a cmp function to assess values within nodes. Functionality is verified in qtests; however, a segmentation fault occurs during make test.
接下來針對 segmentation fault 去偵錯。
發現在 [1 1 1 1] 時會出現 segmentation fault.
應該直接叫 list api ,鏈結沒有處理好。
目前還是卡住,先跳過
觀摩 nosba0957 同學的實作手法。
我在實作時遇到的問題是當有遇到 duplicate 時,在刪除第一個 duplicate 遇到很多困難,這迫使我添加了多個錯誤處理條件,結果導致了段錯誤(segmentation fault),同學使用一個 bool dup 很好的解決我的問題。故我用原本 list 的結構實作相同的手法。
q_swap
剛好 24. Swap Nodes in Pairs
是以前寫過的題目
直觀地用迴圈走訪整個環狀鏈結串列,直到遇到頭部為止。
剩下就是將 one
, two
前後的節點交錯接上,便完成 swap 的動作。
q_reverse
回去翻閱 list.h
發現 list_for_each_safe
裡面的 node , safe 的宣告與 q_reverse
所要求的 traverse 形式類似,所以直接調用 list_for_each_safe
這個函式。
所以在這邊直接將 tmp 的 next 指向 prev , tmp 的 prev 指向 next (在這邊是 safe )
loop 執行完, tmp 會指到 head 的位置。
完成 head 鏈結到第一個 node ,便完成此函式
q_reverse_k
按照文件先排除 queue 為 NULL 或是空,還有只有單節點的情況
TODO: 運用 List API,撰寫更精簡的程式碼。
再回去詳閱 list.h 的內容,一開始遇到的問題是如何去從原本鏈結串列去切割出要做 reverse 的段落,原本是使用 pointer to pointer 的方式去找起始點,並往後找切割之 tail ,之後在看 2023 年 Linux 核心設計/實作課程第一次作業檢討 ,看到同學的方法 yanjiew1
實在很好,所以直接放棄原本的作法…
清楚標註你參照的 GitHub 帳號名稱
收到,已修正
vestata
其中修補我原本的程式的地方有三個主要重點:
1.
當時因為在會改變串列順序的遍歷過程中出錯,所以直接使用 list_for_each_safe
,並透過 if
語句加上 continue
,來彌補我原先所寫的 for 迴圈的不足。這個技巧值得深入了解並記住。
2.
使用 LIST_HEAD 直接初始節點,之後對它直接做 list_cut_position 剛好直接調用到 cut, tmp 一開始我認為 tmp 因為 reverse 會換到該 reverse 的起點,然回頭看 list_for_each_safe 其中他的 node = safe, safe = node->next
就會可以確保 tmp 在反轉串列的下一個繼續 traverse。是一開始讀 list.h 沒有想到的點,銘記在心,獲益良多。
之後變進行 q_reverse
跟 list_splice
,最後再使用 cut = safe->prev;
更新下一個反轉串列的起始位置。
段落中的程式碼標示應使用 1 個 backtick,而非 3 個
收到,已更正
vestata
沒做到的事,不要輕易宣稱,繼續改!
q_sort
本處你指涉者是「測試項目」(test item),而非廣義的「資料」,工程人員說話要精準。
收到
vestata
想要先觀察 make test
的測資 測試項目,實作一個 bubble sort
一開始這樣做的時候沒有考慮到用 for loop 他在 swap 完兩個節點之後他的順序會亂掉,所以這邊不能使用 for loop,會報錯 Segmentation fault occurred
使用 git diff
或 diff -u
命令來產生程式碼之間的變異列表,不要憑感覺填入,注意位移量。
改為使用 pointer to pointer 並在觸發 swap 時加上 current = current->prev ;維持順利順序
最後還是出現 error
發現在邏輯上實作 bubble sort 出錯,但是目前無力更改,應該依照老師引導使用lib/list_sort.c
的
文字訊息請避免用圖片來表示,否則不好搜尋和分類
收到,已更正
vestata
以下直接在 lab0 中實作 lib/list_sort.c
priv
移除@priv: private data, opaque to list_sort(), passed to @cmp
priv
故先將它移除list_sort.h
擷取以下程式碼__attribute__((nonnull(1,2)))
: 這一段為函式第一,第二個傳入參數不能為 NULL ,可以幫助編譯器在編譯階段檢查參數使否被正確傳入。
3. 從 /include/linux/compiler.h
擷取 likely 的巨集宣告
4. 撰寫 compare function : 依照文件中的意思是比較兩個節點中的字串去做比較,所以使用 strcmp
q_ascend
由於限定函式回傳 int ,所以只能 in-place 完成的個操作。
繼續完成這部分,為了想壓在 所以弄巧成拙,一直出現 segmentation fault ,先用兩個 while loop 使用 的時間複雜度實作出來!
參考了 YouTube 頻道 Programming Pathshala 提出的算法,我採用了一個新的策略來處理升序(ascend)和降序(descend)的問題。此方法的核心思想是先對 queue 進行反轉,這樣做可以讓我們更容易地按照升序或降序的要求來刪除不符合條件的節點。
以下是以值為整數(integer)進行降序排序的示例步驟:
q_merge
閱讀敘述,主要功能是將多個已排序的隊列合併成一個單一的隊列,並可以根據參數決定是按升序還是降序排列。
因為要求將所有隊列合併到第一個隊列中,所以我採取的策略是先將所有隊列加入到第一個隊列裡,然後直接使用 q_sort 進行排序,這樣可以省略合併過程中較為複雜的部分。雖然這種做法與傳統的合併演算法不完全對應,但它有效地簡化了程式撰寫的複雜度。
在合併過程中,若是要記得初始化加入的串列不然會出現 segmentation fault.
shuffle
要新增新的指令 'shuffle' ,須完成以下步驟:
q_shuffle
於 queue.q
do_shuffle
ADD_COMMAND(shuffle, "Do Fisher-Yates shuffle", "");
更改之後使用 lab0(D) 中的 python 腳本命名為 test_shuffle.py 的到以下結果:
在結果上有明顯的錯誤。使用 ./qtest
也遇到 shuffle 結果一直重複的問題。
在 git commit 的過程中遇到不能修改 queue.h 的 checksum 限制,所以需要將在 qtest.c, queue.h 所修改的地方刪掉,這在測試的時候會相對複雜。
問題依舊。測試在 shuffle 多次便會遇到 ERROR: Time limit exceeded. Either you are in an infinite loop, or your code is too inefficient
的錯誤。
再想辦法改進。
再次回頭檢查程式:針對 swap()
函式進行修改,發現在之前撰寫的 swap 函式有誤,以下已更正。
這邊發現 --random
的誤用,已更正
最後還是維持 Fisher–Yates shuffle wiki 中提到的 modern method 去完成實作。
以下為 lab0(D) 提供的 python 腳本所執行的結果。
commit dc22cf3
lib/list_sort.c
解釋為何對 cache 不友善。
專注在你的洞見。
由 檢查count+1
後是否為 結尾
在這邊 cmp(priv, a, b) <= 0
a<=b
這邊 else
a>b
這便可以直接在 lab0 做修改
這在一段提到 priv
使以 cmp
的參數傳入,如果在 lab0
的部份是不用用到的
cmp
可以理解為 compare function 。
所以在這一段,以 lab0
的角度,需要的 input 只有 head
list 指向 head 的第一個節點, pending 指向 NULL,先檢查是否沒有任何元素或只有一個元素
jserv
以上程式作到的,實作 q_sort 會遇到:
以下著重分析這段程式碼:
這一段 bitwise 操作可以理解為它根據 count
後面有多少0,讓 tail 要往 next 移多少。
往下:
likely()
是個巨集 macro ,用於給編譯器一個提示:包裹在 likely() 內的條件表達式很可能為真。這個巨集是用於優化目的,幫助編譯器生成更高效的機器碼,特別是在該條件表達式頻繁評估為真的情況下。
查閱 GCC 手冊和計算機組織/結構的教科書,從 branch prediction 的角度去闡述。
但是關於 likely()
實作內容待補。
所以到了非,便進行合併。
這邊有一點點怪,要補一下
用自己的腦袋過一遍。
struct list_head *list = head->next
pending
= NULL進入while loop
struct list_head **tail = &pending;
所以 tail 也指向 null ,沒有 merge ,進到 list->prev = pending
所以 list->prev
指向 NULL
pending = list;
,list = list->next;
: pending 節點數量 + 1, list 節點數量 - 1
pending->next = NULL;
:斷開 pending 跟 list 中間的 link
一樣的操作,所以直接貼上結果
會進行 merge
struct list_head *a = *tail, *b = a->prev;
呼叫 merge
這邊呼叫merge就看不懂了…先直接實作看看
收到
vestata
摘錄關鍵訊息即可。
收到
vestata
test 1~16 沒有出現記憶體錯誤,容易出錯的地方是 test 17 ,在本地電腦跑有時後會過有時候不會過,檢查一下哪裡出錯。
先推斷是 strdup ,嘗試改為 strncpy 以縮小複製的 buffsize.
在 github 的 make test 依然過不了。 95/100
執行完 make valgrind
除了 test trace-17-complexity 這邊有出現錯誤 (要修正 dudect 已修正 dudect 的錯誤),其他的地方沒有發現錯誤。
選用 traces/trace-eg.cmd
輸入以下命令以生成 massif 檔案:
會生成 massif.out.<num>
的檔案
使用 massif-visualizer
(需要下載)查看檔案:
注意用語,以你的認知書寫你在這篇論文得到的啟發。
詞彙對照表
好的收到,馬上更正
vestata
這篇論文從時間攻擊去做介紹,利用程式執行時間的差異來提取密碼等重要資料。所以介紹 dudect 的個方法已檢測程式是否在常數時間(constant time)完成。
在 Approuch 解釋其實作方法:
Welch’s t-test
與 cropping 完的結果去比對時間分佈是否相等。lab0
中的 dudect在 oreparaz/dudect 的程式碼具備 percentile 的處理,但在 lab0-c 則無。
首先先去檢測在 oreparaz / dudect 中 percentile 負責處理什麼東西。
發現在 dudect.h
大量使用到 percentile
,用於實現測量數據的 cropping,。裁剪過程的目的是移除那些異常大的計時值,這些可能由於操作系統中斷或其他非程序本身的活動造成的。這樣的測量值可能會扭曲統計分析的結果。
在 lab0-c/dudect/
中分別找到 constant.c
, fixture.c
, ttest.c
分別對應到上面 approach 提到的三個實作方法。主要目標為將 percentile 加入 fixture.c
之中。
將 percentile 加入 lab0-c/dudect/fixture.c
:
dudect.h
中它將 dutect_ctx_t
中的資料打包,而在 lab0-c
是分開撰寫的,所以先針對每個對應的變數添入更新的cmp
, percentile
, prepare_percentiles
update_statistics
max_test()
以獲取差距最大的樣本doit()
相對於 oreparaz / dudect
中的 dudect_main
,這邊填入相對應的參數,並會放棄第一批資料作為暖身。commit 3fcd442
本次實驗的場合設定在 2024q1 第 1 週測驗題 所提供的 timsort 程式碼基礎上。考慮到這邊有已撰寫好的 compare 程式,該函數已實現比較次數的記錄,且可以使用已寫好的 designated initializers 佈置實驗比較。且由於我在 lab0-c
的實作方法直接採用 list_sort
所以只針對 timsort, list_sort 做比較。
list_sort
整合進去在 perf list
中選擇查看要比較的項目,選出 cache-misses, cpu-cycles, branch-misses, task-clock 作為比較項目。
接下來使用 perf stat --repeat=10 -e cache-misses,cpu-cycles,branch-misses,task-clock ./main timsort
對其做比較 (先放上來紀錄,等等會整理)
以下比較省略詳細資訊,皆已 10 runs , 每 run 1000000 筆資料作為比較。
commit 213ab43
- | timsort | list_sort |
---|---|---|
cache-misses | 331,9122 | 303,7609 |
cpu-cycles | 21,7076,8570 | 19,3518,8604 |
branch-misses | 1924,3092 | 1933,1920 |
task-clock | 421.86 msec | 378.49 msec |
comparisons(avg.) | 20535167 | 18687602 |
在這裡我們發現,在隨機數據條件下,timsort
的排序效能低於 list_sort
。timsort
的適用場景更偏向於大部分數據已經排序好的情形。另外,在 2024q1 第 1 週測驗題 提供的 timsort
實現中,缺少了二分插入排序(binary insertion sort)和 galloping mode,這些遺漏是造成性能不佳的原因。
- | timsort | list_sort |
---|---|---|
cache-misses | 300,6993 | 264,6626 |
cpu-cycles | 5,9107,1787 | 7,6476,5017 |
branch-misses | 434,3442 | 435,1108 |
task-clock | 115.91 msec | 149.41 msec |
comparisons(avg.) | 6224795 | 12120568 |
在部分有序(partial_ordered)的實驗中,可以觀察到 timsort 在已經排序好的數據集上具有明顯的優勢,尤其在比較次數上,timsort 相較於 list_sort 只需約一半的比較量。
comparisons 與其他性能指標(如 cache-misses、cpu-cycles、branch-misses、task-clock)的主要差異在於,它直接反映了排序算法在運行過程中進行元素比較的次數,而不是測量硬體層面或執行時間的指標。反映了算法本身的特性,獨立於執行它的硬件環境。
- | timsort | list_sort |
---|---|---|
cache-misses | 282,4802 | 265,1732 |
cpu-cycles | 6,3614,9481 | 7,0134,8841 |
branch-misses | 95,3924 | 119,3419 |
task-clock | 125.14 msec | 138.07 msec |
comparisons(avg.) | 16916065 | 18079052 |
在 sparse 資料的實驗中,我們可以看到,由於每 20 筆數據中就有一筆受到隨機數的影響,sparse 資料與部分有序(partial_ordered)資料相比,在沒有特別設計 minrun 的情況下,timsort
的排序優勢不明顯。因此,在比較 timsort
和 list_sort
的結果時,兩者表現相近。在比較效能、硬體層面以及執行時間的指標上,timsort
略優於 list_sort
。
commit b01df5c
TODO
實驗使用的是 2024q1 第 1 週測驗題 所提供的 timsort 版本,尚有缺陷。需再引入以下:
binary insertion sort, galloping mode
才可以比較出真實 timsort 與 list_sort 的效能差距
擷取自 2024q1 第 2 週測驗題 中 leetcode 105, leetcode 146 與 hlist 相關的程式碼抽出,成為 hlist.h
commit 02ddb0e
回去觀摩 lab0-b 裡面新增 hello 的步驟,在這邊使用相同的方式將 ttt 加入 qtest 之中。
對 qtest 程式進行了修改,新增了一個名為 'ttt' 的命令。這項改動涉及將 jserv/ttt 的 main.c 文件的內容整合到 qtest 的 do_ttt() 函數中。這樣的修改使得 Tic-Tac-Toe 遊戲能夠在 qtest 環境中執行。
commit 86fd20f
在 jserv/ttt
專案的 main.c 中,有三種算法可供選擇編譯。採用 MCTS 算法,因此將其他算法相關的程式先行刪除。接下來的任務是修改 lab0-c/makefile
以成功編譯 mcts.c 和 mcts.h。由於之前步驟中移除了其他算法,相關的編譯標誌(flag)也從 jserv/ttt/makefile
中刪除了。
為了確保 git commit 能通過 cppcheck 檢查,對 mcts.c 進行了必要的修正。
commit d0a7104
TODO
對 makefile 理解不足,在這邊依照 dudect 的步驟撰寫。建立一個 .agents 目錄
待理解
首先著手修正 calculate_win_value
、simulate
、和 backpropagate
這些函式,並且修改 uct_score
和 exploration_factor
,將其中的 double
類型運算更改為 int
類型。定點數運算中的 SCALE_FACTOR
暫定設為 1000。
TODO
SCALE_FACTOR
應該定義為多少。/trace
目錄,再進行效能比較測試。fixed_power_int
函式來進行定點數運算的理解和實現。commit ff0e896
ttt
命令在 qtest 引入新的 option
commit 75fa689
ttt cc
命令,會出現步法相同的情況,所以在 mcts.c
新增 time.h
作為隨機種子。commit c0ca984
zobrist.c
無法通過 cppcheck。最後發現問題所在是 pre-commit.hook
對它進行修正,使 zobrist.c 可以通過 nullpointer 的 cppcheck。
對於 cppcheck 的不熟悉,導致花大量時間在這邊修正。
commit b8193d5
研究 concurrent-programs/coro/ 中的程式碼搭配 coroutine 實作體驗:協同式多工 中的介紹,其中有提到 coroutine
也可以藉由 signal
等相關操作實作 preemptive
,一般來說 stackful
可自行切換 subroutine。
struct task
中:
在 schedule
會呼叫 setjmp(sched)
schedule
setjmp(sched)
task_switch()
裡面的 longjmp(sched, 1)
,讓 schedule()
可以繼續將新任務加進排程。schedule()
設定的參數決定迴圈次數,
longjmp(sched, 1)
這個 concurrent-programs/coro/ 主要想展現的是在不同任務之間進行協作式切換,實現了簡單的協程調度。每個任務在運行過程中可以多次主動讓出控制權,並在後續被重新調度時從上次暫停的地方繼續執行。
我認為這邊可以實作 作業要求 中的要在螢幕顯示當下的時間 (含秒數),並持續更新。
思路:
先將 MCTS 與 negamax 兩種算法分別定義為 task0 與 task1 ,分別在 task0,1 加入時間計時。我們使用 coroutine 輪轉兩個 task 進行下棋
自己實作了一遍,最後決定參考他人的作法,由於我將 ttt 獨立出 ttt.c 與 ttt.h 在很多參數上的呼叫我遇到問題,已我參考了 HenryChaing 的 commit 與我的實作較為類似我從中去修改,將其內容移到我的專案中的 ttt.c,ttt.h,並定義一個 main_ttt 以方便呼叫回到 qtest.c 之中。我放在以下 commit 5156926
但是目前的實作會遇到一個問題,我將判定勝負的條件定義於 task 之中,但是目前進入 coroutine 之後無法清除棋盤。
我也修改了 pre-commit-hook 因為 ttt 專案的名字一直被 aspell 判定為 misspell word。
發現在 task0, task1 對於終止條件沒有寫好,對於他有進行修正,並在 task 中新增清空鍵盤的功能,以電腦對決電腦保持每一輪結束都會清空鍵盤。
使用搶佔式多工,研究 concurrent-programs/preempt_sched/ 中的程式碼搭配 coroutine 實作體驗:協同式多工 中的介紹
主要透過 SIGALRM 信號來模擬作業系統的計時器中斷。它創建了三個排序任務,每個任務都可以被計時器信號搶佔,從而實現任務之間的切換。
計畫
新增 task3, 在 coroutine 兩個電腦輪轉時,使用鍵盤事件將 task3 搶佔進 scheduler 之中(照上述使用 signal)完成要求。
意味著使用者可連續觀看多場「電腦 vs. 電腦」的對弈,當按下 Ctrl-P 時暫停或回復棋盤畫面更新 (但運算仍持續)
commit 61a7d41
在這邊我先將 mazu-editor
之中鍵盤事件的函式移植到 ttt
之中,針對 ctrl q
, ctrl p
新增鍵盤事件,目前使用的方法將 readkey()
直接放在 task 之中,可以成功接到 ctrl key
並按下 enter
的按鍵事件,但是遇到一個延伸問題。
這會導致程式的執行會在這裡停止,等待用戶輸入。由於你的程式設計是要在兩個協程間輪流執行,所以如果其中一個協程在 read_key()
中阻塞,另一個協程就無法被調度執行,因為原始的協程尚未完成執行且仍持有控制權。故需要其他方法解決這個問題。
先閱讀 preempt_sched 中怎麼使用 signal 在 scheduler 搶佔,還有 trampoline 的實作方法。
由於在實作初期尚未理解如何撰寫合格的 git commit message ,故在作業說明要用 git rebase 去修正過去的 git commit message。
根據 修改歷史訊息 學習怎麼 git rebase
git rebase -i xxxxxxx
會進入 vim 編輯器的界面pick
換成 reword
或是 r
。已修正過去 git commit message 的缺失。