contributed by < Appmedia06 >
這篇主要是研讀 list_sort.c
的筆記,首先先嘗試理解這個檔案寫了什麼,可以看到裡面總共有3個函式,分別是 merge
, merge_final
, list_sort
,簡單看過一遍後得知 list_sort
是使用 merge sort 實作,因此呼叫到 merge
和 merge_final
,以下開始詳細討論個函式功能。
__attribute__((nonnull()))
在每個函式前面都有這一行程式,它是由 GNU C 編譯器所提供的一個特殊屬性,所謂 nonnull
是在告訴編譯器函式的第幾個參數不應該為空指標,否則會產生 undefined behavior 。
以 list_sort
為例
第二個參數 head
和第三個參數 cmp
不能為空指標。
merge
將兩個以排序的鍊結串列 a
和 b
合併成一個已排序鍊結串列。
這和我在 lab0 所實做的 merge_Two_list
雷同,使用間接指標來合併鍊結串列
演算方法
head
,當作合併的鍊結串列tail
,指向 head
a
跟 b
的大小,將小的指標透過 tail
放入 head
中head
結束迴圈,回傳 head
完整程式碼
註解說到,若 a
和 b
的值是相同的,則取 a
放入,其目的是為了要維持 sort stability 。
sort stability: 在排序演算法中,穩定性(stability)指的是如果兩個元素在排序前的順序相等,那麼在排序後它們的相對順序仍然保持不變。 換句話說,穩定性表示排序演算法在排序時會尊重相等元素的原始順序。
merge_final
和 merge
的操作類似,主要差別為因為這是在排序的最後才會呼叫到,因此需要實現以下兩件事
註解說明了當出現高度不平衡時,這個 do_while 迴圈會被呼叫很多次,但 b
這個鍊結串列不是被排序好了嗎,為什麼不直接接在 head
的後面就好啦。這邊就要提到 unlikely
函式。
在 linux kernel 的 /include/linux/compiler.h 中定義了兩個 macro
__built_expect()
是GCC的內建function,用來將branch的相關資訊提供給compiler,告訴compiler設計者期望的比較結果,以便對程式碼改變分支順序來進行優化。所以這樣做有什麼意義以及好處呢?
如上述所題,如果發生高度不平衡的情況, b
就會變得非常的長,因此我們就會在迴圈內一直呼叫到 unlikely
,我們知道在取得資料時,會先從 Cache 找,若沒找到(cache miss),就會從 Memory 搬取一個 block 的資料,因為 patial Locality 的特性,程式碼相近的區域可能會一起被抓取進 cache ,進而大大提昇 cache hit 的機率。而 unlikely
函式就是使不可能的區域往後移,也就使其變成一個對 cache 友善的程式。
prev
串上list_sort
接下來終於來討論 Linux 對於鍊結串列排序的設計,在看完整程式碼前先來看註解。註解不但可以讓我們更快進入狀況,更幫助我們更加理解設計者本身的想法。
priv
: 為一個指標,傳遞 cmp
函式需要的參數head
: 為要被排序的鍊結串列cmp
: 需要自定義的比較 function pointer這邊說明了 cmp
比較函式的規範,如果 a
應該在 b
之後排序,則比較函數應傳回大於 0 的值;如果 a
應該在 b
之前排序或它們原始的順序應該保留,則比較函數應傳回小於等於 0 的值。
這邊說明了這個實作 merge sort 最重要的部份,也就是如何合併,我們一開始學習的 merge sort 都是 top down 的作法,這樣的作法是先做 partition 再做 merge ,而在做 partition 時會有大量資料在 cache 上移進移出,進而導致發生 Cache Thrashing 。
Cache Thrashing: 是指在電腦系統中頻繁地發生快取未命中和刷新的現象,導致系統效能急劇下降的情況。 它通常發生在快取容量不足以容納正在存取的資料或指令時。
而 Linux kernel 的開發者如何避免這問題呢,他們使用了 bottom up 的方式實做,具體來說就是始終保持兩個要合併的串列的大小比例為 ,也就是說大的串列是小的串列的兩倍,而合併方式則是如果有兩個大小 的串列來時並不會馬上合併,而是等到第三個大小為 的串列進來才選擇合併成 和 的量個串列,這樣的大小比例就是前面說的 了。
這樣做的原因就是因為 大小的資料剛好可以放入 L1 Cache 裡,使其很好的避免掉了 Cache Thrashing 的問題。
講完了如何合併後,就來討論何時合併,利用一個計數器 count
來做判斷:
count + 1
後為 就不合併count + 1
後不為 就合併我們透過程式碼看實際如何操作
先將上面 do_while 迴圈分成三大部份
count
為奇數時執行count+1
不為 時合併pending
以下以這三點繪製流程圖,假設有一個鍊結串列有四個節點[4, 3, 2, 1]:
流程圖提示
prev
和 next
若是沒有箭頭,代表指向 NULL
prev
或 next
的箭頭,不要直接刪掉,會導致方塊上下不整齊,可以利用在後面添加 [color="white"]
使其變白色list_sort
後的初始動作合併前,a
和 b
為要合併的部份
合併後
執行19行後
list
為 NULL ,離開 do_while 迴圈在上面的流程可以看到,我們已經將所有節點從 input list 加到 pending list 之中,因此要將 pending list 合併。
最後,合併最後的 pending list 之後,將鍊結串列的頭尾接回來。