contributed by < brianlin314
>
q_new
malloc
會在 size 為 0 時回傳 NULL
,因此需要在 malloc
後檢查 head 是否為 NULL
。
q_free
釋放佇列的所有節點及佇列本身,用 list_for_each_entry_safe
避免釋放 cur
後,無法往下一個節點前進,最後 free(l)
釋放 head
。
改進漢語描述。
q_insert_head
與 q_insert_tail
新增一個節點,並在最後引用 list_add
、 list_add_tail
,分別將其加入到 list 的頭或尾。
在實作 q_insert_head
與 q_insert_tail
時,我學到若 strncpy
傳入的字元個數為 str_len + 1
,那 strncpy
會自動補上 \0
,但我還是選擇額外多寫一行程式碼存入\0
。
為何「選擇額外多寫一行程式碼存入\0
」?請詳述你的考量,以及日後如何改進。
q_remove_head
與 q_remove_tail
先使用 list_first_entry
與 list_last_entry
取得頭尾節點的位址後,再使用 list_del
移除節點。
q_size
使用 list_for_each_safe
走訪整個list,以此算出節點數。
q_delete_mid
設 forward
和 backward
分別指向 head
的下一個與前一個節點,While 迴圈的中止條件為:
forward
與 backward
跑到同一節點上forward
與 backward
擦肩而過時因為本題需要進行 delete 而非 remove ,所以最後刪除 forward
所在的節點。
q_delete_dup
沒必要完整張貼程式碼,開發紀錄著重於你的思考過程和遇到的技術問題。
指標 node
指向 head,然後對 list 進行走訪,若 node->next
與 node->next->next
對應的 value 相同,就將 node->next
及所有後面有相同 value 的節點刪除,記下元素值 tmp_value
,之後不斷將 node->next
從list中移除,直到 node->next
指到 head
或者節點 value
不等於 tmp_value
時,即可刪除list中 value 為 tmp_value
的所有節點。
若 node->next
與 node->next->next
對應的 value 不同,就執行 node = node->next
。
此版本在進行 git commit 時,卻遇到以下錯誤,但若不加 node->next != head
,就無法通過 [3,2,1,1,1] 的測試,且此種寫法確實會造成其他人 code review 的不易,於是捨棄此種寫法,更改為以下:
改用 diff 呈現變更,避免張貼重複的程式碼。
使用 list_for_each_entry_safe
走訪 list ,得到當前與下一個節點的位址,並使用 strcmp
比對 value 是否相同,若相同,則刪除 curr
節點,並設 check = true
; 若不同,則判斷若 check = true
,要刪除 curr
節點。
q_swap
先判斷 head
是否為 NULL
或 list 是否只存在一個節點,先將第一個節點 first
移除,再將其加回第二個節點的頭,即可完成兩個節點的變換位置,而此時 first->next
所指的節點也剛好會是尚未變換位置的第一個節點。
q_reverse
使用 list_for_each_safe
走訪整個list,並將走訪到的 node ,移回 head 的頭,如此一來,就可以很簡單的實做 reverse
。
q_descend
架構 實做方法與 q_delete_dup
相同,只是將 next
全部改為 prev
,即對 list 做反向操作,若 node->prev
的 value 小於node
的 value,則刪除該節點,若不小於,則 node = node->prev
。
q_reverseK
作法與 reverse
相同,但改為 K 個一組進行反轉,且若該組的節點數小於 K ,則維持原樣 header
紀錄每一組的頭節點,change
紀錄要插入到該組的頭的節點,是一個 pointer to pointer ,作用為避免因為指標的改變,而喪失目標位置,可以達到簡化程式碼的長度。
q_sort
參考了你所不知道的C 語言: linked list 和非連續記憶體中的 Merge Sort
,分成三個部份:merge_two_lists
, merge_sort
, q_sort
merge_two_lists
將兩個不同的 list 合併並排序,使用 left
與 right
指標分別指向兩個 list 的開頭,比較指標內容,將較小的節點放到新的 list 中。
迴圈迭代完成後,會剩下其中一個 list 中尚存在節點,採 bit-wsie 找出非空的節點且能加速運算。
merge_sort
使用快慢指針技巧找到第一個與中間節點,並遞迴呼叫 merge_sort
,將左邊的串列與右邊的串列也各自找出中間節點,最後會切成一個一個節點,過程中要將切完的串列的最後一個節點的 next
指向 NULL
,最後呼叫 merge_two_lists
將兩個串合併。
q_sort
先將串列的最後一個節點指向 NULL
,作為其餘兩個函式的終止條件,並呼叫 merge_sort
進行排序,最後的 while 迴圈,是為了將所有節點的 prev
重新串接回去,使其重新符合 circular doubly-linked list。
注意書寫: doubly-linked list 裡頭連字號的位置。
q_merge
實作 q_merge
前,必須先熟知 queue_contex_t
這個資料結構,將各queue_contex_t
所指的佇列全部串連起來,最後使用實做過的 q_sort
排序以串連的串列即可。
與其逐行張貼程式碼,你應該闡述 Linux 核心開發者的考量因素,還有這些工程取捨如何反映到整體的設計中。避免「舉燭」!
避免非必要的條列 (及 -
開頭的項目符號),用清晰且完整的漢語展現。
-
的時機點
上方筆記也該調整
jservImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
torvalds/linux list_sort.c
研讀 list_sort
函式提供的註解,此外list_sort
需要3個參數:
priv
是一個 private data,用來傳遞 cmp
所需的參數
head
為要被排序的list
cmp
是一個元素比較大小的函式指標
@cmp
的回傳值是 int 型態,@cmp
的第一個參數負責接收 @priv
。第二個參數 @a
與第三個參數 @b
為串列的 Node。@a
應該排在 @b
之後,即 @cmp
回傳 >0 時,表示要進行 ascending sort@a
應該排在 @b
之前,即 @cmp
回傳 <=0 時,表示要進行 descending sort 或保留原本狀態@a < @b
和 @a == @b
的情況。接下來提到merge sort
的實作細節,方法會保持 2 : 1 的 list,假設有兩個 2k 大小的 list , merge sort
不會直接合併,而是等到有第 3 個長度為 2k 的 list 才進行合併,變成一個 2k+1 長度的 list 及一個 2k 長度的 list 。
只要這 2 : 1 的 list 都可以被放進 cache 裡,就可以避免 Cache thrashing 問題。
要實作上述方法,會透過一個變數 count
去計算目前 pending
上的節點總數,當每次 count
加1時,會將第 k 個 bit 設為 1 ,而 k-1 到 0 bit 則設為 0,且當 count 來到 2k 時,就把兩個 2k 長度的 list 做合併。
先檢查 list 是否為空或只有一個節點,接著將最後一個節點的 next
指向 NULL
。
以下為 count
0 到 15 的狀態:
舉 count = 4
為例,[2,1,1,1] 的第一個 2 代表這個 sublist 中有 2 個node,分別是 node1、 node2,接下來的 1 各分別代表 node3、node4、node5,因為第2和3個的sublist長度都為 20 (即只有一個節點),且第 4 個sublist長度也是 20,所以進行 merge,因此最終狀態變為[2,2,1]。
count<十進制> | count<二進制> | sublist 的狀態 |
---|---|---|
0 | [1] | |
1 | [1,1] | |
2 (merge) | [1,1,1] -> [2,1] | |
3 | [2,1,1] | |
4 (merge) | [2,1,1,1] -> [2,2,1] | |
5 (merge) | [2,2,1,1] -> [4,1,1] | |
6 (merge) | [4,1,1,1] -> [4,2,1] | |
7 | [4,2,1,1] | |
8 (merge) | [4,2,1,1,1] -> [4,2,2,1] | |
9 (merge) | [4,2,2,1,1] -> [4,4,1,1] | |
10 (merge) | [4,4,1,1,1] -> [4,4,2,1] | |
11 (merge) | [4,4,2,1,1] -> [8,2,1,1] | |
12 (merge) | [8,2,1,1,1] -> [8,2,2,1] | |
13 (merge) | [8,2,2,1,1] -> [8,4,1,1] | |
14 (merge) | [8,4,1,1,1] -> [8,4,2,1] | |
15 | [8,4,2,1,1] |
由以上程式碼與例子可得知:
pending
是已排序好,但尚未被 merge 的 sublist
bits
用於判斷何時必須 merge ,會檢查目前的 sublist 是否滿足 2k 個 Node,
if(likely(bits))
就會成立,並呼叫 merge
來合併最新的兩個 pending sublists,然後讓 list
指向下一個 Node;list
指向下一個 Node。在每次迭代時,list 會指向第 count
個 Node,pending
會指向前一個 Node,indirect pointer tail
會指向 &pending
並在 bits
向右移時,一直指向 tail = &(*tail)->prev
藉以把自己移動到可能將被 merge 的 sublist,並在 sublist 被 merge 後更新 prev
。
當 do while 迭代完成後,以上述例子 n = 15
為例,最後會剩下 [8,4,2,1] 4 個 pending sublist ,透過 for 迴圈合併:[8,4,2,1] -> [8,4,3] -> [8,7]。最後呼叫 merge_final 來合併剩餘的 [8,7],且 merge_final 會將整個 list 恢復成 circular doubly- linked list。
list_sort.c
引入到 lab0-c
專案參考 linhoward/linux2023-lab0 進行實做
研讀 qtest
命令直譯器的實作後,了解到要新增 qtest
的 cmd
的流程,紀錄在下方。
list_sort.c
的程式碼加入到 qtest.c
中。likely
與 unlikely
這兩個巨集,因此也需加到qtest.c
中。
list_cmp_func_t
的定義加入到 qtest.c
,並實作該 function。
do_linux_sort
,且在 console_init
新增 command。
merge_final
中的 u8
改成 uint8_t
。參考 perf 原理和實務 與 linhoward/linux2023-lab0 進行安裝與實做
可使用以下命令查看是否開啟 perf
若想要檢測 cache miss event ,需要先取消 kernel pointer 的禁用。
先撰寫想要進行測試的 .cmd
檔( RAND_1000.cmd ),以下為測試 q_sort
,若想要測試 linux list_sort
,則將 q_sort
改為 list_sort
使用 shell script
來執行 linux_sort
和 q_sort
的效能測試,shell script
撰寫參考 鳥哥私房菜-學習 Shell Scripts
以下為跑完測試的 .report
檔與比較表格
linux list_sort
# node | cache-misses | branches | instructions | context-switches | time |
---|---|---|---|---|---|
1000 | 2014 | 77,8034 | 514,7021 | 0 | 0.004317 |
10000 | 5333 | 615,8033 | 4291,8014 | 0 | 0.006202 |
100000 | 39,5825 | 6296,2214 | 4,3431,3455 | 0 | 0.058937 |
1000000 | 3577,2642 | 6,6064,3230 | 44,8546,9541 | 5 | 0.927776 |
q_sort
# node | cache-misses | branches | instructions | context-switches | time |
---|---|---|---|---|---|
1000 | 3091 | 77,8123 | 510,0095 | 0 | 0.004366 |
10000 | 1986 | 621,7308 | 4251,1116 | 0 | 0.00755 |
100000 | 61,7447 | 6376,2295 | 4,3054,4077 | 0 | 0.061952 |
1000000 | 4560,3884 | 6,6678,2837 | 44,2331,4748 | 4 | 0.99760 |
從我的實驗數據中可以發現兩者的差異不大,雖然隨機產生節點資料可能偶爾會讓 q_sort
的效能看起來更好,但從執行時間上來看都還是 list_sort
快了一點。
善用 gnuplot 製圖,你該分析效能落差的成因。
Valgrind 提供動態分析,用來追蹤及分析程式效能,幫助使用者偵測記憶體錯誤,諸如使用未初始化的記憶體、不當的記憶體配置、或取消配置記憶體,並加以分析。
執行命令 make valgrind
後,trace-13-malloc
得到 0 分
尋著錯誤訊息,找到在 qtest.c
中的 q_quit
函式可能存在錯誤,該函式功能為釋放佇列的所有記憶體,首先要先使用 q_free
釋放所有 queue_context_t
指向的 q
的所有記憶體,再釋放 queue_context_t
的所有記憶體,所以執行 make valgrind
會顯示還有記憶體未釋放。
因為在此函式中的第一行,直接執行 return true
,所以將這行註解掉即可。
修改後,再次執行 make valgrind
,發現 trace-02-ops
沒有過,於是去檢查 delete_mid
的實做,發現我只有 free(middle)
,但是應該要先進行 free(middle->value)
才對,所以更改程式碼為 q_release_element(middle)
。
再次執行make valgrind
,就沒有再顯示任何記憶體錯誤資訊。
qtest
執行過程中錯誤開啟 address sanitizer,並重新編譯
執行 make test
後,並沒有出現任何記憶體有關之錯誤。
透過 massif
查看記憶體狀況,可以觀察到建立的動態記憶體歸為 0,表示記憶體已被釋放。