lib/sort.c
執行人: kart81604
專題解說錄影
解釋 Linux 核心的 lib/sort.c 現有的 bottom-up heap sort 實作,並尋求效能改進,例如 hybrid sort 的引入。
參照 2021 年作業: sort 及對應學員成果。
lib/sort.c
的實作參見 Bottom-up Heapsort,對照 git log 提及的論文,解釋其原理,包含數學推導。藉由 perf 一類的效能分析工具,探討 Linux 核心 lib/sort.c
實作的效益。
先來複習資料結構學到的 heap sort ,利用 heap 的性質,資料量為 n 的陣列 a
,先將此陣列利用 reheap (部分資料結構參考書將此稱為 heapify
) 調整為符合 min-heap (此論文都以 min-heap 來討論) 結構,再將 的值跟 交換,再對 ~ 進行 reheap。(論文內陣列從 開始存值,而不是 )
HEAPSORT(a)
reheap(m, i)
在進行 reheap 時,第一個參數 ,總是會放該陣列的最後一筆資料,因 heap 為 complete binary tree ,因此 指的是 的 parent 。找出 i
以及 i
的 child 之中最小的為 MIN
,如果 MIN
就是 ,則終止,反之將最小的那 index 設為新 i
,重複這個做法,直到第 1 行或是第 5 行的中止條件成立。是由上而下的進行 interchange ,來將樹調整成 min-heap 。
考慮 heap 高度為 d (root 所在的高度為 0),上述的 reheap 最多會執行 次的 comparisons ,以及 d 次的 interchange ,然而,因為在執行 heap-sort 時,每一輪會把 heap 中最後一個值 (考慮儲存最後一個值的 index 為 ) 放到 root ,在進行 reheap 會將 放置合適的位置,但這個位置會傾向靠近於 leaves,因此論文提到一個新的方法來找出代替 reheap 找出合適的位置,且有較少的 comparison 次數以及 interchange 次數。
搭配圖片解說,以下方的 heap 作為示範。(, 為資料量),原文演算法的描述排版會做點修改,但執行順序以及方式不會做改變。
因為要討論的 reheap 的執行方式,那我們就要先進行第一次的 heapsort ,也就是將最後一個位置的值與 root 的值交換,接著要對這棵樹進行 reheap 。(不會動到已排序好的值,此例為從 root 移到最後一個位置的 4)
首先是 leaf-search,目的是要找出 special leaf , special leaf 為從 root 開始,找出目前節點中的 childs 較小的那個,再指向該點,直到指到最後一層,也就是這個 heap 的 leaf ,該點就是 special leaf 。
leaf-search(m, i)
橘色節點的 為 special leaf ,而走訪的路徑(紅色節點的部分與橘色節點)為 special path 。
下圖執行 leaf-search(12, 1) 會回傳 ,其為 special leaf 的 index 。
bottom-up-search 為找出應該放 的合適位置。
透過 leaf-search 找出 special leaf ,再由這個 leaf ,再拿 從這點往上比較,找出適合放入的位置,該位置稱為 special object。
bottom-up-search(i, j)
/* j is the output of leaf-search(m, i) */
從 special leaf 往上找,直到找到比 root 還要小的值。(紫色節點)
下圖執行 bottom-up-search(1 , 10) 會回傳 ,其為 reheap
後, root 要換過去的位置。
interchange-1 為 root 移至指定位置,且 root 到指定位置上的節點往上移動。
interchange-1(i, j)
/* j is the output of bottom-up-search(i, j) */
下圖為執行 interchange-1(1, 2) 後的結果,滿足 heap 的性質。(不考慮最後一個已排序的節點)
(root 移至 ,原本 的值往其親代節點移動,因此移到 root)
因此原本的 reheap 可以被修改成 bottom-up 形式,為
bottom-up-reheap(m, i)
考慮任一個值任意排序的陣列(每個值都相異),對其做 HEAPSORT ,首先要先將陣列調整為 heap,需要做 reheap(), reheap(), …, reheap(),因上述透過 bottom-up的方式改良,因此上述函式會呼叫 leaf-search()、leaf-search()、…、leaf-search(),而這些 leaf-search 最少會執行 次 comparisons,最多會執行 次 comparisons。(論文中的 lemma 4.1)
先考慮 的情況,也就是 full binary tree (共 層,第 層至第 層),第 層的節點 (root) 要找到 special leaf 要進行 次的 comparisons,第 層則是 次的 comparisons,第 層則是 次,因為第 層有 個節點,因此總共會進行
次的 comparisons。
接著考慮不為 full binary tree 的情況,在第 層加入 個節點。 為偶數,才會增加 worst-case 下 comparison 次數,也就是說 與 在 worst-case 增加comparison 次數是一樣的, 與 在 worst-case 增加的 comparison次數是一樣的,…,以此類推,在第 層增加 個節點,會影響第 層中的前 個節點,在 worst-case 下,他們會多做一次的 comparison,而第 層中前 個節點也要多做一次的 comparison,…,第 層則會有 個節點要多做一次 comparison ,因此共要多做 次。
的 upper bound 為 ,證明如下:
因為 不會增加 comparison 次數,所以只考慮 的情況。
因此對於非 full binary tree 來說, worst-case 下,comparison 最多會做
對於非 full binary tree 的 best-case ,就是 root 到第 個節點的路徑上的所有點,在進行 comparison 時,都指向右子節點,遠離第 個節點,這樣這些點進行 leaf-search 都會少一個 comparison ,而這路徑上的點(不包含 leaf)共有 個,因此 best-case 下,comparison的次數至少會是
次。
best-case 發生在 。
雖然分析出 best-case 與 worst-case 情況下的 comparison 次數,但實際上在調整至 heap 的過程,不管是在 best-case 還是 worst-case ,comparison 次數其實是差不多的,平均 comparison 次數就還是以 考慮。
接著來討論 bottom-up-search 在陣列調整至 heap 時的執行次數,考慮透過 leaf-search 時找出的 special path ,將這個 path(不包含 root ) 用 表示, , 為要執行 reheap 的 root, 目的就是要找出滿足 的 ,接著 HEAPSORT 以及 bottom-up-search 的 comparison 次數為以下表格。
HEAPSORT | ||
bottom-up-search |
HEAPSORT 每一層在做 comparison 時,會做 次比較,因此要乘上 。
用 表示 HEAPSORT 的 comparison 次數的一半,以及 表示 bottom-up-search 的 comparison 次數。
再藉由上述表格,可以得到以下結論 :
接著透過〈An average case analysis of Floyd’s algorithm to construct heaps〉的結論,HEAPSORT 平均 comparison 次數為 次,以及平均 interchange 次數為 次,其中
的公式為 。(論文中的 Thm 4.2)
因為在調整 heap 的過程中, bottom-up-reheap 會呼叫 次,因此其中的 bottom-up-search 也同樣會執行 次,以 作為隨機變數,分別代表的是 的總和,目標是要求出 ,也就是在調整至 heap 的過程,執行 bottom-up-search 的次數的期望值。
藉由上述的 lemma 4.1 跟 Thm 4.2 ,可以得出
用 來表示呼叫 bottom-up-search 且 的次數,則 則為呼叫 bottom-up-search 且 的次數。
因此
處理 中的 ,考慮 的情況,也就是 special object 恰巧就為該子樹的 root ,如果該子樹有 r 個節點,則發生 的機率為 ,考慮 full binary tree ,總高度為 ,在允許 的誤差,第 層有 個子樹且該樹的節點為 個,第 層有 個子樹且該樹的節點大致為 個,…,第 層有 個子樹且該樹節點大致為 ,因此 的狀況發生在各個非 leaf 的節點上的機率為
因此發生在 的情況下的期望值為 。
接著是 的情況,在處理前,先說明一個等式, 為在執行 reheap 時的 comparison 次數, 為 interchange 次數, 為 special object 的子代節點的個數,則以下等式成立 :
考慮 分別為 在調整 heap 的總和,由上述等式可得 :
因為二元樹的關係, 只有三個可能性,也就是 , 的情況,也就是 special object 為第 個節點,且 為偶數,這情況最多只會發生 次( root 到最後一個節點這路徑上的節點才有可能會發生),當 很大時, 發生次數少到可以不用考慮,所以我們可以改成 ,設 分別為 發生的機率,可得
則 就能求出,
再回來看要處理 的情況,也就是 special object 為 leaf 的情況,剛好就是 的意思,因此發生在 的情況下的期望值為
求出 的期望值後, 為這兩者之和,因此
,帶回 中,可得 :
因此得到以下結論,在進行 bottom-up-HEAPSORT 一開始 heap creation phase 時 comparison 平均花費次數為
接著分析 select phase 的 comparison ,會進行 leaf-search(n-1,1)、leaf-search(n-2,1)、…、leaf-search(2,1) ,在執行 leaf-search(i,1) 的 worst-case 下,共會執行 次的 comparisons,worst-case 下, 全部 leaf-search 的 comparisons 次數為
將以上調整至 的形式,則
在 時為最大值,此時 。 worst-case 下,leaf-search 會執行 次 comparisons。但這是在 worst-case 下,並不是每次找到的 special path 總會是最長的,也是有可能找到較短的 special path,其長度為 ,在論文的實驗下,在 約會有 的機率找到的 special path 不是最長的。綜上所述,leaf search 平均下會執行 次 comparisons。
bottom-up-search 也會做 comparison,因為每次 bottom-up-search 至少會做一次的 comparison,且大部分作的次數都會很少(因為在做 heap sort 要將最後一個值 x 與 root 交換,再使 x 下沉至 x 應該在的位子,因 x 為 heap 的葉子,預期其值會相當大,因此 x 新的位子也會相當靠近最後一層),論文實驗分析,會執行 次的 comparisons。
那執行整體 bottom-up-HEAPSORT 預計共會花
而論文中則是用猜想的方式,以 為執行 comparison 次數之期望值,來進行實驗,得到以下結果(部分節錄)
n | 1000 | 2000 | 3000 | 4000 | … | 27000 | 28000 | 29000 | 30000 |
---|---|---|---|---|---|---|---|---|---|
0.345 | 0.349 | 0.383 | 0.358 | … | 0.381 | 0.378 | 0.373 | 0.371 |
透過實驗結果,這可以解釋 lib/sort.c 中,開頭的
This performs n*log2(n) + 0.37*n + o(n) comparisons on average
考慮 為容許的誤差,實驗結果也與分析出來的 相當接近。
and 1.5*n*log2(n) + O(n) in the (very contrived) worst case.
也可從論文中 Thm 5.1 中得出。
commit f8f230a
目前建立一個陽春簡單的 driver 可以使用 linux kernel 中的 sort 排序,以及建立 client 的 user space 來讀取相關資料。
commit 0fcbe0f
考慮原本未排序資料過於固定,會影響實驗結果,參考 2021年作業-ksort 後,引入 xoroshiro128+ 這個 Pseudorandom number generator,以下為引入後的時間表現圖。
利用 perf stat 來查看資源利用。
利用 perf record 來查看熱點(僅列出與 sort 有關的函式)。
不意外的,花最多時間 comparison 上。
將每次執行 comparison 次數與論文中的 做比較,結果兩者高度接近!
需要強化排序測試和效能評估,程式碼應實作於 Linux 核心中
延伸閱讀: (涉及排序演算法和現代處理器的關聯)
commit fd5f87b
參考 hankluo6/ksort 的成果,引入 introsort。
透過 perf stat 來查看 introsort 的資源利用。
以及 perf record (僅列出與 sort 有關的函式)。
並與 bottom-up heapsort 比較,這次紀錄方法使用 printk
印出兩者的執行時間,再透過 dmesg
將資料印出,接著做成圖表。因為 dmesg 的 ring buffer 為 8000+ 個,因此資料量從 10000 調整至 8000。比較成果如下圖所示。
如果 dmesg 已經有其他的系統訊息,可以先用以下命令清除。