contributed by < WangHanChi
>
make test
取得 100 分lib/list_sort.c
並將其加入專案中shuffle
select
system call 來完成 lab0-c 中的 web 功能INIT_LIST_HEAD
將 head
往前指標與往後指標都指向自己, 並且這個 head
不會有資料
list_entry
這個巨集主要是藉由 list
取得每個 member
的起始位置, 與 container_of
功能一致
會定義成 list_entry
是為了符合 list
的風格,關於 Container_of
可以參考你所不知道的 C 語言: linked list 和非連續記憶體
list_for_each_entry_safe
這個主要是 list_for_each_safe
與 list_for_each_entry
兩個巨集的功能整合
Entry
代表可以取用到 member
成員的資料safe
代表它會紀錄下一個 node
來方便使用者刪除目前的 node
根據要求,將回傳一個空的 list_head
,可以從 list.h
找到 INIT_LIST_HEAD
這個函式,並且會把 next
與 prev
都指向 head
malloc
分配一個記憶體空間,如果分配失敗就回傳 NULL
INIT_LIST_HEAD
對剛剛的 list_head
進行初始化list_head
根據要求,需要把所有 queue
所配置的記憶體釋放掉,也因為會需要走訪整個 queue
且又需要釋放記憶體空間,會使用前面提及的巨集 list_for_each_entry_safe
來精簡程式碼
會使用到釋放 queue
節點的函式
list_head
是否為空,若為空就離開函式list
的迭代器, 當前 queue
節點以及當前 queue
節點的下一個list_for_each_entry_safe
進行走訪,在每個節點都先釋放 list_head
,接著再使用 q_release_element
這個函式來釋放這個 queue
中的資料與自己list_head
的 head
根據引數,會得到鏈結串列的 head
以及字串 s
,需要新建一個 queue
的節點,並且將字串拷貝給節點的 value
。 :warning: 字串需要以 \0
作為結尾
list_head
是否為空,若為空就回傳 NULL
queue
節點配置記憶體空間,並且檢查是否配置成功str
所需要的大小,並且檢查是否配置成功s
給 str
,並且把結尾設置為 \0
list.h
中的函式 list_add
將這個新的節點插入到鏈結串列的 head
,完成後就回傳 true
原本沒有做好的部份
queue
的節點後,如果字串 str
的配置失敗的情況下,要記得釋放節點 queue
再離開函式strlen(s)
,經過老師的提醒後,修改成使用一個變數儲存這個字串的大小
因為
strlen(s)
的時間複雜度為 如果大量使用且字串長度變長的話,會降低效能
此部份與 q_insert_head
的實作方法大同小異,僅需要將 list_add
的插入點從原本鏈結串列的頭改成尾端即可,需要注意的地方也相同
list_head
是否為空,若為空就回傳 NULL
queue
節點配置記憶體空間,並且檢查是否配置成功str
所需要的大小,並且檢查是否配置成功s
給 str
,並且把結尾設置為 \0
list.h
中的函式 list_add
將這個新的節點插入到鏈結串列的 tail
,完成後就回傳 true
根據要求,此函式會將 queue
的 head
節點移除,並且根據 bufsize
把原本節點的資料存進 *sp
這個字串中。可以從Difference between “delete” and “remove”中得知 delete 與 remove 這兩個的不同
head
是否為 NULL
以及透過 list_empty
這個函式檢查鏈結串列是否為空的list_entry
取得要移除的節點list.h
中提供的 list_del
將這個節點移除並且把他的前後兩個節點相連此部份與 q_remove_head
的實作方法大同小異,僅需要將 list_del
的移除點從原本鏈結串列的開頭改成尾端即可,也就是在 list_entry
的 ptr
部份把 head->next
改成 head->prev
,其他需要注意的地方也相同
head
是否為 NULL
以及透過 list_empty
這個函式檢查鏈結串列是否為空的list_entry
取得要移除的節點list.h
中提供的 list_del
將這個節點移除並且把他的前後兩個節點相連TODO
在聽過老師的 Code Review 後,學到了一種更簡潔且程式碼共用行高的寫法
根據要求,可以使用 list_for_each
這個走訪的函式來進行實作
head
是否為 NULL
以及透過 list_empty
這個函式檢查鏈結串列是否為空的根據要求,將鏈結串列中點刪除。在 2095. Delete the Middle Node of a Linked List 上我所採用的方式為d快慢指標的方式,設定一個指標 rabbit
一次移動2個 node
,另一個指標 turtle
一次移動1個 node 。當 rabbit
移動到結尾的時候, turtle
的下一個 node 便是中點。於是我將這樣的思路搬到 q_delete_mid
上
head
是否為 NULL
以及透過 list_empty
這個函式檢查鏈結串列是否為空的rabbit
與 turtlr
的起點都設定在 head
rabbit
移動兩個 node 後,檢查是否到達鏈結串列的尾端或是回到 head
,若尚未到達就將 turtle
移動一個 noderabbit
檢查是否到達鏈結串列的尾端或是回到 head
turtle
的下一個 node 使用 list_entry
取得佇列的元素,再將其刪除發現可以改進的地方
原本使用的快慢指標適用於環狀與非環狀的 Linekd list,並且他的時間複雜度為 可以近似於 ,但是他的缺點是 rabbit
指標每次移動時會需要讀取兩次 next
,而 turtle
則需要讀取一次 next
,加總來看,每次移動需要花費三個記憶體操作的時間。
但是在環狀的 Linked-list 有一種方法也可以在相同的時間複雜度完成要求並且記憶體操作為快慢指標的 也就是每次移動花費2個記憶體操作時間
實作步驟如下
head
是否為 NULL
以及透過 list_empty
這個函式檢查鏈結串列是否為空的front
往 next
的方向移動,另一個 back
往 prev
的方向移動修改過後的程式碼也在下方的 完整程式碼
快慢指標
反向迭代
根據要求,需要將鏈結串列中有重複字串的節點刪除,可以從 82. Remove Duplicates from Sorted List II 中對於題目的敘述得知,head
所指的鏈結串列是已經 Sorted ,因此只需要考慮重複的節點會是相鄰的即可,也就是下面的第一張圖的情況
head
是否為 NULL
, 透過 list_empty
這個函式檢查鏈結串列是否為空的以及使用 list_is_singular
這個函式來檢查鏈結串列是否只有 head
與一個節點flag
並給予初始值 0,他的用途是檢測字串是否有出現重複list_for_each_entry_safe
來走訪每個節點,使用 str
這個變數來儲存下一個節點的字串內容head
並且目前節點與下一個節點的字串不一致且 flag
為 0 就進行正常迭代,若是 flag
為 1,就刪除目前的節點flag
設定為 1,接著進行迭代true
根據要求,需要將鏈結串列中的兩兩一對位置交換,從 24. Swap Nodes in Pairs 可以得知,我們僅能交換節點的位置來達成目的,並不可以透過更改節點的字串的指標來完成。
實作流程
head
是否為 NULL
,並且透過 list_is_sigular
這個函式來判斷使否需要進行 swaplist_head
的指標,分別指向 head->next
(以下稱為後者) 與 head->next->next
(以下稱為前者)for-loop
進行迭代,需要進行以下步驟
list_del
將前者與鏈結串列斷開連結prev
指派給前者的 prev
next
prev->next
prev
圖解說明
back = head->next
與前者 front = back->next
front
的與鏈結串列的連結back
的 prev
指派給 front
的 prev
front
的 next
指向 back
front
指派給 back
的 prev->next
front
指派給 back
的 prev
back
與 frnot
都往前移動兩個節點TODO
在聽過老師的 Code Review 之後,發現 q_swap
與 q_reverseK
是可以共用程式碼的,因此想要找時間比較目前的 q_swap 與 直接呼叫 q_reverseK 的效能差異
根據要求,需要將鏈結串列中的節點反向連接,並且不可以使用 q_insert_head
或是 q_remove_head
這類有 malloc 或是 free 的指令。 可以從 206. Reverse Linked List 完成 Single-Linked_list 的解法,再將其延伸到環狀雙向的版本
實作流程
head
是否為 NULL
,並且透過 list_empty
檢查是否為空的鏈結串列list_head
的指標,分別是 before
, current
, after
while-loop
進行迭代,迭代過程中需要進行以下幾個步驟會使用 Graphviz進行表示圖解說明
before = head->prev
, current = head
, after = NULL
while-loop
,直到 after
指到 head
為止after
指向 current->next
current->next
指派到 before
, current->prev
指派到 after
current
指派給 before
after
指派給 current
while-loop
迭代即可根據要求, reverseK
是 reverse
與 swap
的混合體。在一條鏈結串列中,每 K 個元素進行 reverse
,若是 K 的值大於剩下的節點數,就不做任何的改變。
在實作當中會使用到以下兩個在 list.h
中所定義的函式
這個函式的用途是把一條鏈結串列切成兩條
傳入的引數
linked_list
所要接入的點linked_list
的前一個節點linked_list
的點把 linled_list
插入到另外一條中
實作流程
head
是否為 NULL
,並且透過 list_empty
檢查是否為空的鏈結串列以及使用 list_is_singular
確保鏈結串列裡面有不只一個節點list_cut_position
這個函式,所以我們先使用 LIST_HEAD
這個巨集建立一個新的 list_headlist_for_each_safe
這個函式來完成迭代num
,當 num
等於 k
的時候,就執行剪下 K
個節點,並且 reverse
,最終再將其接回原本的點。圖解說明
node
, safe
, tmp = head
LIST_HEAD
建立一個新 list-head
的 head
k
目前為 3 ,也就是 list_for_each_safe
進行三次迭代才會開始進行切斷…等等動作,下圖展示的是第三次迭代list_cut_position(&new_head, tmp, node);
,從 tmp
得下一個節點到 node
切下後貼去 new_head
,可以看到下面變成兩條鏈結串列了new_head
進行 reverse
new_head
插入 tmp
之後,再初始化 new_head
tmp
移動到 safe
的前一個,便完成了一次的迭代根據要求,需要將鏈結串列進行由小到大排序。這題參考自 你所不知道的 C 語言: linked list 和非連續記憶體 中的Merge_Sort
以及 陳日昇 同學的實作。
實作流程
q_merge
head
是否為 NULL
,並且透過 list_empty
檢查是否為空的鏈結串列head->next
作為 merge_divide
的引數以及函式回傳值prev
都是亂掉的以及環狀結構尚未接回,因此要走訪每個節點,並且把 prev
都接上,最後再把尾端接回 head
merge_divide
head
是否為 NULL
,並且檢查 head->next
是否 NULL
,若是就直接回傳 head
list_head
,再呼叫自己進行迭代merge_two_nodes
merge_two_nodes
left
與 right
的字串大小圖解說明
merge_divide
這個函式主要在進行下面橘色框框的動作,而 merge_two_nodes
是在進行藍色及綠色框框
這題 leetcode 解法是參考自 Youtube 影片中所講解的第二種思路(Approach 2)
由於 Leetcode 是單向的 linked_list
因此由左邊比較到右邊較為複雜,因此這題的關鍵點就在於 reverse ,只要先將原本的鏈結串列 reverse
,就可以方便地進行比較所有節點大小的動作,最後再將鏈結串列 reverse
即可達成要求
而實作 q_descend
的時候就更為方便,因為使用的是環狀雙向鏈結串列,可以不須經過反向,直接透過 prev
操作即可,以下為實作流程
list_head
的指標 node
指向 head->prev
以及 node_prev
指向 node->prev
list_entry
各別取得佇列內的資料ndoe
的字串比 node_prev
的字串還小的時候,就不進行移除節點,將兩個節點都分別往 prev
的方向移動一格ndoe
的字串比 node_prev
的字串還大,就將 node_prev
移除(delete),然後再進行下一輪的走訪根據要求,傳入的 head
為 queue_contex_t
的 head
,因此也會需要將節點往 next
移動一格才開始存取每個queue
的 head
。 他可以視作 merge sort 的延伸版,透過已經實作好的merge_two_nodes 這個函式,可以把兩段長度不相同的鏈結串列由小至大合併,因此只需要將每個 queue
的 head
都加入後便可以把所有的點併入一條
head
是否為 NULL
,並且透過 list_empty
檢查是否為空的鏈結串列queue
的節點個數,再走訪每個 queue_contex_t
的 q
merge_two_nodes
這個函式將兩條鏈結串列合併成一條INIT_LIST_HEAD
將每個空的 head
都指向自己,避免出錯q_sort
的實作一樣,需要將 每個節點的prev
都接回,並且把它接回環狀的lib/list_sort.c
先去取得 lib/list_sort.c,接著開始研讀程式碼,因為怕篇幅太長超過限制,因此,我將研讀的筆記額外放在這Lab0 lib/list_sort.c 學習筆記
接下來要將 list_sort 加入專案中
queue.c
同個層級的目錄unlikely
與 likely
這樣的巨集,因此需要將 compiler.h 中的這兩行加入 list_sort.h
中list_sort.c
的檔案中,有使用到 u8
這樣的型別,所以也將其以巨集的方式定義在 list_sort.h
下Makefile
中 OBJS,新增一個 list_sort.o
qtest.c
這個檔案
include "list_sort.h"
do_sort
中的執行 sort的部份add_param
加入 help
的選單中可以看到下方的 option
有多了 listsort
這個選項
貼心提醒(x) 採雷實錄(o)
在使用 clang-format -i
的時候,記得不要對 Makefile 使用
由於我的電腦是這學期重灌到 ubuntu-22.04 的,所以效能測試工具 perf 需要重新下載,跟著老師的筆記一步一步即可,不過這邊要特別注意,我們一般安裝的 kernel.perf_event_paranoid
會是 4 ,可以透過
$ cat /proc/sys/kernel/perf_event_paranoid
來進行查看,但是我們如果想要把權限全部打開的話,必須使用這條指令
$ sudo sh -c "echo -1 > /proc/sys/kernel/perf_event_paranoid"
來將權限值設為 -1
再來可以輸入 $ perf top
來確認自己是否有拿到最高權限值
在 trace 這個目錄下創建一個新的測試命令集 trace-sort.cmd
,裡面的測試如下
接著執行它
$ ./qtest -f traces/trace-sort.cmd
就可以得到
可以看到我們使用了 qtest 中的 time
功能,來取得我們執行 sort
所花費的時間,最後的那個 Delta time 就是我們所希望得到的
接著開始比較 q_sort
與 list_sort
這兩個排序的效能差別
我們分別測試資料比數從 10 萬筆資料到 100 萬筆資料
並且重複做三次
資料數 | 1. q_sort() | 2. q_sort() | 3. q_sort() | 平均 |
---|---|---|---|---|
10 萬筆 | 0.038 | 0.036 | 0.039 | 0.038 |
20 萬筆 | 0.095 | 0.089 | 0.096 | 0.093 |
30 萬筆 | 0.198 | 0.192 | 0.218 | 0.203 |
40 萬筆 | 0.313 | 0.309 | 0.340 | 0.321 |
50 萬筆 | 0.440 | 0.427 | 0.491 | 0.453 |
60 萬筆 | 0.556 | 0.592 | 0.616 | 0.588 |
70 萬筆 | 0.697 | 0.702 | 0.762 | 0.720 |
80 萬筆 | 0.846 | 0.831 | 0.901 | 0.859 |
90 萬筆 | 0.975 | 0.960 | 0.996 | 0.976 |
100 萬筆 | 1.123 | 1.098 | 1.119 | 1.113 |
資料數 | 1. list_sort() | 2. list_sort() | 3. list_sort() | 平均 |
---|---|---|---|---|
10 萬筆 | 0.023 | 0.023 | 0.023 | 0.023 |
20 萬筆 | 0.069 | 0.064 | 0.060 | 0.064 |
30 萬筆 | 0.129 | 0.124 | 0.132 | 0.128 |
40 萬筆 | 0.213 | 0.220 | 0.216 | 0.216 |
50 萬筆 | 0.296 | 0.290 | 0.290 | 0.290 |
60 萬筆 | 0.411 | 0.401 | 0.392 | 0.401 |
70 萬筆 | 0.498 | 0.507 | 0.505 | 0.503 |
80 萬筆 | 0.645 | 0.611 | 0.600 | 0.619 |
90 萬筆 | 0.708 | 0.680 | 0.708 | 0.699 |
100 萬筆 | 0.805 | 0.808 | 0.791 | 0.801 |
可以看到他 list_sort 比起 q_sort 的效能大約都好了 30% ~ 40%
接下來使用 perf
這個工具來看更詳細的對比資料
根據老師的筆記,可以使用以下指令來取得 cache-misses
, cache-references
, instructions
, cycles
這些項目的資訊
$ perf stat --repeat 10 -e cache-misses,cache-references,instructions,cycles ./qtest -f trace/trace-sort.cmd
這條指令會重複跑10次 ./qtest -f trace/trace-sort.cmd
這個命令,並且將我們所設定要取得的資訊統計出來,如下面所列
接著,我因為覺得每次都要打這麼長的命令很不人性化,因此我將它加入 Makefile
裡面
這樣以後只要輸入 make perf
就可使用了
接著來比較兩者的差異在 50萬 比資料下的狀況
項目 | q_sort | list_sort |
---|---|---|
cache-misses | 27,057,256 | 20,610,754 |
cache-references | 75,094,812 | 59,647,674 |
% of all caches refs | 35.770 % | 34.833 % |
instructions | 2,292,705,155 | 2,276,127,334 |
cycles | 3,545,449,526 | 2,488,046,136 |
insn per cycle | 0.70 | 0.95 |
可以看到 list_sort 在 cache-misses 的數量上少了將近 35% ,這樣該就是老師在作業提示這邊所說的,linux kernel 開發者有針對 cache
來做演算法的設計,因此才可以減少這麼多的 cache-misses,並且同時也減少了 instructions ,所以 list_sort 的 IPC 會比起 q_sort 好 35%
qtest
提供新的命令 shuffle
根據 Fisher–Yates shuffle 演算法的概念,可以得知該演算法的實作步驟分為以下幾點
q_size
取的節點個數shuffle
完成之際觀察 lab0-c
的檔案結構可以知道如果要加入新的命令需修改 qtest.c
這個檔案,首先先實作 q_shuffle
這個功能
當主要的功能實作完成之後,再來需要的就是將執行他的函式實作出來,這邊主要進行的就是引數的檢查, 還有啟用 set_noallocate_mode
這個功能後,就開始執行 q_shuffle
,再來就是判斷是否執行超過時間,最後再將 set_noallocate_mode
取消啟用即可,回傳值使用 q_show(3)
接著開始研究 q_show
這個函式,假如們希望印出正確的回應 ( 例如 l = [ a b c d e ]
) ,就必須要通過像是雙向環狀或是非 NULL
這樣的條件,才可以透過第893行將正確的值輸出
經過上述,我們可以實作出 do_shuffle
這個函式
首先研讀了老師的作業提示以 Valgrind 分析記憶體問題,可以知道 Valgrind 這個工具的強大
並且我們只要在終端機輸入
就可以執行 Makefile 內預先寫好的腳本,以下是 Makefile 中關於 valgrind 的部份
可以看上面的 valgrind_existence
是用來檢查是否安裝了 valgrind
接著繼續看到 valgrind
的腳本,可以看到它會利用 mktemp
建立一個暫存的檔案,接著複製檔案過去,再幫該檔案加上所有使用者( +a )都可以執行( +x )的權限
這邊我認為在 chmod u+x $(patched_file)
這行指令應該要使用 root 權限,從鳥高私房菜-改變權限, chmod 這篇文章中的範例來看,要執行 chmod 操作是需要切換到 root 身份的。所以我覺得這行應該改為 sudo chmod u+x $(patched_file)
會比較恰當,或是在執行 make 腳本時要使用 sudo make valgrind
。
提交 pull request?
:notes: jserv
後來詳細觀察了檔案權限以及程式碼,發現是我搞錯了,加上 sudo
會使得編譯出的檔案變成僅限 root 權限的,在之後的刪除 make clean 會產生問題,下次會再更加謹慎的。錯誤的提交
接著執行 make valgrind
,可以發現執行的速度下降了許多,符合老師所提及的
最後發現並沒有洩漏的記憶體
接著使用 Massif 視覺化工具
可以在 valgrind 使用的參數列表中加入 --tool=massif
就可以得到一張 massif.out.XXXXX,其中的 XXXXX 會是一串數字
接著要去massif-visualizer 安裝 massif-visualizer桌面版
接著輸入
就可以得到 massif 記憶體分析的圖了!
根據 Makefile 中關於 Sanitizer 的部份,可以看到
若是執行 make
的時候一起加上了 SANITIZER=1 這樣的參數就可以開啟 Sanitizer,來檢查程式碼
:warning: Sanitizer 與 Valgrind 是不可以同時使用的,所以假如在 make
的時候有加上了SANITIZER=1
這樣的參數的話,就要記得先執行 make clean
清除先前所編譯好的才能正常使用 Valgrind
SANITIZER 使用步驟如下:
$ make SANITIZER=1
$ make test
// 若是沒有報錯就可以清除
$ make clean
原本針對程式碼的偵測工具,是針對不同的硬體所設計的,因此換了一個平台,例如不同的 CPU 或是 micro-code 更新,原本的工具就沒有辦法使用了,為此,這篇文章提出了基於統計機率的方法來檢測程式碼。
論文使用的方法是 TIMING LEAKAGE DETECTION,首先測量兩個不同輸入數據類型的執行時間,然後檢查這兩個時間分布是否在統計上不同
it
, ih
, rh
及 rt
) 對屬於兩個不同輸入資料類型的輸入的執行時間。
RDTSC
,而在 aarch64 平台下可以使用 mrs
個暫存器搭配內嵌組合語言的方式來呈現,詳情可以看這裡可以知道從原始的實作來看,PERCENTILE 可以去除掉極端值,但是在 leb0-c 並沒有相關的實作,因此這會是其中一個改變的方向。 再來是上面所提到的 Classes definition ,在 lab0-c 的實作中, 固定資料的字串是用 0 來進行填充的,但是 0 在 ascii 碼中代表的是 '\0' ,這與字串的結尾符號一致,所以這可能會導致問題。 再來是在 measure 這個函式中,使用了兩個 int 來保存插入或是移除前後的個數是否為正確的,但是這樣的檢查僅只有確認個數,但卻沒有明確知道它是否為插入在頭尾…等等,因此這也是一個問題
故目前發現的問題為
首先因為老師原本的參數是散裝的,但在原本的 dudect 中是使用 struct 來包住許多的參數
所以我把上面替換成下面,這邊的 struct 也與原本的有所不同,原本的 ticks 是儲存在同一個陣列的,但我改成像是原本的定義,分成 before_ticks 與 after_ticks 。
同時也新增了關於 percenteils 相關的巨集以及函式
特別注意,因為我把上述這些參數都改成 struct 了,所以相關函式的引數都要進行更改。
可以看到原本的老師對於 fix string 的部份是以 0 來進行 memset 的填充,但是在 ascii 碼中, 0 代表的是 '\0',也就是字串的結尾符號,我認為在這邊不應該使用結尾符號來進行填充,所以我將 'a' 填充進去,並且在字串的結尾使用 '\0' 。同理,在隨機生成的的話,就會需要避免隨機產生的數字為 0 ,所以我將隨機生成的數字加上了 65,以避免這問題。
在 measure
這個函式中,以 insert_tail 這個函式為例
可以看到它取得了插入前後的 q_size(),並且相減來比對是否有插入,但是這樣的檢查僅能檢查出是否有插入成功,並不能知道它是否插在尾巴,所以這個檢查的操作我就先將其移除,畢竟在 make test
的前面也有檢查過 insert head 等等的功能了。
在經過這樣的修改過後, make test
就可以得到 100 分了
TODO
把 lab0-c/dudect 下的檔案整理的好看一些
select
system call 來完成 lab0-c 中的 web 功能