contributed by < yy214123
>
Andrushika
此處在 malloc 新的 element_t
後,直接指定 new_elem->value = s
:
然而,這樣的方法很容易造成 dangling pointer:因為 s
是由傳進函式的 char*
,身為函式的實作者,沒辦法保證函式呼叫者之後會不會對 s
做其他處置。如果函式呼叫者將 s
的記憶體空間釋放(或做其他修改),後續就會發生 use after free 或存取的值和預想中不同的問題。
比較好的作法,是將 s
先 copy 一份後放進 value
:
你先前使用 q_reverse
來簡化操作,後來發現錯誤。先假設這樣的實作是正確的,也會存在效能的問題。q_reverse
的時間複雜度是 ,所以 q_descend
會比 q_ascend
多一次對鏈結串列的走訪。雖然兩者在 big-O notation 下都是 ,但 前方的常數會有所差異。
對,這的確是我當下沒考量到的細節,多執行一次
q_reverse
的確會造成額外的效能損失。 yy214123
可以將各標題層級、內文的結構分清楚一些,目前「研讀〈Dude, is my code constant time?〉」被和 queue.c
相關說明放在一起,但兩者似乎沒有太大相關;「處理 commit message 遺失 Change-Id 的錯誤」也可以獨立寫成一個標題,方便查找。
感謝你的建議,我已經對排版進行了修改。關於「研讀〈Dude, is my code constant time?〉」,我將其獨立為新的標題。而對於「處理 commit message 遺失 Change-Id 的錯誤」,相關的內容我已移至額外問題紀錄中進行描述。若有其他排版建議也歡迎提出,我希望能使整體筆記排版更精美。 yy214123
這是我第二次選修這門課,因此去年已經有相同的 repository。我利用 GitHub 網站提供的 import repository
功能,將原先的 repository 重新命名為 lab0-c_2024,接著,我使用 GitHub 提供的 sync fork
功能來初始化 lab0-c。
課程教材會反覆出現,你不用特別提,除非你要指出其中關鍵描述。
收到。
在實作 queue.c
中的任意函式之前,應先仔細閱讀對應的標頭檔 queue.h,理解其中對各函式的說明。此外,可輔以 The Linux Kernel API - List Management Functions 中的相關巨集來實現功能。
依據作業指示,在執行 git commit
前,須先執行 clang-format -i queue.c
,來排除程式碼排版和風格檢查的錯誤。
在共筆紀錄中須附上對應的 commit message 連結,輸入下方命令可以節省前往 GitHub 複製的時間:
commit 55ed393
既然是作業描述,就沒必要特別放超連結。你該探討作業要求背後的因應措施。
收到。
上面程式碼沒有討論的必要,不用列出,你該回顧自己的 commit message 是否有長進。
回顧去年的 commit 97f2a53,當時的 commit message 雖然詳細,但過於冗長,包含了不必要的細節(如 list_for_each 的具體行為),此外,標題使用了 "Refactor",這個單字適用於優化程式架構或提高程式可讀性,而
q_size
原先總是回傳-1
,這顯然是錯誤的實作,故使用 "Fix" 更為恰當。我認為今年的 commit message 比較好,準確描述了問題與修正內容,使讀者能快速理解。
commit 0a99902
首先,使用 malloc
為 head
分配 配置記憶體空間。然而,由於 malloc
可能會失敗,因此在配置後應進行檢查。
若記憶體配置成功,則可使用 list.h
中的 INIT_LIST_HEAD
來初始化節點,並將其回傳。
allocate 譯作「配置」,避免和 dispatch 的翻譯混淆。
注意空白字元。
已修正。
commit a17ec8e
在釋放佇列之前,應先檢查 head
是否存在,若 head
為空,則表示佇列不存在,無需進一步處理。
接著,檢查佇列是否僅包含 head
節點而無其他元素,若確實如此,則直接釋放 head
的記憶體空間即可。
若佇列內仍有其他元素,則應使用 list_for_each_safe
巨集 遍歷 逐一走訪每個節點佇列中的所有節點,並透過 q_release_element
釋放每個元素佔用的記憶體。最後,再釋放 head
節點的記憶體,以確保佇列被完全釋放
commit a7b2431 & commit 2deb4b2
我檢查 commit message 時發現,這兩則 commit message 的結尾沒有正確顯示 Change-Id,我不知道問題發生的原因。使用
git rebase -i
,修改 commit message,接著 Git hooks 應該就會生效問題已解決,我先嘗試
git rebase -i
的方式,但沒有效果,後來使用git reset --hard HEAD~2
將這兩則 commit message 刪除後重新 push ,參見 額外問題紀錄-處理 commit message 遺失 Change-Id 的錯誤。
commit a61a1d0
專案中的 qtest
執行檔提供了方便的測試工具,可用於驗證各函式實作的正確性,對應 q_insert_head
及 q_insert_tail
的命令分別為 ih
及 it
:
從結果可以看出,出現了錯誤訊息 "ERROR: Need to allocate and copy string for new queue element",而且佇列元素還出現了亂碼。為了確定這個錯誤訊息出現在程式碼的哪個部分,我使用 grep 命令進行搜尋:
在 qtest.c
中:
inserts
變數存放 argv[1] 的記憶體位址(即 ih xxx 的 xxx);而 cur_inserts
會在指定的佇列插入操作完成後,使用 list_last_entry
或 list_first_entry
讀取特定佇列節點中結構體的 value 值的記憶體位址。
而我原先的實作如下:
這會導致 inserts
與 cur_inserts
相等,進而觸發對應的錯誤資訊,我進行修改:
但當執行 git commit -a
時出現下方資訊:
於是我再次修改,我將 +1
移出:
再次執行 git commit -a
時出現下方資訊:
根據 Common vulnerabilities guide for C programmers 的描述:
The strcpy built-in function does not check buffer lengths and may very well overwrite memory zone contiguous to the intended destination. In fact, the whole family of functions is similarly vulnerable: strcpy, strcat and strcmp.
並且提到最佳的解決方式是使用 strlcpy
。
首先根據 queue.h
的描述,要注意這邊只是 remove 而非 delete,兩者差異如下:
commit 2c2d15f
現有的實作存在瑕疵,導致在執行 make test
時出現以下的警告資訊:
首先搜尋相關的程式碼段落位置:
相關的程式碼如下:
STRINGPAD 被定義為 1024,string_length 也被靜態宣告為 1024。
先為指標變數 remove
分配記憶體空間,大小為 1024 + 1024 + 1:
接下來看到這三行程式碼:
此時 remove
會被填充為 \0XX,....XX\0
,然後透過下方的 while
迴圈檢查原先填充至 STRINGPAD
區段的 X
總數是否有變動,以判斷是否發生溢出:
看到下方的程式碼,之前提過 string_length 被靜態宣告為 1024。
因此我之前實作的錯誤在於,我不小心覆蓋了 caller 的 buffersize,應進行如下修正:
在 queue.h
中可以看到本作業各個函式的細節說明,我注意到:
其中對於 sp
的描述與 q_insert_head 及 q_insert_head 中對於 s
的描述相同,但實際上 sp
並沒有進行插入的動作,詢問授課老師後,可提交 pull request 以修正此錯誤。
由於我沒有提交 pull request 的經驗,授課老師提供 About pull requests 指南,讓我進一步熟悉相關流程。
根據指南描述:
A pull request is a proposal to merge a set of changes from one branch into another. In a pull request, collaborators can review and discuss the proposed set of changes before they integrate the changes into the main codebase. Pull requests display the differences, or diffs, between the content in the source branch and the content in the target branch.
目前的分支包含了 queue.c 內所有函式的功能修改。如果在此分支上進行註解修改並提交 pull request,這些變更也會一併提交,因此應該建立一個新的分支。
目前只有我 forked 的 repository,輸入下列命令新增:
此時就會看到:
接著在這個乾淨的 repository 新增分支:
切換到這個分支後,本地端的檔案會恢復乾淨:
此時就可以進行對應的註解修正。
但當我想要提交時遇到下方問題:
詢問授課老師後,老師指出須產生新的 SHA-1 checksum。
原始的 SHA-1 checksum 如下:
我修改 queue.h
後其 SHA-1 checksum 如下:
使用 grep
命令搜尋:
對 checksum
檔案進行修改:
此時提交依然出現:
後來發現是我用於查詢修改後的 SHA-1 checksum 有誤,不應該使用:
而是:
再次修改 checksum
檔案:
此時執行 git commit -a
就不會出現相關警告了,撰寫完 commit message 後,須先提交這個分支:
再用此分支發 pull request 到 sysprog21/lab0-c。
根據 LeetCode 2095 的說明,中間節點定義為第 個節點,其中 n 為佇列長度,並且是基於 0-based indexing。
最基本的方式是先取得佇列的長度(可使用 q_size 函式 函數)。接著,使用除法運算計算欲刪除的元素索引,然後逐一走訪佇列,最終刪除對應的佇列元素。
詳閱資訊科技詞彙翻譯,以得知 function 的翻譯。
已修正,應該使用函式而非函數
另一個策略是使用快慢指標。慢指標每次走訪一個節點,而快指標每次走訪兩個節點。當快指標到達佇列的最後一個節點時,慢指標會恰好位於要刪除的中間節點。計算走訪次數如下:
快指標: 次
慢指標: 次
總共: 次
commit 684ea99
"access" 譯作「存取」,而非「訪問」(visit)。
已修正。
commit 4555913
在 3 月 27 日的直播課程中獲得了老師的建議,由於在合併排序演算法中也需要使用搜尋中間節點的操作,因此我將相關程式碼邏輯獨立出來成為一個函式,這樣可以實現程式碼的重用。
考慮到使用的資料結構是環狀雙向鏈結串列,我們可以使用兩個指標:一個指標每次存取 訪問 下一個元素,另一個指標則返回到前一個元素。如下圖所示:
再次計算其走訪次數:
指標 1: 次
指標 2: 次
總共: 次
相比之前快慢指標的策略,走訪總次數減少了 次。
在實作過程中,需要特別考慮到佇列元素為偶數的情況。在這種情況下,兩個指標不會指向同一個佇列節點,而是會交錯,如下所示:
因此,我在實作中使用了以下邏輯進行判斷:
這樣不論佇列中元素的總數是奇數還是偶數,都能夠正確地實現其功能。
目前上述各種方法均是基於理論分析提出,授課老師在 3 月 20 日的直播課程中指出,可以進行相應的效能分析。直播課程提到可以將額外的程式碼使用 GitHub Gist 展示,我將會提交方法一和方法二的程式碼(已更新於上方)。
根據去年的經驗,在 qtest
中使用的 q_show
會顯著影響效能分析的結果:若開啟 q_show
,執行結果為:
若關閉q_show,執行結果為:
因此,我另準備了一個分支,將 q_show
註解掉以便於實驗進行:
我先寫了一個 Python
檔案來生成測試資料:
這將生成 1000 筆尺寸在 100K 至 1000K 之間的佇列,最低從 100K 開始是因為若低於 100K,顯示的時間會為 0 秒,經過篩選發現只有大於 100K的數據才會顯示時間。接著,我用一個腳本去執行這些檔案,並將結果記錄於 CSV 中。
隨後,我使用 gnuplot
將三種方法的執行時間呈現如下圖:
從結果來看,方法三是最穩定且平均執行時間最低的。不過,有一部分引起了我的好奇,藍色和綠色的部分隨著佇列尺寸增大而發散的情況愈加明顯,在佇列尺寸相近的情況下,時間上卻有兩倍的差距,我尚未搞懂原因。
目前的時間顯示精度只到小數點後三位,於是我嘗試放大精度:
接著,我使用 taskset 將程式碼的執行限制於給定的 CPU 核,這時發現發散幾乎消失,三種方法均非常穩定,並未隨佇列尺寸增大而出現震盪:
為進一步推導起因,我使用 perf
進行分析,比較開關 taskset
對效能指標的具體影響,這裡我隨意選了一個樣本數較多的測試資料,各進行 10
次測試觀察結果。
run | cache_references | cache_misses | cache_miss_rate | cycles | instructions | ipc | time_elapsed | user_time | sys_time |
---|---|---|---|---|---|---|---|---|---|
1 | 17241656 | 9892382 | 57.37 | 826228499 | 1923288981 | 2.33 | 0.339832 | 0.179012 | 0.191972 |
2 | 14612480 | 8109421 | 55.50 | 795509847 | 1700698307 | 2.14 | 0.356477 | 0.204828 | 0.185673 |
3 | 23332294 | 12157972 | 52.11 | 1107577268 | 2504119125 | 2.26 | 0.335217 | 0.201407 | 0.163024 |
4 | 19864791 | 11531163 | 58.05 | 930359706 | 2187284696 | 2.35 | 0.340756 | 0.194819 | 0.175354 |
5 | 17080863 | 10516592 | 61.57 | 836851854 | 1979001851 | 2.36 | 0.339477 | 0.186157 | 0.185172 |
6 | 16963089 | 10041551 | 59.20 | 815348651 | 1933153510 | 2.37 | 0.338824 | 0.181302 | 0.189109 |
7 | 16047071 | 10110594 | 63.01 | 782730855 | 1884546384 | 2.41 | 0.341982 | 0.195674 | 0.177970 |
8 | 16136009 | 10222131 | 63.35 | 780970954 | 1873592083 | 2.40 | 0.347788 | 0.191405 | 0.191768 |
9 | 16059625 | 8368806 | 52.11 | 877706813 | 1839762446 | 2.10 | 0.353229 | 0.199560 | 0.185950 |
10 | 17805306 | 10371304 | 58.25 | 809727991 | 1909271619 | 2.36 | 0.342096 | 0.200188 | 0.174454 |
taskset
的執行結果如下::run | cache_references | cache_misses | cache_miss_rate | cycles | instructions | ipc | time_elapsed | user_time | sys_time |
---|---|---|---|---|---|---|---|---|---|
1 | 32026149 | 6741449 | 21.05 | 1446414938 | 2584233584 | 1.79 | 0.268393 | 0.188281 | 0.078488 |
2 | 32269571 | 6576403 | 20.38 | 1444059468 | 2583152521 | 1.79 | 0.268202 | 0.167397 | 0.098835 |
3 | 32133614 | 6689984 | 20.82 | 1448809063 | 2583787717 | 1.78 | 0.269490 | 0.173320 | 0.093786 |
4 | 31761274 | 6846540 | 21.56 | 1442830213 | 2582785998 | 1.79 | 0.267744 | 0.177639 | 0.088509 |
5 | 31735067 | 7403465 | 23.33 | 1448393784 | 2583511397 | 1.78 | 0.269289 | 0.179936 | 0.087251 |
6 | 31906122 | 6534310 | 20.48 | 1433374836 | 2579936789 | 1.80 | 0.266988 | 0.170548 | 0.093753 |
7 | 32207039 | 6501005 | 20.19 | 1440156894 | 2583018317 | 1.79 | 0.268275 | 0.186147 | 0.079369 |
8 | 31717134 | 6502431 | 20.50 | 1438018532 | 2577943850 | 1.79 | 0.267203 | 0.172772 | 0.092433 |
9 | 31970193 | 6832129 | 21.37 | 1447859351 | 2580957381 | 1.78 | 0.269134 | 0.183556 | 0.083528 |
10 | 31956559 | 6654394 | 20.82 | 1443032754 | 2580864068 | 1.79 | 0.269123 | 0.183641 | 0.082409 |
對比結果顯示,開啟 taskset
後的 miss rate 顯著下降,並且觀察到兩個表格中執行時間的部分:
taskset | min | max | 波動 |
---|---|---|---|
關閉 | 0.335217 | 0.356476 | 0.0213 |
打開 | 0.266988 | 0.269489 | 0.0025 |
可以看到,震蕩幅度相差接近 10 倍,這也就是為何實驗初期出現嚴重的發散情況。
此函示為布林型別,會根據是否成功刪除佇列的中間節點來回傳 ture 或 false,在原先的註解有一處錯誤:"If there're six elements, the third member should be returned." 應該將 returned 改為 deleted 才正確,因為該節點僅被刪除並沒有作為函式的回傳對象。
commit 2f84552
我一開始的想法如下:
但這邊有個致命性的錯誤,我在內層迴圈使用了 list_for_each
,這會導致佇列中的重複字串若是相鄰節點,則刪除後會導致無法繼續存取下一個元素,並且我宣告了 curr_2 = curr->next
,這個行為也沒有意義,因為查看 list_for_each
可知:
該巨集會初始化為 head->next
,如此會造成 Segmentation fault。應該要搭配 for 迴圈來實作。
使用 qtest
進行測試時我發現奇怪的地方,當重複的節點是相鄰的:
此時沒有問題,但當重複節點不是相鄰的:
最後的結果雖正確,但卻先出現了 "ERROR: Duplicate strings are in queue or distinct strings are not in queue" 這段警告訊息。
詢問授課老師後,此題目有預設佇列已排序好,所以我的實作有改善空間,目前使用內外兩層迴圈去走訪節點刪除,時間複雜度為:,但考慮到佇列已排序,可以優化使時間複雜度降為:。
commit 09d310d
其實這個作法是q_reversek
的一個特例,因此我們可以直接調用q_reversek
,並將k
設定為2
即可。
commit 075d580
commit 641e410
使用了 list_move 來簡化程式碼。
在實作 shuffle 時獲得了一個啟發,原先的交換方式是去改變串列節點的結構來達成此實作,而既然每個節點都內嵌在 element_t
這個結構體,可不可以只對值做交換而不去改變串列節點的結構呢?於是我做了以下的實驗。
測試資料準備:
隨機生成 100 個長度介於 1000000 ~ 10000000 間的佇列,其中每個佇列元素的字串均相同(16個字元),並紀錄其執行 reverse
時所花的時間。且有使用 taskset 將程式碼的執行限制於給定的 CPU 核。
結果:
以結果來看,僅對值交換一定程度上有改善執行時間,且觀察後可發現會隨著佇列尺寸變大,兩者間的差距也逐漸變大。
commit 069f235
commit 774d3bf
此次更新對於 k 的某些邊緣情況進行了優化,當 k 等於 1 時,函式將直接結束;而當 k 與佇列的長度相符時,將調用 q_reverse。
commit d6d8c49 & commit bd72375
我先完成了q_ascend
的實作,基本上這邊的整體程式架構與q_delete_dup
雷同,我們可以注意到一系列數字若是以降序排列,反轉後將會變成升序。因此,我們可以首先使用q_reverse
將佇列反轉,接著呼叫q_ascend
來進行排序,最後再呼叫一次q_reverse
就能實現所需的功能。
commit 4fd07ed
對程式碼進行優化,使檢查提早結束,不需要每次都走訪到最後一個節點。gi
commit 27345e0
先前的實作有缺陷,並不能透過調用佇列反轉及 q_ascend
,來實現 q_descend
,假設現有一個佇列:3 -> 1 -> 2
,原因如下:
q_ascend
定義(從左到右看):
移除任何一個節點,如果該節點右側存在一個比它更小的數字。
節點 | 右邊節點 | 是否右邊有更小的數字 | 是否刪除 |
---|---|---|---|
3 | 1,2 | 有,1<3 | 刪除 |
1 | 2 | 無 | 不刪除 |
2 | no | 無 | 不刪除 |
正確結果:1 -> 2
q_descend
定義(從左到右看):
移除任何一個節點,如果該節點右側存在一個比它更大的數字。
節點 | 右邊節點 | 是否右邊有更小的數字 | 是否刪除 |
---|---|---|---|
3 | 1,2 | 無 | 不刪除 |
1 | 2 | 有,2>1 | 刪除 |
2 | no | 無 | 不刪除 |
正確結果:3 -> 2
按照原先的程式邏輯,首先先將佇列反轉,變成 2 -> 1 -> 3
,執行 q_ascend
:
節點 | 右邊節點 | 是否右邊有更小的數字 | 是否刪除 |
---|---|---|---|
2 | 1,3 | 有,1<2 | 刪除 |
1 | 3 | 無 | 不刪除 |
3 | no | 無 | 不刪除 |
結果為:1 -> 3
,在執行一次佇列反轉:3 -> 1
,與正確結果不符合。
commit b484286
去年直接將merge sort
、tim sort
等排序算法的實作寫在queue.c
中,但發現這樣會使程式碼變得很混亂,因此今年額外新增了sort.c
檔案。
commit 49e47aa
引入 lib/list_sort.c到 lab0-c 專案
commit 39829a8
實作非遞迴版本的合併排序。
去年在進行這個作業時,我在這一部分卡住了,讓我有許多好奇的點:測試資料的準備以及相關的統計分析。我為什麼設計了1000筆測試資料,而每筆又是由長度介於5到15的隨機字串組成,數量從1K到1000K,會不會其實不需要這麼多?
隨機字串長度設計:
隨機字串的長度我參考了《Introduction to Information Retrieval》這本書的第五章第二節,書中提到英文中一個詞的平均長度約為八個字符,因此我將隨機字串的長度設為8,字元範圍為A到Z以及小寫字母a到z。
單一樣本的元素數目:
至於單一樣本的元素數量,我參考了論文《Engineering a Sort Function》中關於比較排序的段落:
To assess timing, we ran the programs on computers of disparate architectures: a VAX
8550 and a 40MHz MIPS R3000 (with 64-kilobyte data and instruction caches and a sec-
ondary cache of one megabyte), compiling with lcc on both. Table II reports times in sec-
onds to sort 10,000 integers chosen randomly from the range 0 to 999,999 on each of the
testbed’s six input types. Each time is the average of ten runs.
該段描述了他們在不同架構的電腦上運行程序來測試時間,包括VAX 8550和40MHz MIPS R3000,結果顯示在範圍 0 到 999,999 中隨機選擇 10000 個整數進行排序的時間。我決定將元素數量提高到 1000K,因為相較於當時的設備,我的 CPU 效能更強。
在實驗中我遇到了一些問題,當使用list_sort時可以順利記錄時間,但切換到merge_sort時出現了:
不過這個情況僅在元素數量為 1000K 時發生,降低到 500K 後就沒有這個警報,由於需要進一步分析,我只能暫時降低這個數字。這也意味著我目前的 merge_sort 在效率上還有很大的改進空間,雖然理論上的時間複雜度與 list_sort 相同,但實際效率卻有差異。
樣本數:
根據中央極限定理,當樣本數量大於等於 30 時,樣本的分佈相對穩定。因此我決定將樣本數設為 100。
所有測試均使用taskset執行。
排序法 | 平均值 | 標準差 | 變異數 |
---|---|---|---|
list_sort | 0.189545 s | 0.007821 s | 0.000061 |
merge_sort | 0.719091 s | 0.017078 s | 0.000292 |
可以明顯看到,list_sort 比 merge_sort來得更快,平均僅需 26% 的時間,且 list_sort的穩定性也較高。
接下來,我考慮不同分佈的測試資料,這次使用已排序好的資料進行測試:
排序法 | 平均值 | 標準差 | 變異數 |
---|---|---|---|
list_sort | 0.248384 s | 0.007395 s | 0.000055 |
merge_sort | 0.108697 s | 0.004558 s | 0.000021 |
這裡顯示 merge_sort 在已排序資料下更快且穩定,這現象的原因在於我對 merge_sort 加入了額外的判斷邏輯,如下:
我們可以將這次的 list sort 結果與前一個實驗 list sort 結果疊加來看:
從這邊可以發現有議題值得討論:
commit 6ab9c8f
這邊一開始在實作時,使用測試程式時一直出現錯誤資訊,原先的程式碼如下:
思考後發現是迴圈中止條件的設定有誤,假設目前有三個佇列需要合併。
這邊用加號來代表完成合併
這邊會把 head
也加進來,但實際上 head
並沒有嵌入 q_context_t
結構體,所以會出現錯誤。
此處實作有優化的空間,Merge k Sorted Lists,還在參考 案例探討: LeetCode 21. Merge Two Sorted Lists 進行改善中。
在完成前面數個指定的佇列操作後,此時執行 make test
,其分數顯示如下:
目前還沒有辦法獲得滿分,先追蹤專案中相關的程式碼以便確認問題所在處。
從 test_const
開始:
看到外層迴圈,在這邊因為 TEST_TRIES
被定義為 10,所以針對個別指定的佇列操作常數時間判定,最多會有十輪,每輪中會進行多次測量,已確保樣本數充沛(內層的迴圈)。
如何測量時間呢?我們進一步追蹤 doit
這個函式:
這邊總共有幾個關鍵步驟,我們依序追蹤,首先是 prepare_input
:
每次測試都準備 N_MEASURES 筆資料,並隨機分類(class 0 或 class 1),第一個 for 迴圈與此次指定佇列的操作次數有關:
input_data
會指向連續的記憶體空間,分為 150 個 chuck(每個 chuck 的大小為 2)。示意圖如下:
接著會呼叫 randombytes
將這 300 bytes 進行填充:
接著會對每個 chunk 進行分類,若被分派程 class 0 則將其 2 bytes 清為 0,關鍵程式碼如下:
而第二個迴圈生成的就是這筆樣本插入的隨機字串。
下一個呼叫的函式是 measure
,這邊假設我們進行的是 q_insert_head:
可以看到 dut_insert_head
有兩個傳入參數:
參數一:透過
get_random_string()
去獲得該樣本對應的隨機字串(對應到prepare_inputs
中第二個 for 迴圈)。
參數二:將個別 chunk 裡面的 2 bytes % 10000,可以得到介於 0~9999 的數值。
用 chunk 0 來舉例,就會是插入某個隨機字串至佇列頭部 0x5F12 % 10000 次。過程中透過 before_tick
與 after_tick
來紀錄起始與結束時間。
下一個呼叫的函式是 differentiate
,其實就只是紀錄 before_tick
與 after_tick
間的差值,就是該樣本的執行時間。
最後呼叫的函式是 update_statistics
:
呼叫 t_push
來將各個執行時間依序放入對應的資料結構中:
在各類別中,會獨立保存各自的平均值及平方差總和,這邊使用的是 Welford 演算法,不需要紀錄歷史資料即可更新:
當有新資料加入,先計算 delta:新資料值 - 過去歷史平均值,
接著更新平均值,新的平均值 = 過去歷史平均值 + delta / 資料總數。
舉例如下:
平方差總和同理:
經過上述步驟,我們現在擁有這些資料:
有了這些資料,在 report
中:
這邊會透過 t_compute
去計算先前兩個類別的變異數:
我統計非常爛,所以得先搞懂變異數是什麼,根據維基百科的描述,我的總結如下:
翻閱統計學教科書
已在下方說明。
變異數(Variance)是用來衡量一組數據的離散程度或波動幅度。換句話說,它告訴你:
→ 整體資料「偏離平均值有多遠」。
在這個實驗中,資料被分為兩個類別(class 0 與 class 1):
兩個樣本的統計結構如下:
類別 | Class 0 | class 1 |
---|---|---|
樣本數 | ||
樣本平均值 |
Class 0 中有 筆資料
Class 1 中有 筆資料
因此,兩組樣本的平均值分別為:
有了平均值,就能計算平方差,兩組樣本的各個樣本點的平方差分別為:
為什麼要平方呢?
舉例來說:
有三個數字分別是 ,其平均是 ,各值與平均相減為:
直接加總會抵銷為 ,所以才須對各樣本點的差值取平方再加總。
所以兩組樣本的平方差總和為:
有了上述的資料,我們就可以進一步計算變異數,平方差總和是一個母體中所有樣本的總體偏離程度,那麼變異數可以視為個別樣本點的每筆資料的平均偏離程度。
所以兩組樣本的樣本變異數為:
為什麼分母是樣本數 -1 而非樣本數呢?
因為後續要做 t test,所以將一個母體分成了兩個樣本,所以我們計算的是樣本變異數。
接著進行 Welch’s t-test
這個檢定方式來自於 Student's t-test 的改良,當要進行比較的兩個樣本其樣本數或變異數不一致,就可以使用 Welch’s t-test 來計算兩組樣本的差異性,程式當初在分組是隨機的,顯然 Class 0 與 Class 1 的樣本數不一致。
公式如下:
以上就是 t_compute 這個函式的背景知識,最後會將 t 值回傳,回到 report 這個函式:
宣告一個 max_t 變數儲存剛剛的 t 值。
取絕對值的原因在於會因為出來的 t 值有負有正,這取決於兩組樣本的大小,不過這不是我們關注的地方,我們只關注差距有多大。
接著要進行正歸化:
宣告一個 max_tau 變數儲存正歸化後的 t 值。
原因在於前面提到的 Welch’s t-test 公式,觀察分母不難得知會因為樣本數變大而使 t 值變大,我們需要將這個部份的影響抵銷。
最後進行比較:
對應原始 dudect 專案:
其實進行了多次的測試,第一組與目前專案相同,直接使用原始的執行時間進行 t test,這會得到一個 t 值,接著第二階段是會對執行時間進行裁剪,舉一個簡單的範例說明:
class [0] = [0,0,0,0,0,0,0,0,0,0]
class [1] = [1,2,3,4,5,6,7,8,9,10]
第一階段直接進行 t test 得到 t1。
第二階段會設計裁剪門檻,並分成 10 個 bucket,並為每個 bucket 設定門檻值:
門檻值計算公式:
class[1] 中索引為 5 的是 6,所以 bucket[0] 會搜集所有比 6 小的數:
1,2,3,4,5
依此類推,並拿各個 bucket 與 class[0] 進行 t test 這也會求算出多個 t 值(t2,…,t11)。
最後從 t1,…,t11 中取最大的 t 值,作為 constant time 的判斷結果,由此可以發現,原專案的精神更像是「寧可錯殺,也不錯放」。
我們可以將這個裁剪策略引進 lab0 中,我們可以先看看原始方式的執行時間分佈:
下圖所呈現的是執行
make test
時,經統計檢定後有顯著差異的總體資料分佈。
對這些資料,我們套用先前提過的裁剪方式,經裁剪後的分佈如下:
這邊的實驗有一點問題,正在排除中。
commit 83077d6
有其他學員導入原始 dudect 專案的裁剪策略,現在執行 lab0-c 的make test
時可通過trace trace-17-complexity
。
也順利的在 Actions 出現可愛的卡比:
去年我也沒有完成這部份,應該說我一直不懂這個部份到底要幹麻,不像是其他佇列指定操作那麼具體,雖然作業截止期限已過,但我還是決定在進行作業六之前,嘗試解決這個部份。
在 qtest
中也有提供 web
命令,如下輸入後會等待 port 9999 的輸入:
此時開啟另一個終端機,當輸入:
此時第一個終端機會如同正常使用 qtest
初始化一個佇列:
接著進行一連串的佇列操作:
當然剛剛初始化的佇列也會完成對應的操作:
在 qtest
中的命令都會對應到一個 do_xxx 函式,搜尋 do_web
後會發現其實作在 console.c
中:
這邊可以看到,將 9999 作為參數傳入 web_open
函式,其實作在 web.c
中:
在 CS:APP 第 11 章重點提示 有介紹到 client 及 server 的細節:
追蹤程式的執行順序:
第 1 步:
在 qtest.c
的 main
函式,接近末端的部份會呼叫 run_console()
。
第 2 步:
因為在 console.c
中會把變數 use_linenoise
設為 false,所以在 run_console() 中,一旦 CLI 輸入命令 web 時,另一個 client 的終端機會執行下面的程式區塊:
第 3 步,在 cmd_select
中會呼叫 linenoise()
,而接著又呼叫 line_raw()
:
而由於在 do_web()
中會:
所以此時 eventmux_callback 為 web_eventmux。而回到 line_edit()
中:
第 4 步,在 web_eventmux
實現 accept(),其中 web_recv
會解析我們輸入的 client 命令:
qtest
提供新的命令 shufflecommit 1729ab3
在 qtest.c
中,可以觀察到各個 do_xxx
函式與 console_init
中的 ADD_COMMAND
是成對出現的:
shuffle
並不需要額外參數,在命令列中應該輸入 shuffle 就能執行對應的操作,所以可以模仿其他函式進行錯誤警告的實作:
結果當我執行 qtest
時:
回顧我目前的 實作,我發現我的程式碼有一個致命的問題:
在這種狀況下交換是沒有問題的,但是當 old
與 new
相鄰時:
我原先的實作又用了兩個指標去指向 old->prev
及 new->prev
:
接著透過 list_move 來完成交換,這樣會發生 list_move(old,old)
,而 list_move 本身的實作是透過 list_del 及 list_add 來完成,所以才會出錯。
所以我後來額外實作一個用於交換兩個指定節點的 swap 函式。再次執行 qtest
驗證:
在實作的過程中,我意識到一個問題:
實際上,交換兩個節點的值未必得進行節點的移動。可以直接利用 list_entry
來獲取內嵌的 element_t
結構體的值,僅需進行簡單的值交換。這個思路或許可以進一步應用於 q_reverse
、q_reverseK
和 q_swap
等函式中,仍待驗證。
根據 N03: review 檢查事項 3,須利用 git rebase
,將先前的已提交的 commit 移至 3 月 18 日或之後的更新之上。
首先加入上游的 branch:
用 rebase 把自己的 commit 移到最新 upstream 後面:
首先看到衝突出現在 scripts/checksums
根據先前提交 pull request 的經驗,這邊選擇保留 upsteam 的版本。
接著:
這邊顯示還有另一個檔案的衝突須處理,也是保留 upsteam 版本的 scripts/install-git-hooks
,將所有的衝突解決後最後再用:
如此即完成指定的檢查項目。
留意 git rebase 操作,預期:
已修正,目前的 commit 歷史 已符合預期了。
在 3 月 20 日的直播中,授課老師檢查了我的 rebase 情況,發現了一些錯誤。我的解決方式如下:
首先檢查我自己所提交的 commit:
切換到一個與 upstream 相同的乾淨分支,利用 cherry-pick 將之前的 commit 合併到這個乾淨的分支上:
接著切換回 master 分支:
並將 master 的內容完全重設為剛才乾淨的內容:
最後,再次執行 pus:
根據信件,指定我回顧及審查的學員有以下 5 位:
為什麼不存浮點數?
用 arm 架構舉例,平常做 context switch 時只要保存 cpsr 及 spsr 內容即可,若要存浮點數,則須額外依賴 vfp(一個 double 8 bytes,需要額外 8 * 16 bytes 空間)。
使用 git rebase -i
來解決上方 Change-Id 不正確顯示的問題。由於出現問題的是最新兩則 commit message,故使用下方命令:
因為我只要修改 commit message,並沒有要修改 commit 的程式碼,所以此處選擇 reword。
此時會開啟這兩則 commit message,我進行小幅度修改後,接著:
commit 012fc82 & commit 98d66c4
檢查新的 commit message 還是沒有觸發 Git hooks。
隨後我刪除這兩次 commit:
commit f63c3aa & commit 4123e63
重新 clone 專案並修改程式碼,再重新git push
一次,檢查 commit message 後發現 Change-Id 遺失的問題已解決。