# Linux 核心專題: `lib/sort.c` > 執行人: kart81604 > [專題解說錄影](https://youtu.be/ECGinZ1t5U4) ## 任務描述 解釋 Linux 核心的 [lib/sort.c](https://github.com/torvalds/linux/blob/master/lib/sort.c) 現有的 bottom-up heap sort 實作,並尋求效能改進,例如 hybrid sort 的引入。 參照 [2021 年作業: sort](https://hackmd.io/@sysprog/linux2021-sort) 及對應[學員成果](https://hackmd.io/@sysprog/linux2021-homework5)。 ## TODO: 解釋現有 Linux 核心的 `lib/sort.c` 的實作 參見 [Bottom-up Heapsort](https://hackmd.io/@Uduru0522/bottomup-heapsort-analyze),對照 git log 提及的論文,解釋其原理,包含數學推導。藉由 perf 一類的效能分析工具,探討 Linux 核心 `lib/sort.c` 實作的效益。 ### 〈[BOTTOM-UP-HEAPSORT](https://www.sciencedirect.com/science/article/pii/030439759390364Y)〉論文研讀 先來複習資料結構學到的 heap sort ,利用 [heap](https://en.wikipedia.org/wiki/Heap_(data_structure)) 的性質,資料量為 n 的陣列 `a`,先將此陣列利用 **reheap** (部分資料結構參考書將此稱為 `heapify` ) 調整為符合 min-heap (此論文都以 min-heap 來討論) 結構,再將 $a[1]$ 的值跟 $a[n]$ 交換,再對 $a[1]$ ~ $a[n-1]$ 進行 **reheap**。(論文內陣列從 $a[1]$ 開始存值,而不是 $a[0]$) :::info ***HEAPSORT(a)*** 1) /* heap creation phase */ 2) for ( **i** from **$\lfloor \frac{n}{2} \rfloor$** downto **1** ) begin 3) &emsp;***reheap*** **(n, i)** 4) end 6) /* select phase */ 7) for ( **m** from **n** downto **2** ) begin 8) &emsp;**interchange** **a[1]** and **a[m]** 9) &emsp;if **m** $\neq$ 2 then ***reheap*** **(m - 1, 1)** 10) end ::: :::info ***reheap(m, i)*** 1) if (**i** > **$\frac{m}{2}$**) exit 2) if (**i** < **$\frac{m}{2}$**) **MIN** = min( **a[i]**, **a[2 * i]**, **a[2 * i + 1]** ) with 2 comparisons 3) if (**i** = **$\frac{m}{2}$**) **MIN** = min( **a[i]**, **a[2 * i]** ) with 1 comparison 4) if ( **MIN** = **a[i]** ) exit 5) else if ( **MIN** = **a[2 * i]** ) begin 6) &emsp;**interchange** **a[i]** and **a[2 * i]** 7) &emsp;***reheap*** **(m, 2 * i)** 8) end 9) else if ( **MIN** = **a[2 * i + 1]** ) begin 10) &emsp;**interchange** **a[i]** and **a[2 * i + 1]** 11) &emsp;***reheap*** **(m, 2 * i + 1)** 12) end ::: 在進行 **reheap** 時,第一個參數 $m$,總是會放該陣列的最後一筆資料,因 heap 為 complete binary tree ,因此 $a[\frac{m}{2}]$ 指的是 $a[m]$ 的 parent 。找出 `i` 以及 `i` 的 child 之中最小的為 `MIN` ,如果 `MIN` 就是 $a[i]$ ,則終止,反之將最小的那 index 設為新 `i` ,重複這個做法,直到第 1 行或是第 5 行的中止條件成立。是由上而下的進行 interchange ,來將樹調整成 min-heap 。 考慮 heap 高度為 d (root 所在的高度為 0),上述的 **reheap** 最多會執行 $2 \times d$ 次的 comparisons ,以及 d 次的 interchange ,然而,因為在執行 **heap-sort** 時,每一輪會把 heap 中最後一個值 $a[m]$ (考慮儲存最後一個值的 index 為 $m$) 放到 root ,在進行 **reheap** 會將 $a[m]$ 放置合適的位置,但這個位置會傾向靠近於 leaves,因此論文提到一個新的方法來找出代替 reheap 找出合適的位置,且有較少的 comparison 次數以及 interchange 次數。 搭配圖片解說,以下方的 heap 作為示範。($n = 13$,$n$ 為資料量),原文演算法的描述排版會做點修改,但執行順序以及方式不會做改變。 :::danger :warning: 陣列初始位置的 index 為 $1$ 而非 $0$ ::: ```graphviz graph heap{ 4 -- 7 4 -- 8 7 -- 23 7 -- 21 8 -- 14 8 -- 10 23 -- 31 23 -- 29 21 -- 24 21 -- 25 14 -- 15 14 -- 17 } ``` 因為要討論的 **reheap** 的執行方式,那我們就要先進行第一次的 **heapsort** ,也就是將最後一個位置的值與 root 的值交換,接著要對這棵樹進行 **reheap** 。(不會動到已排序好的值,此例為從 root 移到最後一個位置的 4) ```graphviz graph heap1{ 17 -- 7 17 -- 8 7 -- 23 7 -- 21 8 -- 14 8 -- 10 23 -- 31 23 -- 29 21 -- 24 21 -- 25 14 -- 15 sorted[label = 4, color = gray] 14 -- sorted } ``` 首先是 **leaf-search**,目的是要找出 **special leaf** , **special leaf** 為從 root 開始,找出目前節點中的 childs 較小的那個,再指向該點,直到指到最後一層,也就是這個 heap 的 leaf ,該點就是 **special leaf** 。 :::info ***leaf-search(m, i)*** 1) **j = i** 2) while ( **2 * j < m** ) begin 3) &emsp;if ( **a[2 * j]** < **a[2 * j + 1]** ) **j = 2 * j** 4) &emsp;else **j = 2 * j + 1** 5) end 6) if ( **2 * j = m** ) **j = m** 7) return **j** ::: 橘色節點的 $24$ 為 **special leaf** ,而走訪的路徑(紅色節點的部分與橘色節點)為 **special path** 。 下圖執行 ***leaf-search(12, 1)*** 會回傳 $10$,其為 **special leaf** 的 index 。 ```graphviz graph heap2{ 17[label = 17, color = red] 17 -- 7 17 -- 8 7[label = 7, color = red] 7 -- 23 21[label = 21, color = red] 7 -- 21 8 -- 14 8 -- 10 23 -- 31 23 -- 29 24[label = 24, color = orange] 21 -- 24 21 -- 25 14 -- 15 sorted[label = 4, color = gray] 14 -- sorted } ``` **bottom-up-search** 為找出應該放 $a[m]$ 的合適位置。 透過 **leaf-search** 找出 special leaf ,再由這個 leaf ,再拿 $a[m]$ 從這點往上比較,找出適合放入的位置,該位置稱為 **special object**。 :::info ***bottom-up-search(i, j)*** /* **j** is the output of ***leaf-search(m, i)*** */ 1) while ( **a[i] < a[j]** ) **j = $\lfloor \frac{j}{2} \rfloor$** 2) return **j** ::: 從 **special leaf** 往上找,直到找到比 root 還要小的值。(紫色節點) 下圖執行 ***bottom-up-search(1 , 10)*** 會回傳 $2$,其為 `reheap` 後, root 要換過去的位置。 ```graphviz graph heap3{ 7[label = 7, color = purple] 17 -- 7 17 -- 8 7 -- 23 7 -- 21 8 -- 14 8 -- 10 23 -- 31 23 -- 29 24[label = 24, color = orange] 21 -- 24 21 -- 25 14 -- 15 sorted[label = 4, color = gray] 14 -- sorted } ``` **interchange-1** 為 root 移至指定位置,且 root 到指定位置上的節點往上移動。 :::info ***interchange-1(i, j)*** /* **j** is the output of ***bottom-up-search(i, j)*** */ 1) **l** = **$\lfloor log(\frac{j}{i}) \rfloor$** 2) **x** = **a[i]** 3) for ( **k** from **l - 1** downto **0** ) **a[$\lfloor\frac{j}{2^{k+1}}\rfloor$] = a[$\lfloor \frac{j}{2^k} \rfloor$]** 4) **a[j]** = **x** ::: 下圖為執行 ***interchange-1(1, 2)*** 後的結果,滿足 heap 的性質。(不考慮最後一個已排序的節點) (root 移至 $a[2]$,原本 $a[2]$ 的值往其親代節點移動,因此移到 root) ```graphviz graph heap4{ 17[label = 17, color = purple] 7 -- 17 7 -- 8 17 -- 23 17 -- 21 8 -- 14 8 -- 10 23 -- 31 23 -- 29 24[label = 24, color = orange] 21 -- 24 21 -- 25 14 -- 15 sorted[label = 4, color = gray] 14 -- sorted } ``` 因此原本的 **reheap** 可以被修改成 bottom-up 形式,為 :::info ***bottom-up-reheap(m, i)*** 1) **j** = **leaf-search(m, i)** 2) **j** = **bottom-up-search(i, j)** 3) **interchange-1(i, j)** ::: 考慮任一個值任意排序的陣列(每個值都相異),對其做 **HEAPSORT** ,首先要先將陣列調整為 heap,需要做 **reheap($n, \lfloor \frac{n}{2} \rfloor$)**, **reheap($n, \lfloor \frac{n}{2} \rfloor-1$)**, ..., **reheap($n, 1$)**,因上述透過 bottom-up的方式改良,因此上述函式會呼叫 **leaf-search($n, \lfloor \frac{n}{2} \rfloor$)**、**leaf-search($n, \lfloor \frac{n}{2} \rfloor-1$)**、...、**leaf-search($n, 1$)**,而這些 **leaf-search** 最少會執行 $n - \lceil \log{(n+1)} \rceil - \lceil \log{(n)} \rceil + 1$ 次 comparisons,最多會執行 $n - 2$ 次 comparisons。(論文中的 lemma 4.1) :::spoiler 證明(點我展開) 先考慮 $n = 2^{k}-1$ 的情況,也就是 full binary tree (共 $k$ 層,第 $0$ 層至第 $k-1$ 層),第 $0$ 層的節點 (root) 要找到 **special leaf** 要進行 $k - 1$ 次的 comparisons,第 $1$ 層則是 $k - 2$ 次的 comparisons,第 $k - 2$ 層則是 $1$ 次,因為第 $i$ 層有 $2^i$ 個節點,因此總共會進行 $$ \begin{split}\sum_{i = 0}^{k-2}2^i(k-1-i) & = 2^0(k-1)+2^1(k-2)+...+2^{k-2}(1) \\ & =(2^0+2^1+...+2^{k-2})+(2^0+2^1+...+2^{k-3})+...+(2^0+2^1)+2^0 \\ & =(2^{k-1}-1)+(2^{k-2}-1)+...+(2^2-1)+(2^1-1) \\ & = (2^{k-1}+2^{k-2}+...+2^1)-(k-1)\times 1 \\ & = (2^{k-1}+2^{k-2}+...+2^1+2^0)-2^0-(k-1)\times 1 \\ & =(2^k-1)-k \\ & =n - \lceil\log{(n+1)} \rceil \end{split} $$ 次的 comparisons。 接著考慮不為 full binary tree 的情況,在第 $k$ 層加入 $i$ 個節點。 $i$ 為偶數,才會增加 worst-case 下 comparison 次數,也就是說 $i=0$ 與 $i=1$ 在 worst-case 增加comparison 次數是一樣的,$i=2$ 與 $i=3$ 在 worst-case 增加的 comparison次數是一樣的,...,以此類推,在第 $k$ 層增加 $i$ 個節點,會影響第 $k-1$ 層中的前 $\lceil\frac{(i-1)}{2}\rceil$ 個節點,在 worst-case 下,他們會多做一次的 comparison,而第 $k-2$ 層中前 $\lceil\frac{(i-1)}{2^2}\rceil$ 個節點也要多做一次的 comparison,...,第 $0$ 層則會有 $\lceil\frac{i-1}{2^{k}}\rceil$ 個節點要多做一次 comparison ,因此共要多做 $\begin{split}\sum_{j=1}^{k}\lceil\frac{(i-1)}{2^j} \rceil\end{split}$ 次。 $\begin{split}\sum_{j=1}^{k}\lceil\frac{(i-1)}{2^j} \rceil\end{split}$ 的 upper bound 為 $i-2+k$,證明如下: $$ \begin{split}\sum_{j=1}^{k}\lceil\frac{(i-1)}{2^j} \rceil & = \sum_{j=1}^{k}\frac{(i-1)}{2^j}+(\sum_{j=1}^{k}\lceil\frac{(i-1)}{2^j} \rceil-\sum_{j=1}^{k}\frac{(i-1)}{2^j})\\ &\leq(i-1)(1-\frac{1}{2^k})+\sum_{j=1}^{k}(1-\frac{1}{2^j}) \\ & =i-1-\frac{i}{2^k}+\frac{1}{2^k}+k-(1-\frac{1}{2^k})\\ & =i-2+k+\frac{2-i}{2^k}\\ & \leq i-2+k \ \ \ (\text{for}\ i \geq 2) \end{split} $$ 因為 $i<2$ 不會增加 comparison 次數,所以只考慮 $i\geq 2$ 的情況。 因此對於非 full binary tree 來說, worst-case 下,comparison 最多會做 $\begin{split}\underbrace{(2^k-1-k)}_{\text{full binary tree}}+(i-2+k) &= (2^k-1+i)-2 =n-2 \ 次。\end{split}$ 對於非 full binary tree 的 best-case ,就是 root 到第 $n$ 個節點的路徑上的所有點,在進行 comparison 時,都指向右子節點,遠離第 $n$ 個節點,這樣這些點進行 **leaf-search** 都會少一個 comparison ,而這路徑上的點(不包含 leaf)共有 $\lceil\log n\rceil-1$ 個,因此 best-case 下,comparison的次數至少會是 $n-\lceil\log(n+1)\rceil-(\lceil\log n\rceil - 1)=n-\lceil\log(n+1)\rceil-\lceil\log n\rceil + 1$ 次。 best-case 發生在 $n = 2$。 ::: \ 雖然分析出 best-case 與 worst-case 情況下的 comparison 次數,但實際上在調整至 heap 的過程,不管是在 best-case 還是 worst-case ,comparison 次數其實是差不多的,平均 comparison 次數就還是以 $n+\Theta(\log n)$ 考慮。 接著來討論 **bottom-up-search** 在陣列調整至 heap 時的執行次數,考慮透過 **leaf-search** 時找出的 **special path** ,將這個 path(不包含 root ) 用 $b(1),b(2),...b(d)$ 表示,$b(0) = -\infty,b(d+1) = \infty$ , $x$ 為要執行 **reheap** 的 root, 目的就是要找出滿足 $b(j)<x<b(j+1)$ 的 $j$,接著 **HEAPSORT** 以及 **bottom-up-search** 的 comparison 次數為以下表格。 | | $j<d$ | $j=d$ | |:--------------------:| :------: |:-----:| | **HEAPSORT** | $2(j+1)$ | $2d$ | | **bottom-up-search** | $d-j+1$ | $d$ | :::info **HEAPSORT** 每一層在做 comparison 時,會做 $2$ 次比較,因此要乘上 $2$ 。 ::: 用 $l_{\text{HS}}$ 表示 **HEAPSORT** 的 comparison 次數的==一半==,以及 $l_{\text{BUS}}$ 表示 **bottom-up-search** 的 comparison 次數。 再藉由上述表格,可以得到以下結論 : | | $l_{\text{HS}}+l_{\text{BUS}}$ | |:---------------------:|:------------------------------:| | $0<j<d$ | $d+2$ | | $j=0 \text{ or } j=d$ | $d+1$ | 接著透過〈[An average case analysis of Floyd’s algorithm to construct heaps](https://www.sciencedirect.com/science/article/pii/S0019995884800534)〉的結論,**HEAPSORT** 平均 comparison 次數為 $(\alpha_1+2\alpha_2-2)n+\Theta(\log n)$ 次,以及平均 interchange 次數為 $(\alpha_1+\alpha_2-2)n+\Theta(\log n)$ 次,其中 $\alpha_1=1.6066951...,\alpha_2=1.1373387...,$ $\alpha_i$ 的公式為 $\begin{split}\alpha_i=\sum_{j=1}^{\infty}(2^j-1)^{-i}\end{split}$ 。(論文中的 Thm 4.2) 因為在調整 heap 的過程中, **bottom-up-reheap** 會呼叫 $\lfloor\frac{n}{2}\rfloor$ 次,因此其中的 **bottom-up-search** 也同樣會執行 $\lfloor\frac{n}{2}\rfloor$ 次,以 $L_{\text{BUS}},L_{\text{HS}},D$ 作為隨機變數,分別代表的是 $l_{\text{BUS}},l_{\text{HS}},d$ 的總和,目標是要求出 $E(L_{\text{BUS}})$ ,也就是在調整至 heap 的過程,執行 **bottom-up-search** 的次數的期望值。 藉由上述的 lemma 4.1 跟 Thm 4.2 ,可以得出 $$ E(D)=n+\Theta(\log n)、E(L_{\text{HS}})=(\alpha_1/2+\alpha_2-1)n+\Theta(\log n) $$ 用 $T$ 來表示呼叫 **bottom-up-search** 且 $l_{\text{HS}}+l_{\text{BUS}}=d+1$ 的次數,則 $\lfloor\frac{n}{2}\rfloor-T$ 則為呼叫 **bottom-up-search** 且 $l_{\text{HS}}+l_{\text{BUS}}=d+2$ 的次數。 因此 $$ \begin{split} E(L_{\text{BUS}}) &= E(T)\cdot(E(d)+1-E({l_{\text{HS}}}))+(\lfloor\frac{n}{2}\rfloor-E(T))\cdot(E(d)+2-E(l_{\text{HS}})) \\ &=\lfloor\frac{n}{2}\rfloor E(d)+2\lfloor\frac{n}{2}\rfloor-\lfloor\frac{n}{2}\rfloor E(l_{\text{HS}})-E(T) \\ &=E(D)+2\lfloor\frac{n}{2}\rfloor-E(L_{\text{HS}})-E(T) \\ &=n+2\lfloor\frac{n}{2}\rfloor-(\alpha_1/2+\alpha_2-1)n-E(T)+\Theta(\log n) \\ &=(3-\alpha_1/2-\alpha_2)n-E(T)+\Theta(\log n) \end{split} $$ 處理 $E(L_{\text{BUS}})$ 中的 $E(T)$,考慮 $j=0$ 的情況,也就是 **special object** 恰巧就為該子樹的 root ,如果該子樹有 r 個節點,則發生 $j=0$ 的機率為 $\frac{1}{j}$,考慮 full binary tree ,總高度為 $h$ ,在允許 $\Theta(\log n)$ 的誤差,第 $0$ 層有 $1$ 個子樹且該樹的節點為 $n$ 個,第 $1$ 層有 $2$ 個子樹且該樹的節點大致為 $\frac{n}{2}$ 個,...,第 $h-2$ 層有 $2^{h-2}$ 個子樹且該樹節點大致為 $\frac{2}{2^{h-2}}$,因此 $j=0$ 的狀況發生在各個非 leaf 的節點上的機率為 $$ \begin{split} \beta=\sum_{h=2}^{\infty}[2^h(2^h-1)]^{-1} = 0.1066952 \end{split} $$ 因此發生在 $j=0$ 的情況下的期望值為 $\beta n+\Theta(\log n)$ 。 接著是 $j=d$ 的情況,在處理前,先說明一個等式,$c$ 為在執行 **reheap** 時的 comparison 次數,$i$ 為 interchange 次數,$s$ 為 **special object** 的子代節點的個數,則以下等式成立 : $c=2\times i+s$ 考慮 $C、I、S$ 分別為 $c、i、s$ 在調整 heap 的總和,由上述等式可得 : $$ E(C)=2\times E(I)+E(S) \Rightarrow E(S)=E(C)-2\times E(I) $$ 因為二元樹的關係,$s$ 只有三個可能性,也就是 $s\in \{0,1,2\}$,$s=1$ 的情況,也就是 **special object** 為第 $\frac{n}{2}$ 個節點,且 $n$ 為偶數,這情況最多只會發生 $\log n$ 次( root 到最後一個節點這路徑上的節點才有可能會發生),當 $n$ 很大時, $s=1$ 發生次數少到可以不用考慮,所以我們可以改成 $s\in \{0,2\}$,設 $p_{s=0}、p_{s=2}$ 分別為 $s=0, s=2$ 發生的機率,可得 $$ \begin{align} &E(s)=0\times p_{s=0}+2\times p_{s=2}\\ \Rightarrow &E(s)=2\times p_{s=2}\\ \Rightarrow &\lfloor\frac{n}{2}\rfloor E(s)=\lfloor\frac{n}{2}\rfloor\times 2\times p_{s=2} \\ \Rightarrow &E(S)=\lfloor\frac{n}{2}\rfloor\times 2\times p_{s=2}\\ \Rightarrow &p_{s=2}=(E(S)/\lfloor\frac{n}{2}\rfloor)/2=2-\alpha_1+\Theta(n^{-1}\log n) \end{align} $$ 則 $s=0$ 就能求出, $p_{s=0}=1-p_{s=2}=\alpha_1-1+\Theta(n^{-1}\log n)。$ 再回來看要處理 $j=d$ 的情況,也就是 **special object** 為 leaf 的情況,剛好就是 $s=0$ 的意思,因此發生在 $j=d$ 的情況下的期望值為 $$ \lfloor\frac{n}{2}\rfloor p_{s=0}=\lfloor\frac{n}{2}\rfloor(\alpha_1-1+\Theta(n^{-1}\log n))=(\alpha_{1}/2-1/2)n+\Theta(\log n) $$ 求出 $j=0、j=d$ 的期望值後,$E(T)$ 為這兩者之和,因此 $E(T)=(\alpha_1/2-1/2+\beta)n+\Theta(\log n)$,帶回 $E(L_{\text{BUS }})$ 中,可得 : $$ \begin{split}E(L_{\text{BUS}})&=(3-\alpha_1/2-\alpha_2)n-(\alpha_1/2-1/2+\beta)n+\Theta(\log n)\\ &=(7/2-\alpha_1-\alpha_2+\beta)n+\Theta(\log n)\end{split} $$ 因此得到以下結論,在進行 **bottom-up-HEAPSORT** 一開始 heap creation phase 時 comparison 平均花費次數為 $$ \begin{split} &\underbrace{n+\Theta(\log n)}_{\text{leaf-search}}+\underbrace{(7/2-\alpha_1-\alpha_2+\beta)n+\Theta(\log n)}_{\text{bottom-up-search}} \\ = &(9/2-\alpha_1-\alpha_2+\beta)n+\Theta(\log n)\\ \approx &1.649271n \end{split} $$ 接著分析 select phase 的 comparison ,會進行 **leaf-search(n-1,1)、leaf-search(n-2,1)、...、leaf-search(2,1)** ,在執行 **leaf-search(i,1)** 的 worst-case 下,共會執行 $\lfloor\log(i-1)\rfloor$ 次的 comparisons,worst-case 下, 全部 **leaf-search** 的 comparisons 次數為 $$ \begin{split} \sum_{i=2}^{n-1}\lfloor\log(i-1) \rfloor &= \sum_{i=1}^{n-2}\lfloor\log i\rfloor \\ &=\sum_{i=1}^{\lfloor\log(n-2)\rfloor-1}i2^i+\lfloor\log(n-2)\rfloor(n-2^{\lfloor\log(n-2)\rfloor}-1)\\ &= n\lfloor\log (n-2)\rfloor-2\cdot2^{\lfloor\log(n-2)\rfloor}-\lfloor\log(n-2)\rfloor+2 \end{split} $$ 將以上調整至 $n\log n-c(n)n$ 的形式,則 $$ c(n)=(\log n-\lfloor\log(n-2)\rfloor)+2\cdot2^{\lfloor\log(n-2)\rfloor}/n $$ $c(n)$ 在 $n\approx 1.386\cdot 2^k$ 時為最大值,此時 $c(n)\approx 1.91393$ 。 worst-case 下,**leaf-search** 會執行 $n\log n-1.91393n$ 次 comparisons。但這是在 worst-case 下,並不是每次找到的 **special path** 總會是最長的,也是有可能找到較短的 **special path**,其長度為 $\lfloor\log (n-1)\rfloor-1$ ,在論文的實驗下,在 $n\approx1.4\cdot2^k$ 約會有 $0.519$ 的機率找到的 **special path** 不是最長的。綜上所述,**leaf search** 平均下會執行 $n\log n -1.91393n-0.519n=n\log n -2.43293n$ 次 comparisons。 **bottom-up-search** 也會做 comparison,因為每次 **bottom-up-search** 至少會做一次的 comparison,且大部分作的次數都會很少(因為在做 heap sort 要將最後一個值 x 與 root 交換,再使 x 下沉至 x 應該在的位子,因 x 為 heap 的葉子,預期其值會相當大,因此 x 新的位子也會相當靠近最後一層),論文實驗分析,會執行 $1.17n$ 次的 comparisons。 那執行整體 **bottom-up-HEAPSORT** 預計共會花 $$ \begin{split} \underbrace{1.649271n}_{\text{heap creation phase}}+\underbrace{n\log n-2.43293n}_{\text{select phase leaf-search}}+\underbrace{1.17n}_{\text{select phase bottom-up-search}}=n\log n+0.386341n \end{split} $$ 而論文中則是用猜想的方式,以 $n\log n+d(n)n$ 為執行 comparison 次數之期望值,來進行實驗,得到以下結果(部分節錄) | n | 1000 | 2000 | 3000 | 4000 | ... | 27000 | 28000 | 29000 | 30000 | | ------ | ----- |:-----:|:-----:|:-----:|:---:|:-----:|:-----:|:-----:|:-----:| | $d(n)$ | 0.345 | 0.349 | 0.383 | 0.358 | ... | 0.381 | 0.378 | 0.373 | 0.371 | 透過實驗結果,這可以解釋 [lib/sort.c](https://github.com/torvalds/linux/blob/master/lib/sort.c) 中,開頭的 > This performs n\*log2(n) + 0.37\*n + o(n) comparisons on average 考慮 $o(n)$ 為容許的誤差,實驗結果也與分析出來的 $n\log n+0.386341n$ 相當接近。 > and 1.5\*n\*log2(n) + O(n) in the (very contrived) worst case. 也可從論文中 Thm 5.1 中得出。 ### 建立相關實驗環境 [commit f8f230a](https://github.com/kart81604/linux2023_final/commit/f8f230acbd6f8b8ef794407fd806ceb0084677ac) 目前建立一個陽春簡單的 driver 可以使用 linux kernel 中的 sort 排序,以及建立 client 的 user space 來讀取相關資料。 [commit 0fcbe0f](https://github.com/kart81604/linux2023_final/commit/0fcbe0f6521bb678dbc305c90b4362c4d16f7a2c) 考慮原本未排序資料過於固定,會影響實驗結果,參考 [2021年作業-ksort](https://hackmd.io/@sysprog/linux2021-sort) 後,引入 [xoroshiro128+](https://en.wikipedia.org/wiki/Xoroshiro128%2B) 這個 [Pseudorandom number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator),以下為引入後的時間表現圖。 ![](https://hackmd.io/_uploads/Hk4He2Tvn.png) 利用 perf stat 來查看資源利用。 ``` Performance counter stats for './client': 9,396.16 msec task-clock # 1.000 CPUs utilized 40 context-switches # 4.257 /sec 1 cpu-migrations # 0.106 /sec 60 page-faults # 6.386 /sec 31,746,894,265 cycles # 3.379 GHz 23,920,611,236 instructions # 0.75 insn per cycle 3,855,864,140 branches # 410.366 M/sec 658,348,492 branch-misses # 17.07% of all branches 9.396961274 seconds time elapsed 0.011975000 seconds user 9.364692000 seconds sys ``` 利用 perf record 來查看熱點(僅列出與 sort 有關的函式)。 ``` 49.76% client [kernel.kallsyms] [k] cmpint64 37.61% client [kernel.kallsyms] [k] sort_heap 10.00% client [kernel.kallsyms] [k] do_swap 0.88% client [kernel.kallsyms] [k] sort_read ``` 不意外的,花最多時間 comparison 上。 [commit 2751912](https://github.com/kart81604/linux2023_final/commit/27519128b5b7302f359e853cb5bf57210fe1bca8) 將每次執行 comparison 次數與論文中的 $n\log(n)+0.37n$ 做比較,結果兩者高度接近! ![](https://hackmd.io/_uploads/BJu2ptSO3.png) ## TODO: 參照[學員成果](https://hackmd.io/@sysprog/linux2021-homework5),引入 hybrid sort 需要強化排序測試和效能評估,程式碼應實作於 Linux 核心中 延伸閱讀: (涉及排序演算法和現代處理器的關聯) * [Hoare’s Rebuttal and Bubble Sort’s Comeback](https://blog.reverberate.org/2020/05/29/hoares-rebuttal-bubble-sorts-comeback.html) * [Lomuto’s Comeback](https://dlang.org/blog/2020/05/14/lomutos-comeback/) [commit fd5f87b](https://github.com/kart81604/linux2023_final/commit/fd5f87b9a437d035e5cd403064ca999fd0cd070a) 參考 [hankluo6/ksort](https://github.com/hankluo6/ksort) 的成果,引入 introsort。 透過 perf stat 來查看 introsort 的資源利用。 ``` Performance counter stats for './client': 6,553.87 msec task-clock # 0.999 CPUs utilized 223 context-switches # 34.026 /sec 3 cpu-migrations # 0.458 /sec 60 page-faults # 9.155 /sec 22,107,043,269 cycles # 3.373 GHz 13,762,687,088 instructions # 0.62 insn per cycle 2,719,459,349 branches # 414.939 M/sec 617,936,408 branch-misses # 22.72% of all branches 6.559289342 seconds time elapsed 0.011913000 seconds user 6.492720000 seconds sys ``` 以及 perf record (僅列出與 sort 有關的函式)。 ``` 63.88% client [kernel.kallsyms] [k] cmpint64 32.18% client [kernel.kallsyms] [k] sort_intro 1.35% client [kernel.kallsyms] [k] sort_read ``` 並與 bottom-up heapsort 比較,這次紀錄方法使用 `printk` 印出兩者的執行時間,再透過 `dmesg` 將資料印出,接著做成圖表。因為 dmesg 的 ring buffer 為 8000+ 個,因此資料量從 10000 調整至 8000。比較成果如下圖所示。 ```shell sudo dmesg -t > ttest.txt ``` 如果 dmesg 已經有其他的系統訊息,可以先用以下命令清除。 ```shell sudo dmesg -C ``` ![](https://hackmd.io/_uploads/Syq4XoRw2.png)