contributed by < scottxxxabc
>
(使用 VirtualBox)
注意書寫方式: VirtualBox (中間沒有空格)
安裝必要的開發工具套件
CppCheck版本
malloc
一個大小為 struct list_head
的記憶體,並將head
指向這個記憶體空間。prev
以及 next
指標指向 head
。l
指向要刪除的節點,刪除 l
時 l->next
指標也會遺失,因此用指標 tmp
指向 l
的下一個節點。container_of(l, element_t, list)
取得包含 l
的 element_t
節點。q_release_element()
釋放占用的記憶體。l
指向的節點為 NULL
,也就是list尾端。注意書寫方式: doubly-linked list (留意連字號和空白字元)
增加下方程式碼避免輸入 list 為空
可改寫為:
head
是否為空以及新加入的元素 new
是否成功 malloc
。strdup
配置記憶體並複製字串,並檢查是否成功。Memory leak
new
指向的新配置的記憶體未在結束時釋放,導致 new
指向的該記憶體在 q_inserthead()
結束後無法存取。
然而我們仍能利用container_of()
及 該記憶體空間包含的 list 節點位址來重新取得記憶體空間的位址
當需要使用到該記憶體時,可以使用
加入註解 // cppcheck-suppress memleak
忽略 CppCheck 判斷 new
指標未釋放的 memory leak error。
你應該說明為何抑制 Cppcheck 的 memory leak 錯誤是正確的操作。
q_insert_head()
相似,將最後一個步驟改為插入尾端。memcpy
將 bufsize
長的的記憶體空間複製至 sp
。sp
最後一個字元設為 \0
。** 將if (head == NULL || head->next == NULL)
的判斷修改為q_size(head) == 0
,
以及將
改成
q_remove_head()
相似,將第二個步驟改為尾端元素。利用額外空間存放 list 長度可使q_size()的呼叫為 constant time。
slow
以及 fast
指向 list 第一及第二個節點。
slow
每次前進一個節點。fast
每次前進兩個節點。fast
前進到 list 的尾端,也就是list 的最後一個節點或 head 時,slow
會指向中間的節點。q_release_element()
刪除 slow
指向的節點。tmp
指向第一個節點。
tmp
與 tmp->next
值相同,移出並刪除 tmp->next
。tmp
與 tmp->next
值不同tmp
指向 tmp->next
。tmp
指向陣列尾端。node1
及 node2
指向的節點。ptr
指向現在的節點,prev_node
指向 ptr
的前一個節點,next_node
指向 ptr
的後一個節點。
ptr->prev
、ptr->next
指向的位址。ptr
、prev_node
、next_node
往下一個節點前進。ptr
指向 list 尾端。q_sort()
由 q_sort()
, merge()
, mergesort()
三個函式組成。
將環狀結構拆開,呼叫 merge_sort()
排序後將 list 重新鏈接成 circular。
排序輸入的 doubly-linked list
將兩個 doubly-linked list merge成一個。
採用遞迴的方式實作,不使用原本的環狀結構以及head
節點,避免多餘的指標數值指派以及空間配置。
head
移出 list ,將環狀結構打斷。merge_sort()
排序 list。head
加入 list 前端,重新連結首尾。delete_dup()
相同的方式找到中間的節點。merge()
將排序好的兩個 list 合併。list1
、list2
分別指向兩個 doubly-linked list。newhead
指向新 doubly-linked list 的第一個節點,tail
指向 list 尾端。node
指向下一個要接在 tail
後的節點。*node
指向的節點連接到tail後。*node
指向下一個節點。tail
後。Condition always true/false
在 merge()
中的 while (list1 && list2)
迴圈中透過指標的指標操作 list1
、 list2
,但CppCheck仍偵測出 Condition always true
,推測是由於函式起始時if空指標的判斷
導致 CppCheck 認為 list1
, list2
非空以及無法偵測出指標的指標的間接操作造成。
下方的程式碼
也會因相同原因造成Condition always false
。
加入註解 // cppcheck-suppress knownConditionTrueFalse
可忽略此問題。
在 console_init()
加入程式碼
執行 make
,發現錯誤訊息
第 5 行顯示 ADD_COMMAND(cmd, msg)
巨集內的 do_##cmd
函式未宣告,於是加入
執行 qtest
,輸入 help
後 shuffle
出現於第 20 行。
tail
指向 list 最後一個節點。根據 Fisher–Yates shuffle 演算法實作,步驟如下
tail
節點之間隨機選一節點 ptr
。ptr
與 tail
互換。ptr
的下一節點為 tail
,交換節點之方法可參考q_swap()
ptr
的下一節點不為 tail
,:
tail
移出 list 。ptr
後ptr
移出 list 。ptr
加入 tmp
後。** 將 tail
指向 tmp
指向的節點。
priv
在 list_sort()
中並未使用,只是將 priv
傳給 cmp
,若 cmp
也不使用則可傳入 NULL
。head
為要排序的 list。cmp
為比較元素大小用的 function pointer, cmp
在 list_sort.h 的定義如下 :__attribute__((nonnull(2,3)))
表示第二及第三個參數的值不可能為 NULL
,
list_sort()
list_sort()
嘗試將待合併的 list 維持 的長度,若已存在 2 個 大小的 sublist ,後面的元素總量又達到 時,就會合併 2 個 大小的 sublist。
如此作法只要在 L1 Cache 能夠容納 數量的元素時就能避免 thrashing,也能夠減少 次的比較。
首先先將 list 尾端連向 head
的連結打斷,使其變成單向的 list,*pending
指向一個 list,其中每個元素皆為等待被 merge 的 sublist,並巧妙利用原 list 的 prev
指標作為指向下一個節點的指標。
初始狀態
count
= ,bits
= ,經過 for
迴圈之後,bits
為 0 ,因此不進行 merge
,將 list
節點加入 pending
。
count
= ,bits
= ,經過 for
迴圈之後,bits
為 0 ,因此不進行 merge
,將 list
節點加入 pending
,建立新的 sublist。
count
= ,bits
= ,經過 for
迴圈之後,bits
為 ,因此將*tail
指向的 sublist 與前一個 sublist merge
。
再把 list
節點加入 pending
,建立新的 sublist。
重複以上步驟至 list 沒有剩餘節點,
參考 SmallHanley以及 arthurchang09 的方式,在 qtest.c
中加入 linuxsort
命令來執行。
接著使用 qtest.c
的 time
命令來計算排序一百萬個節點所花費的時間,如下表。
節點個數 | 我的sort | linux sort | 我的sort / linux sort |
---|---|---|---|
500000 | 0.887(s) | 0.435(s) | 0.49 |
1000000 | 1.816(s) | 0.971(s) | 0.53 |
2000000 | 4.098(s) | 2.171(s) | 0.53 |
使用 Valgrind 的 Cachegrind 工具分析我的實作,以一百萬個節點為例。
Cachegrind 工具分析 linux list_sort,同樣排序一百萬個節點。
Valgrind cg-manual
Cachegrind gathers the following statistics (abbreviations used for each statistic is given in parentheses):
L1 read miss | LL read miss | Bcm | |
---|---|---|---|
my mergesort | 17,318,797 | 10,463,712 | 2,332,635 |
list_sort |
16,577,220 | 6,374,861 | 11,430,567 |
根據兩次的測試結果可以發現,我的實作在 Cache miss 方面比 list_sort
多,但在 Branch mispredicted 則比較少。Valgrind cg-manual 提到
On a modern machine, an L1 miss will typically cost around 10 cycles, an LL miss can cost as much as 200 cycles, and a mispredicted branch costs in the region of 10 to 30 cycles. Detailed cache and branch profiling can be very useful for understanding how your program interacts with the machine and thus how to make it faster.
可見 LL miss 的懲罰比起分支預測錯誤還要重很多。對於 list_sort
而言,減少 Cache miss 顯然是效益較高的。
仔細查看我的實作程式碼的部分可以發現在 merge_sort()
以快慢節點找出中間位置的時候發生了許多 miss :
此方法必須要走訪每一個節點,由於資料量高達一百萬個,也不可能將全部資料搬進 Cache,因此每一次調用 next
指標都很有可能造成 Cache miss。
merge()
函式中的 *node = (*node)->next;
另一個造成嚴重 Cache miss 的則是比較字串的部分,以下為 merge()
字串比較的部分
list_sort
則是傳入 list_sort()
參數的 cmp
function。
你所不知道的 C 語言: linked list 和非連續記憶體