lib/list_sort.c
執行人: visitorckw
專題解說錄影
解釋 Linux 核心的 lib/list_sort.c
現有的 bottom-up merge sort 實作,並尋求效能改進,例如 hybrid sort 的引入。
lib/list_sort.c
彙整〈你所不知道的 C 語言: linked list 和非連續記憶體〉、〈Linux 核心的鏈結串列排序〉,以及 Homework1 學員關於 lib/list_sort.c
的實作討論成果,在本頁面解釋各項設計和實作議題,應涵蓋以下:
注意用語:
lib/list_sort.c
中實作的 merge sort 演算法採用 bottom-up 策略。它首先將給定的串列轉換為一個以空結尾的單向鏈結串列。該演算法維護一個稱為 "pending" 的數據結構,表示等待進一步合併的已排序子串列。
lib/list_sort.c
中 merge sort 演算法的特點如下:
George Spelvin 在 commit message 中附上的分析合併排序的論文:
為了評估和比較各個排序演算法,會關注以下三種數據:
為了說明多種不同的排序演算法執行時間存在顯著差異並且希望不侷限某特定 CPU 硬體平台,我們可以引入統計學中的變異數分析的手法並搭配 F 分布來檢驗。其執行步驟如下:
The number of comparisons
論文 Bottom-up Mergesort: A Detailed Analysis 當中使用 K 值來分析比較次數。論文中提及比較次數與排序的元素個數關係如下:
其中
其中大括號
我們可以進一步將比較次數的函數總結成以下式子:
由於
double average_K(int size)
{
double sum_k = 0;
for (int n = size; n < end; n++) {
ptr *list = new_list();
for (int i = 0; i < n; i++) {
insert_tail(list, random());
}
int cmp_count = sort(list);
sum_k += log2(n) - (double) (cmp_count-1) / n;
}
return sum_k / size;
}
Cache miss ratio
可用 valgrind 之中的 cachegrind 工具分析 cache miss ratio,其用法如下:
$ valgrind --tool=cachegrind ./executable_file
延續〈Timsort 研究與對 Linux 核心貢獻嘗試〉的成果,著重於以下:
lib/list_sort.c
的應用場景,必須做到最低的堆疊使用開銷,因此需要從演算法層面調整 Timsort,使其得以達到 in-place (或最低的記憶體開銷)涉及排序演算法和現代處理器的議題,可對照閱讀以下:
排序執行時間與鏈結串列節點數量關係圖
比較次數與鏈結串列節點數量關係圖
N (鏈結串列節點數量) = 5
list_sort | timsort | shiversort | |
---|---|---|---|
list_sort | |||
timsort | 1.11022e-16 | ||
shiversort | 6.1281e-08 | 0.00254456 |
list_sort | timsort | shiversort | |
---|---|---|---|
list_sort | |||
timsort | 4.44089e-16 | ||
shiversort | 4.44089e-16 | 1.0 |
N (鏈結串列節點數量) = 100000
list_sort | timsort | shiversort | |
---|---|---|---|
list_sort | |||
timsort | 1.11022e-16 | ||
shiversort | 1.11022e-16 | 6.11238e-10 |
list_sort | timsort | shiversort | |
---|---|---|---|
list_sort | |||
timsort | 1.11022e-16 | ||
shiversort | 1.11022e-16 | 1.11022e-16 |
Timsort 需要一個 stack 來存放所有的 runs。由於排序時,會將待排序的鏈結串列當成單向鏈結串列。因此每個節點中的 prev
指標就可以被利用來儲存各種額外資訊。由於每個 runs 都保證最少會包含有兩個節點,因此我們可以用每個 runs 的第一個節點的 prev
指標來串接 stack ,而第二個節點的 prev
指標則用來儲存 runs 的大小。
首先實作用來取得 runs 大小的函式, runs 的大小存放在第 2 個節點的 prev
指標中。
size_t get_run_size(struct list_head *run_head)
{
return run_head ? 0 : (size_t)(run_head)->next->prev;
}
接著我們需要一個函式用來合併兩個相鄰的 runs。
static struct list_head *merge_at(void *priv, list_cmp_func_t cmp, struct list_head *at)
{
size_t len = get_run_size(at) + get_run_size(at->prev);
struct list_head *prev = at->prev->prev;
struct list_head *list = merge(priv, cmp, at->prev, at);
list->prev = prev;
list->next->prev = (struct list_head *) len;
--stk_size;
return list;
}
接著根據 runs 的大小實作合併策略。
static struct list_head *merge_collapse(void *priv, list_cmp_func_t cmp,
struct list_head *tp)
{
int n;
while ((n = stk_size) >= 2) {
if ((n >= 3 && get_run_size(tp->prev->prev) <= get_run_size(tp->prev) + get_run_size(tp)) ||
(n >= 4 && get_run_size(tp->prev->prev->prev) <= get_run_size(tp->prev->prev) + get_run_size(tp->prev))) {
if (get_run_size(tp->prev->prev) < get_run_size(tp)) {
tp->prev = merge_at(priv, cmp, tp->prev);
} else {
tp = merge_at(priv, cmp, tp);
}
} else if (get_run_size(tp->prev) <= get_run_size(tp)) {
tp = merge_at(priv, cmp, tp);
} else {
break;
}
}
return tp;
}
最後則是 Timsort 的本體,不斷地找尋新的 runs 並且合併。
do {
/* Find next run */
struct pair result = find_run(priv, list, cmp);
result.head->prev = tp;
tp = result.head;
list = result.next;
stk_size++;
tp = merge_collapse(priv, cmp, tp);
} while (list);
測試結果:
Implementation | Elapsed time | Comparisons |
---|---|---|
Linux | 462341 | 20621567 |
shiverssort | 269158 | 14339471 |
timsort | 298119 | 1525486 |
Implementation | Elapsed time | Comparisons |
---|---|---|
Linux | 170253 | 20573832 |
shiverssort | 94199 | 14318621 |
timsort | 95611 | 14906465 |
Implementation | Elapsed time | Comparisons |
---|---|---|
Linux | 298097 | 20621567 |
shiverssort | 183847 | 14339471 |
timsort | 203153 | 15254864 |
利用第 7 週教材提到的 user-mode-linux 和 virtme,對改進的 lib/list_sort.c
進行正確性和效能測試,確保能在 Linux v6.1+ 運作。
隨後可準備作出 Linux 核心的程式碼貢獻