# 2021q1 Homework5 (sort) contributed by < `ccs100203` > ###### tags: `linux2021` > [sort](https://hackmd.io/@sysprog/linux2021-sort) > [@Uduru0522: Note of Bottom-up Heapsort (English Version)](https://hackmd.io/@Uduru0522/bottomup-heapsort-analyze) ## [sort.c](https://github.com/torvalds/linux/blob/master/lib/sort.c) 分析 > A fast, small, non-recursive O(n log n) sort for the Linux kernel 這是一個建立在連續記憶體上的 heapsort 實作。 而 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 是不連續記憶體上的排序實作。 ## 時間複雜度 翻完演算法講義還是不太了解 sort.c 算出 $n*log2(n) + 0.37*n + o(n)$ 的方法 ## 程式碼分析 ### Swap - line 50: Exchange the two objects in memory. This exploits base+index addressing, which basically all CPUs have, to minimize loop overhead computations. 不知道為什麼有這些優點 - aligned vs unaligned (覺得他的 swap 好厲害,第一次看到這種寫法。除了這裡也看到作者很多為了加速跟優化的寫法,教科書上的東西實際使用感到很驚豔) 在對齊的情況下,藉由指標的特性,可以一次交換 4 或 8 bytes。 在不對齊時,就用原本讀取 array 的方式逐 byte 去做交換。 (TODO 把說明寫的更詳細) ### Parent ```cpp= __attribute_const__ __always_inline static size_t parent(size_t i, unsigned int lsbit, size_t size) { i -= size; i -= size & -(i & lsbit); return i / 2; } ``` 此函式是為了找一個 node 的 parent。在一般的 heap 中,可以直接用 $(j-1)/2$ 得到 parent 的 index。但因為這裡的 `i` 並不是 index 而是 offset,所以不能直接用這種方式去處理。變成要判斷他是 left child 或 right child,藉此決定要減一次 `size` 或兩次 `size`,之後才進行除二。可以想成因為不能夠當成 index 那樣藉著整數除法的性質自動捨去小數,所以必須精確的減去正確數量的 `size` 才能夠做除二。 #### 用下面的圖例說明: ``` 0 | |-------| 1 2 | | |-----| |-----| 3 4 5 6 ``` - 如果只是一般的 index,只要 $(j-1)/2$ 就可以得到 parent - 因為這邊是 offset 所以需要透過額外的計算才能找到 parent - 先假設 size 是 4,如果要找 index 5 的 parent,那麼 $i = 5 * 4 = 20$, - `lsbit = size & -size`,代表 `size` 的 least set bit。 - 第五行會先做一次減 `size`,再來第六行就是要不要做第二次減 `size` 的關鍵。 如果此時的 `(i & lsbit)` 不為 0,代表此數字需要減第二次 `size` ,反之則不用。可以想像成 odd 跟 even 的概念,這邊目的是要讓他在基於 `size` 的情況下屬於偶數,那 `size` 的 least set bit 的要是 0 才能代表是偶數,因為偶數除二才不會有小數點或餘數的問題。 - 此時的 $i=16_{10}=010000_2$,而 $lsbit=4_{10}=000100_2$,交集為 0,所以在第六行 BITWISE AND 的右半邊就會是 0,這樣 `i` 就不會做任何的運算,最後回傳 `i` 除二就是 $offset = 16/2=8$,換算回 $index = 8/4 = 2$。 - 那如果 index 是 6,$i = 6*4 = 24$, 第一次減 `size` 會變成 $i=20_{10}=010100$,則 `i & lsbit` 會得到 4,這樣第六行就會變成 `i -= 4 & -4`,這樣就會得到 16,回傳後就得到了 parent 的 offset。 #### 順帶一提,上述的程式用更直覺的寫法會寫成: 可以馬上看出來是用來判斷是否需要再減去一次 size ```cpp i -= size; if (i & lsbit) i -= size; ``` 但是這有明顯缺陷,就是多了一個 branch 的 overhead,而作者利用 bitwise 將其省去了,這就是 bitwise 的魅力嗎 ### Sort > Sorting time is O(n log n) both on average and worst-case. While quicksort is slightly faster on average, it suffers from exploitable O(n*n) worst-case behavior and extra memory requirements that make it less suitable for kernel use. 一開始就寫明了使用 heapsort 而不是 quicksort 的原因,因為 $O(n*n)$ worst-case 是沒辦法被接受的。 作者利用 backtrack 的方法降低 compare 的次數,所以 heapsort 的方式不同於平時看到的 Top-down heapsort,而是 [Bottom-up heapsort](https://en.wikipedia.org/wiki/Heapsort#Bottom-up_heapsort)。 #### 先來解釋兩種 heapsort 的差別 這邊是一個 heapsort 的架構: (假設要從小排到大) **HEAPSORT(Array, n):** ``` // build max heap for i <- floor(n/2) to 0: siftdown(i, n) // swap & sort for i <- n downto root: swap(A[0], A[i]) siftdown(0, i–1) ``` - 第一個迴圈用來將 heap 建立為 [MAX HEAP](https://en.wikipedia.org/wiki/Min-max_heap),siftdown 是一個會不斷向 child 比較與交換的函式,後面會再解釋。 - 因為 MAX HEAP 的特性,藉由第二個迴圈會不斷的把 root 與當前的最後一個 node 交換,並利用 siftdown 維持其 MAX HEAP 的性質,就可以將此陣列由小到大排序完成。 **siftdown function:** 此函式的功能在於將傳進來的 node 與其 child 進行比較,將自身交換為三者 (假設有 left & reight child) 之中最大的值,並持續的向下交換以及比較。 而這個函式就是 Top-down 與 bottom-up 的差異所在,以下圖來說明: 假設我目前將 node 0 傳入函式 siftdown ``` 0 2 | | |-------| |-------| 1 2 ----> 1 6 | | | | |-----| |-----| |-----| |-----| 3 4 5 6 3 4 5 0 ``` - Top-down parent, left child, right child 三者會透過兩次的 compare 找出最大值 2,將其與 parent 0 交換,此時會進行下一輪比較,在數值 0, 5, 6 之間比較出最大的點,就可以得到右圖。 - Bottom-up 與 Top-down 的差別在於,每一輪的 compare 中只先比較兩個 child,找出比較大的 child 就會由他繼續往下比,一直到 leaf 不能繼續向下比較時,會將傳進來的 node 沿著剛剛往下的 path,從 leaf 開始一路向上比較回去,一直到 input_node < current_node,代表找到了傳進來的 node 要放置的新位置。此時將此位置紀錄為 new,就會進行 swap(new, parent(new)),再來會繼續進行 swap(new, parent(parent(new))) 一直進行到 new 與 input_node 進行交換過後結束。 在這裡會進行第一次的 compare 找出 $2 > 1$,然後進行第二次的 compare 找出 $6>5$,此時因為已經到達 leaf,所以要開始向上 backtrack,於是透過 $0<6$ 找到 0 所應該放置的點,就可以開始進行 swap(2, 6),以及 swap(2, 0),最後也會得到右圖。 - compare 次數的比較 可以發現兩者會得到相同的結果,但是 Top-down 在每一輪都需要兩次的 compare,而 bottom-up 則是藉由一輪一次的 compare 到達 leaf,再一路向上 compare,雖然如果每一步都需要比較到最後的話都會是兩次,但是需要交換的 element 是傾向於靠近 leaf 的,代表了 compare 次數會隨之減少。 >根據 [Bottom-up heapsort](https://en.wikipedia.org/wiki/Heapsort#Bottom-up_heapsort) 的敘述 在 Top-down 中的 worst & average case 為 $2nlog_2n + O(n)$ 而 bottom-up 的 average case 為 $nlog_2n+O(1)$,worst case 為 $1.5nlog_2n+O(n)$。 即便是在 worst case 都只有 Top-down 3/4 的比較次數,大大減少了 compare 的 overhead。 #### sort 的程式碼 以下是 sort.c 內的程式碼,每個部分都能夠與前面 heapsort 的敘述相呼應,藉此來做說明。 ```cpp= for (;;) { size_t b, c, d; if (a) /* Building heap: sift down --a */ a -= size; else if (n -= size) /* Sorting: Extract root to --n */ do_swap(base, base + n, size, swap_func); else /* Sort complete */ break; for (b = a; c = 2*b + size, (d = c + size) < n;) b = do_cmp(base + c, base + d, cmp_func, priv) >= 0 ? c : d; if (d == n) /* Special case last leaf with no sibling */ b = c; /* Now backtrack from "b" to the correct location for "a" */ while (b != a && do_cmp(base + a, base + b, cmp_func, priv) >= 0) b = parent(b, lsbit, size); c = b; /* Where "a" belongs */ while (b != a) { /* Shift it into place */ b = parent(b, lsbit, size); do_swap(base + b, base + c, size, swap_func); } } ``` - 這裡的 `a` 是 $\lfloor{n/2}\rfloor$ - Line 4 的 if 將程式分為兩個階段 - 第一個是 a > 0 時,此階段對應到上面所說的 heapsort 架構中 build max heap 的部分 - 第二個階段就是 a = 0 且 n > 0 時,對應到上述的 swap & sort 階段 - Line 11 的 for 對應到的是前面 bottom-up 敘述中,不斷向下找比較大的 child 直到 leaf 的部分 - Line 17 的 while 對應到的是,從 leaf 開始向上尋找 `a` 所應該要放置的位置的階段。而 `c = b` 就是前面說的設定擺放位置 new 的操作。 - Line 20 的 while 對應到從 new location 開始,不斷的跟 parent 做 swap,一直到進行到 `a` 時的部分。