contributed by < fewletter >
list_head
element_t
根據 list.h
中的 INIT_LIST_HEAD
來實作 q_new
,此巨集可產生一個 prev
和 next
都指向自己的 list_head
指標,並且沒有包含資料
實作流程:
使用 malloc
分配記憶體空間,當配置記憶體空間失敗時,回傳 NULL
,若成功則產生一個 prev
和 next
都指向自己的 list_head
指標,並且回傳此結構
利用 list.h
的巨集 list_for_each_safe
,其中 entry
代表著進行迭代的節點, safe
則是 entry
的下一個節點, head
則指向我們要釋放的串列
實作流程:
l
是否為 NULL
,若不為 NULL
則進行下面的步驟list_for_each_safe
走過每個節點並且移除每個節點l
的記憶體空間實作步驟:
head
指向是否為 NULL
,是的話回傳 falsemalloc
配置出 element_t
大小的記憶體空間,若配置空間失敗,同樣回傳 falsestrlen
計算出 s
的長度malloc
配置出 s
長度再加上1的記憶體空間,若配置空間失敗,同樣回傳 falseelement
中,再利用 list_add
將它加入到 head 後面此函式和 q_insert_head
的程式幾乎相同,除了倒數第二行從 list_add
改成list_add_tail
利用巨集 list_first_entry
,得到開頭節點的字串
實作流程:
NULL
list_first_entry
,創造出一個 element_t 包含第一個節點和第一個節點的字串sp
後,刪除此節點並且將開頭交給原本串列的第二項此函式和 q_remove_head
幾乎相同,只有 list_first_entry
需改成list_last_entry
本函式讓指標 *node
走訪過每個節點,每走過一個讓 size+1
,邏輯並不複雜,所以主要考慮的點為讓程式碼盡量精簡
本函式利用〈你所不知道的 C 語言: linked list 和非連續記憶體〉提及的快慢指標來找尋一個串列中間的節點,通過兩個指標不同的速度,讓其中一個指標走完全程時,另一個在此串列的中間。
實作流程:
fast
和 slow
,fast
為 slow
的兩倍速度fast
迭代到底或是迭代到 head
時,slow
即為我們要找的中點slow
並釋放記憶體上面是 Leetcode 的標準 常見解法,但是此題並不只是單向的鏈結串列,而是具備環狀雙向特性,於是除了可指向後一個節點的 *next
以外,尚有參照前一節點的 *prev
,運用前述特性,實作下面程式碼。
實作流程:
left
和 right
,left
為從頭開始走的指標,right
為從尾巴開始走的指標,兩者速度相同right
指標善用 diff
展現程式碼變異處,查閱 diff(1) 以熟悉相關用法。
本函式使用 list_move
來達成兩兩互換的串列
原本想利用 list_for_each_safe
來實作此題的迴圈,但是其中迭代時的其中一行迭代的程式 node = safe
會造成迴圈停止,因為此時 node
已經移到 safe
後面,所以迭代時要使用 node = node->next
來進行迭代。
實作流程
下面是一個一般的串列
list_del
將 node
從原本的地方移除list_add
將 node
接在 safe
後面node = node->next
和 safe = node->next
,然後依照上面步驟再做一次本函式的目的為將 next
和 prev
相互交換,方法是將指標由指向 next
改成指向 prev
, 指向 prev
改成指向 next
。
實作流程
front
指向 head->prev
, now
指向 head
, next
指向head->next
now
的 *next
指到節點 front
, 節點 now
的 *prev
指到節點 next
,交換 front
和 next
的位置本函式利用 list_move
,將每個需要被反轉的節點,依序接到另行建立的 empty
節點,再用 list_splic_init
連接已觸及的節點。
撰寫更精簡的程式碼。
實作流程
k=2
作為舉例list_move
第一個節點到 empty
後list_move
第二個節點到 empty
後,得到反轉後的串列list_splic_init
接回去仿照 list_for_each_safe
的迴圈方法,寫一個 prev
的迴圈版本,想法是將所有節點與迭代中的最大值做比較。
實作流程
char *s
來記錄結尾的字串s
小於節點的內含值,s
則變為節點的值,反之則將節點刪除並且釋放記憶體空間上方程式碼迴圈內的 if-if-else 可簡化。
此函式會使用到下面的結構體
避免只用圖示來解釋 queue_context_t
,你應該先用清晰的文字敘述,再搭配圖解。
queue_contex_t
結構體, head
與 chain
的關係
此函式必須要靠著 head
迭代出所有結構體 queue_contex
當中的串列 q
,然後把每一個串列 q
以由小到大排序的順序串接起來,其中必須先將串列 q
利用指標 q->prev->next
指向 NULL
變成 singly-linked list,最後頭尾再接起來形成完整的 doubly-linked list。
實作流程
iter->q->prev->next = NULL
mergeTwolists
合併q_size(q_head)
參考〈你所不知道的 C 語言: linked list 和非連續記憶體: Merge Sort 的實作〉和〈Merge Sort 與它的變化〉中的 merge sort 的遞迴程式碼。
本函式的目的為將兩個串列以由小到大排序的順序連接在一起,並利用間接指標 **ptr
來指向兩個串列中迭代到的節點,避免使用 malloc
配置多的記憶體,無法釋放記憶體空間,此函式也是實作 merge
時會利用到的函式。
實作流程
list_entry
將兩個串列個別節點的 value
相互比較*node
指向 value
比較過後較小的節點*ptr
指向 *node
,最後再迭代到下一個節點本函式的想法即為將一個串列從中間一直切成兩份並在最後結合在一起
實作流程
mergeTwolists
將兩個節點以由小到大排序的順序一直合併實作流程
merge_sort
一直分開和合併節點與其逐行列出程式碼,你應該先闡述 Linux 核心開發者到底要解決什麼問題,以及這些考量如何反映在設計中。
了解,謝謝老師
研讀 list_sort.c前,第一個問題是自己所寫 merge sort
有什麼問題需要解決?
從Linux 核心的鏈結串列排序中可以知道,自己所寫 merge sort
為 Top-down mergesort
是透過不停的 partition
(分割),最後在兩兩串列合併成最後的串列,並且過程中完全沒有考慮到 cache
。
從以上註解可知道 Linux 核心開發者採用了只有合併串列,並且避免 cache thrashing
的合併方法,此種方法少了partition
(分割)這樣一直更新串列的動作,同時也對 cache
更友善。
閱讀 list_sort.c 程式碼時可以發現,此程式碼由三個函式 merge
, merge_final
, list_sort
所組成。
merge
此函式可以看到 cmp(priv, a, b)
, 從 list_sort.h
中可以看到其定義。
雖然還不知道 __attribute__((nonnull(2,3)))
是什麼,但是從下面的程式可以看出這是一個比較大小的函式,放到 merge
裡面的程式碼中,就可以看出如果當一個串列是以升冪 –- 由小到大排序的方式傳入, merge
就會以由小到大排序的方式結合兩個串列,反之由大到小排序也是相同情況。
查閱教育部辭典「升冪」:
數學代數式中,某一字母的指數,由低順次至高,稱為此一字母的「升冪」。
這裡不是多項式。
對解說的程式碼以一致的程式碼風格排版,善用 clang-format
lab0-c
專案中list_sort.c
和所需要的標頭檔到 list_sort.h
標頭檔程式碼
likely
,unlikely
和函式指標 list_cmp_func_t
linux_sort
到 qtest.c
中,可以讓自己手動測試為了能夠分別測試 list_sort
和實作的 sort
,所以按照 qtest
命令直譯器的實作的步驟在 qtest
裡面新增命令 linux_sort
。
研讀 option
相關程式碼,以此切換排序的實作,這樣就不用引入彆扭的 linux_sort
新命令,日後可繼續在 option
擴充新的排序演算法,但不用新增命令。
仿照 do_sort
實作 do_linux_sort
,作法是將下面程式碼
改成
trace-linux_sort.cmd
和 trace-sort.cmd
來自動測試 list_sort
和 sort
trace-linux_sort.cmd
trace-sort.cmd
merge sort
與 Linux 核心程式碼效能執行 trace-sort_compare.cmd
,執行以下命令:
執行結果如下
ih RAND 100000
q_sort | list_sort | |
---|---|---|
Elapsed time | 0.179 | 0.159 |
Delta time | 0.179 | 0.159 |
ih RAND 500000
q_sort | list_sort | |
---|---|---|
Elapsed time | 1.1014 | 0.808 |
Delta time | 1.1014 | 0.808 |
ih RAND 1000000
q_sort | list_sort | |
---|---|---|
Elapsed time | Time limit exceed | 1.725 |
Delta time | Time limit exceed | 1.725 |
接著利用 perf 原理和實務檢測 cache-misses
,cache-references
,instructions
,cycles
四個項目
ih RAND 500000
q_sort | list_sort | |
---|---|---|
cache-misses | 5032,7722 | 3538,6984 |
cache-references | 8940,8835 | 6581,8426 |
instructions | 21,2075,2982 | 21,1753,5235 |
cycles | 45,0536,4432 | 33,9000,1693 |
從上面數據可以看到除了 instructions
大約相同外,其他項目 linux_sort
都比 q_sort
還少
TODO: 針對排序演算法的限制,設計足以產生 worst-case 的測試案例。
想法:
list_sort
是一種只有合併的 mergesort
, 代表其 worst-case 是比較次數較多的串列實作流程:
list_sort
將串列排列list_sort
和 q_sort
的時間測試結果如下:
q_sort
開始排序時間為 0.57,排序完成時間為 0.74,排序時間為 0.17
list_sort
開始排序時間為 0.45,排序完成時間為 0.63,排序時間為 0.18
串列大小 / merge 種類 | q_sort | list_sort |
---|---|---|
10000 | 0.02 | 0.03 |
30000 | 0.17 | 0.18 |
100000 | 0.97 | 0.59 |
串列大小 10000
q_sort | list_sort | |
---|---|---|
cache-misses | 10,7731 | 21,2893 |
cache-references | 136,5123 | 132,8497 |
instructions | 4637,2298 | 4617,1204 |
cycles | 5828,7503 | 6080,4767 |
串列大小 30000
q_sort | list_sort | |
---|---|---|
cache-misses | 179,5892 | 205,1925 |
cache-references | 504,5521 | 473,5060 |
instructions | 1,3893,6266 | 1,3853,7035 |
cycles | 2,5346,8567 | 2,5478,4476 |
串列大小 100000
q_sort | list_sort | |
---|---|---|
cache-misses | 1189,0006 | 1050,7305 |
cache-references | 2089,1345 | 1916,7928 |
instructions | 4,7343,4558 | 4,6870,1310 |
cycles | 11,5324,7931 | 9,4894,3256 |
從上面四個表格可以看出,即使是在 list_sort
排序的最壞情況,時間都可以幾乎跟 q_sort
一樣,但是在 cache-misses
這項指標中,在 list_sort
排序的最壞情況,串列大小越小, list_sort
比 q_sort
多了很多,代表此項指標影響速度的程度很大,要降低排序時間首先的目標是要降低此項指標。
qtest.c
中加入下面程式碼q_shuffle
於 qtest.c
中
q_size
取得整體串列大小do_shuffle
程式碼,主要是讓自己所寫的 q_shuffle
可以在 qtest.c
中測試
為了讓測試程式可以在本地端直接使用 make shuffle
命令執行,所以執行下面的流程
Makefile
中執行 make shuffle
命令即可得到一個串列 shuffle
過後的出現次數的直方圖
由上圖來看,此圖符合 uniform distribution。
計算chi-squared test statistic
公式計算 | chi-squared | |
---|---|---|
總和 |
改用 LaTeX 重寫數學式
在此將 設為 0.05。
由於 [1,2,3] 有六種排列情形,所以它有 5 個自由度,根據卡式分佈表, value 介於 0.95 到 0.9 之間,因為 value(0.95~0.9)> alpha(0.05),統計檢定的結果不拒絕虛無假說()。
本篇論文在討論 dudect
,一項工具來評估一段程式是否可以在給定時間(constant time)運行。
而為什麼 constant time
對一段程式碼如此重要,可以參考解讀計算機編碼:在資訊安全的應用,又或是論文上的這段話
簡單來說就是通過程式實作時的一些指標,比如說程式執行時間,來推斷程式碼隱藏的東西。
dudect
是一種不需要知道 CPU 硬體結構也可以實作的檢測方法,此論文提及一些其他的研究作法,不過其他研究的共同缺點是都需要知道 CPU 硬體結構。
以下是此論文所提及的檢測步驟:
Cropping
主要是修剪掉較長的執行時間, Higher-order preprocessing
則是根據統計檢測來應用。Welch’s t-test
, 第二種則為 Non-parametric tests
。constant time
。simulation
程式實作原理
qtest.c
中有關 simulation
的命令simulation
模式,重點是 is_insert_head_const()
此函式會傳出 do_ih
執行時間是否為 constant time
。is_insert_head_const()
,在 fixure.h
中找到下面這項巨集fixure.c
中找到下面巨集test_const()
此項函式繼續尋找,路徑大概如下 test_const()
doit()
report()
,得知決定 is_insert_head_const()
是 true 或是 false 的關鍵在 report()
中t_compute()
就是在計算 Welch’s t-test 中的 treport()
中可以看到有三種情況會讓 qtest.c
中的 ih
測試結果不是 constant time
trace-17-complexity.cmd
通過最簡單的方法就是調整測試標準,不過此方法就代表上述統計檢驗方法完全沒用到,所以還在研究其他方法中。TODO