# 2024q1 Homework1 (lab0) contributed by < [wu81177](https://github.com/wu81177/lab0-c) > ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz CPU family: 6 Model: 140 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 1 CPU max MHz: 4700.0000 CPU min MHz: 400.0000 BogoMIPS: 5606.40 ``` ## 指定的佇列操作 ### `q_new` 實作 `q_new` 過程中,我學習到 `list.h` 中 `list_head` 和 `INIT_LIST_HEAD` 的部份 #### `LIST_HEAD` 從結構成員可以發現 `list_head` 組成的佇列是雙向佇列,同時我也理解到 `list_head` 除了獨立當作佇列的頭,也會嵌入佇列成員結構體中,佇列成員在這份作業中為 `element_t` #### `INIT_LIST_HEAD` 此<s>函數</s> 函式是用來初始化已存在的 `list_head` ,如果要同時宣告並初始化可用 `LIST_HEAD()` :::danger 無論標題和內文中,中文和英文字元之間要有空白字元 (對排版和文字搜尋有利) ::: ### q_free `list_for_each_safe` 和 `list_for_each_entry_safe` 都能確保釋放當前節點不會遺失下一個節點,然而後者有使用 `list_entry` ,選用它就能輕鬆獲得元素的指標並釋放成員 迴圈中使用 `q_release_element` 來釋放空間,最後再釋放 head :::danger 改進你的漢語表達。 ::: 而在運行 `q_test` 的時候發現有問題因此使用 Valgrind 檢查,以下為檢察結果 : ``` ERROR: Attempted to free unallocated block. Address = 0x4b8e850 ==31792== Invalid read of size 8 ==31792== at 0x10F43C: find_header (harness.c:98) ==31792== by 0x10F43C: test_free (harness.c:181) ==31792== by 0x10F72E: q_release_element (queue.h:120) ==31792== by 0x10F72E: q_free (queue.c:30) ... ``` 發現少檢查 head 為空的情況 ```diff void q_free(struct list_head *head) { + if (!head) + return; element_t *entry, *safe; list_for_each_entry_safe (entry, safe, head, list) q_release_element(entry); free(head); } ``` ### q_insert_head & q_insert_tail 原先 `value` 是直接使用 `s` 的空間,後來參考別人的發現應該使用 `strdup` ,因為 `s` 可能會被改動 ```diff bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *e = malloc(sizeof(element_t)); if (!e) return false; - e->value = s; + e->value = strdup(s); list_add(&e->list, head); return true; } ``` 使用 `list_add` 可以將新元素放置在 head 後面, `list_add_tail` 則可以放到 head 前面 發現應該要檢查 `strdup` 是否成功,如果失敗應該要釋放當前元素,如下 ```diff bool q_insert_head(struct list_head *head, char *s) { if (!head || !s) return false; element_t *e = malloc(sizeof(element_t)); if (!e) return false; e->value = strdup(s); + if(!e->value){ + free(e); + return false; + } list_add(&e->list, head); return true; } ``` ### q_remove_head & q_remove_tail 從 `list.h` 中可以發現 `container_of` 和 `list_entry` 的功能完全相同,我選擇了前者來獲取 `element_t` 的指標 移除元素有 `list_del` 和 `list_del_init` 可用,我選用後者,否則再次使用已移除的元素時可能會出錯 * 發現沒有排除佇列為空的情況,原先的程式在下列第四行會出錯 ```diff - if (!head) + if (!head || list_empty(head)) return NULL; element_t *e = container_of(head->next, element_t, list); ``` ### q_size 使用 `list_for_each` 走訪佇列並且計數 ### q_delete_mid 我使用的方法是從鏈結串列頭 head 出發,同時使用兩個指標進行走訪,一個指標順行,另一個指標則逆行,直到它們相遇(奇數個元素情況)或者在交會前一步(偶數個元素情況)再刪除順行指標所指向的元素 * 發現使用 `q_release_element` 後再用 `free` 釋放元素內容是多餘的 ```diff bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head *forward = head->next; struct list_head *backward = head->prev; while (forward != backward && forward->next != backward) { forward = forward->next; backward = backward->prev; } element_t *e = container_of(forward, element_t, list); list_del(forward); q_release_element(e); - free(forward) return true; } ``` :::danger :warning: 留意詞彙的使用: * [資訊科技詞彙翻譯](https://hackmd.io/@sysprog/it-vocabulary) * [詞彙對照表](https://hackmd.io/@l10n-tw/glossaries) ::: ### q_delete_dup 使用兩層迴圈比對,原本都想用 `list_for_each` ,後來發現會重複比對,改成以下這樣 ```diff list_for_each (outer, head) { - list_for_each(inner, head){ + for (inner = (outer)->next; inner != (head); inner = inner->next) { ... } } ``` 發現當沒有元素或單元素應該 return true ```diff - if (!head || head->prev == head->next) { + if (!head) return false; + if (head->prev == head->next) + return true; ``` 原先的寫法一直無法順利運作 <s> bool q_delete_dup(struct list_head *head) { // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/ if (!head || head->prev == head->next) { return false; } struct list_head *outer, *inner, *tmp; element_t *outer_ele, *inner_ele; for (outer = (head)->next; outer != (head)->prev; outer = outer->next) { for (inner = (outer)->next; inner != (head);) { outer_ele = container_of(outer, element_t, list); inner_ele = container_of(inner, element_t, list); if (strcmp(outer_ele->value, inner_ele->value) == 0) { tmp = inner->next; list_del(inner); q_release_element(inner_ele); inner = tmp; } else { inner = inner->next; } } } return true; } </s> :::danger 明確標注同學的 GitHub 帳號名稱 ::: 於是換了寫法,經過<s>同學</s> [kkkkk1109](https://github.com/kkkkk1109/lab0-c) 提醒發現輸入的佇列是已排序的,因此改用他的寫法 :::danger 你的洞見和檢討呢? ::: 原先針對為排序的串列要使用兩層迴圈檢查有沒有重複,但由於輸入串列已排序,相同的鍵值一定會相鄰,因此只需要檢查相鄰的節點,如果和前者相同即刪除,如此一來可以將時間複雜度降低 N 倍 ### q_reverse 使用 `list_for_each_safe` 走訪佇列,沿途使用 `list_move` 將元素移至首位,如此一來就能倒轉順序 原先想將 `q_reverse` 視為 `q_reverseK` 的特例,後來覺得在 `q_reverseK` 中呼叫 `q_reverse` 比較好實作 * 多檢查空元素和單元素的情況以提早退出 ```diff - if (!head) + if (!head || head->prev == head->next) return; ``` ### q_reverseK 使用 `list_cut_position` 剪下子佇列,用 `q_reverse` 倒序後再用 `list_splice` 接回去 `list_cut_position(head_to, head_from, node)` 剪下的部份並不包含 `head_from` 和 `node` 兩個節點 ### q_swap 視為 `q_reverseK` k 為2的特例 ### q_ascent 從 head 逆向走訪佇列,如果下一個元素大於當前元素則使用 `list_del` 刪除下個元素,如此一來就能留下遞增元素 學會 `strcmp ()` 怎麼用,此函數會開始比較每一個字串的第一個字元。 如果它們彼此相等,會繼續往下進行配對,直到字元不同或到達較短字串的結尾 ### q_descent 從 head 逆向走訪佇列,如果下一個元素小於當前元素則使用 `list_del` 刪除下個元素,如此一來就能留下遞減元素 ### q_sort 由於我在 `q_merge` 中會使用這個函式,在使用的前一步它已經是分段排序的佇列,這時候如果用 merge sort 會有不錯的效果,所以我選擇用 merge sort實作之 用來合併兩以排序佇列的函式我定義為 `merge_two` ,在裡面原先判斷是否遞減和兩元素比較大小的部份使用兩層 `if` ,後來改為以下較緊湊的寫法 ```diff while (!list_empty(l1) && !list_empty(l2)) { element_t *ele_l1 = list_first_entry(l1, element_t, list); element_t *ele_l2 = list_first_entry(l2, element_t, list); - if(descend){ - if(strcmp(ele_l1->value, ele_l2->value) < 0) - list_move_tail(l2->next, &tmp_head); - else - list_move_tail(l1->next, &tmp_head); - } else { - if(strcmp(ele_l1->value, ele_l2->value) < 0) - list_move_tail(l1->next, &tmp_head); - else - list_move_tail(l2->next, &tmp_head); - } + if (descend == (strcmp(ele_l1->value, ele_l2->value) < 0)) { + list_move_tail(l2->next, &tmp_head); + } else { + list_move_tail(l1->next, &tmp_head); + } } ``` 而在 `q_sort` 中,使用先前 `q_delete_mid` 函式中的方法找到中點,再使用 `list_cut_position` 剪下後半段,用遞迴的方式分別排序前後兩段再用 `merge_two` 合併 ### q_merge 我首先去理解 `queue_contex_t` 的運作原理 每個 `queue_contex_t` 代表了一個佇列,其中 `q` 會指向佇列的頭,而 `chain` 則是用來連接不同的 `queue_contex_t` 在這個函式中,我將所有的佇列使用 `list_splice` 拼接到第一個佇列,並且使用 `list_del_init` 移除已合併的佇列,最後再使用 `q_sort` 排序 * 取第一個佇列時誤用 `list_entry`,後來使用 `list_first_entry` ```diff -queue_contex_t *first_q = list_entry(&head->next, queue_contex_t, chain); +queue_contex_t *first_q = list_first_entry(head, queue_contex_t, chain); ``` 有時會出現 merge 完佇列消失的情況 ``` l = [c r z] cmd> merge l = [] ``` 後來發現在清空佇列後不移除當前 `queue_contex_t` 就好了 ```diff list_for_each_entry_safe (curr, safe, head, chain) { if (curr == first_q) continue; first_q->size += curr->size; list_splice_init(curr->q, first_q->q); - list_del_init(&curr->chain); } ``` ### 結果 使用 `make test` 測試,只剩 trace-17 無法通過 ``` +++ TESTING trace trace-17-complexity: # Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant ERROR: Probably not constant time or wrong implementation ERROR: Probably not constant time or wrong implementation Probably constant time Probably constant time --- trace-17-complexity 0/5 --- TOTAL 95/100 make: *** [Makefile:60: test] Error 1 ``` 可以看到唯 `it` 和 `ih` 無法通過,有時候 `it` 會通過,分數為95 最後經由底下[修改 lab0/dudect](https://hackmd.io/D8_dNDEwTHi7RHGinG0bPQ?view#%E4%BF%AE%E6%94%B9-lab0dudect) 後可以得到100分 ## 使用 Valgrind 檢查錯誤 先前使用 Valgrind 發現了實作 `q_free` 的暇疵,而最後我也嘗試使用它來找出 `trace-17` 中 `it` 和 `ih` 無法通過的原因,使用過程如下 ``` $ valgrind -q --leak-check=full ./qtest cmd> option simulation 1 cmd> it Probably constant time cmd> ih Probably constant time cmd> rh Probably constant time cmd> rt Probably constant time cmd> option simulation 0 cmd> quit Freeing queue ``` Valgrind 沒有找到記憶體使用問題,同時也發現可能在通過邊緣,上面單獨測試`it` 和 `ih` 沒問題 ## 實作 Shuffle 在 `queue.c` 中加上 `q_shuffle` 函式, `q_test` 中加上 `do_shuffle` 函式檢查是否為空或單節點,還有 `console_init` 中加上 `ADD_COMMAND(shuffle, "Do Fisher-Yates shuffle", "");` ```diff * void q_shuffle(struct list_head *head) { if (!head || head->next == head->prev) return; for (int len = q_size(head)-1; len > 0; len--) { int random = 1 + rand() % len; struct list_head *old = NULL, *new = NULL; int j = 1; struct list_head *pos; list_for_each(pos, head) { if (j == random) { old = pos; } if (j == len + 1) { new = pos; break; } j++; } struct list_head *pre_new = new->prev; struct list_head *pre_old = old->prev; if(old->next == new){ list_del(old); list_add(old,new); + } else { + list_del(new); + list_del(old); + list_add(new, pre_old); + list_add(old, pre_new); } } } ``` :::danger 使用 `git diff` 或 `diff -u` 來產生程式碼之間的變異清單,不要憑感覺填入,注意位移量。 ::: 首先在外層迴圈把 len 減少,裡面抽出要交換的 index 後用 `list_for_each` 找到要交換的 `old` 和 `new` ,最後交換節點 ,交換的部份原先的寫法沒考慮到兩節點相鄰的狀況 ### 實驗結果 ![Figure_1](https://hackmd.io/_uploads/SycnYG766.png) 測試 [1, 2, 3, 4] 做 shuffle 1,000,000 次的直方圖,可以看到結果很平均 --- ## 改進 lab0-c 的 constant time test ### 閱讀 [〈Dude, is my code constant time?〉](https://eprint.iacr.org/2016/1123.pdf) :::danger 避免非必要的項目縮排 (即 `* ` 和 `- `),以清晰、明確,且流暢的漢語書寫。 ::: 檢查程式是否為常數運行時間很重要是因為如果不是,有機會受到時序攻擊,資安上存在疑慮 而作者提出了 dudect 工具來測試程式是否為 constant time ,且相較於其他常見工具, dudect 不需要對硬體行為進行建模,也使用了 cropping 預處理,捨棄掉大於 p percentile 的資料,避免離群值過分影響統計結果,而 t value 的計算是使用 Welch’s t 檢定法。 ### 修改 lab0/dudect 檢查 `fixture.c` 發現沒有進行 cropping ,因此從作者的原始碼以 percentile 為關鍵字找到 `percentile` 和 `prepare_percentiles` 函式,接著將前述兩個函式和他們會使用到的函式複製到 `fixture.c` 裡面,再修改 data type 就可以通過 trace-17-complexity,得到滿分 ``` +++ TESTING trace trace-17-complexity: # Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant Probably constant time Probably constant time Probably constant time Probably constant time --- trace-17-complexity 5/5 --- TOTAL 100/100 ``` --- ## Linux 核心鏈結串列排序 研讀 Linux 核心原始程式碼的 [lib/list_sort.c (commit b5c56e)](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) ,首先覺得陌生的就是` __attribute__((nonnull())) `,後來了解到` __attribute__ ` 是 gcc 中用來附加屬性給函式或結構的用法,提供編譯器額外的訊息,而 `nonnull(2,3,4,5)` 是用來表示函式內的第 2,3,4,5 個參數不能為空,如此一來就不用在函式內檢查,接下來逐一記錄研讀 `list_sort.c` 中函式的心得。 :::danger 不要「舉燭」,你該質疑 Linux 核心開發者「如果不這樣做,會怎樣?」 ::: ### `merge` 此函式是用來合併已排序鏈結串列,過程中走訪兩個傳入的串列並且使用 `cmp(priv, a, b)` 來比較 a, b 節點中 `priv` 的大小,決定誰要排在前面,最後回傳合併的串列 值得注意的是,根據註解的說法,此函式回傳的是單向並且非環狀的串列,這樣比較適合多次合併使用。 ### `merge_final` ```clike= for (;;) { /* if equal, take 'a' -- important for sort stability */ if (cmp(priv, a, b) <= 0) { tail->next = a; a->prev = tail; tail = a; a = a->next; if (!a) break; } else { tail->next = b; b->prev = tail; tail = b; b = b->next; if (!b) { b = a; break; } } } ``` 前面的 for 迴圈中,除了將兩個串列合併之外,也建立了雙向的連接,然而就算是 b 先結束, a 剩下部份的開頭也統一改名為 b ```clike= tail->next = b; do { /* * If the merge is highly unbalanced (e.g. the input is * already sorted), this loop may run many iterations. * Continue callbacks to the client even though no * element comparison is needed, so the client's cmp() * routine can invoke cond_resched() periodically. */ if (unlikely(!++count)) cmp(priv, b, b); b->prev = tail; tail = b; b = b->next; } while (b); /* And the final links to make a circular doubly-linked list */ tail->next = head; head->prev = tail; } ``` 這段主要是將 b 剩下的部份改成雙向串列,並且在最後將頭尾相接使其形成環狀,但 `if (unlikely(!++count))` 這段看不懂,經過查找資料後了解 `unlikely` 是一個巨集被定義為 `# define unlikely(x) __builtin_expect(!!(x), 0)` ,其中 `!!(x)` 會確保輸入值轉變為布林值 ,而 `__builtin_expect` 是 gcc 的內建函式,查看[手冊](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html)中 Built-in Function: long __builtin_expect (long exp, long c) 舉例: ``` if (__builtin_expect (x, 0)) foo (); ``` 當 `foo ()` 被執行的情況很罕見的時候可以這樣寫,幫助編譯器最佳化 而在 `merge_final` 中 `count` 增加,就會變非 0 ,執行 `cmp(priv, b, b)` ,但我還是不知道為什麼要執行 `cmp(priv, b, b)` 比較 b 自己,而且由於 ++ 是在前面,所以在執行第一次的時候 `count` 就會是非 0 ### `list_sort` ```clike= /* Find the least-significant clear bit in count */ for (bits = count; bits & 1; bits >>= 1) tail = &(*tail)->prev; /* Do the indicated merge */ if (likely(bits)) { struct list_head *a = *tail, *b = a->prev; a = merge(priv, cmp, b, a); /* Install the merged result in place of the inputs */ a->prev = b->prev; *tail = a; ``` for 迴圈會逐位尋找二進位 `count` ,直到遇到第一個 0 結束迴圈,接下來判斷如果剩餘的位數存在 1 就進行合併,這樣做相當於當 count+1 不為 $2^k$ 時合併,參考[何時合併](https://hackmd.io/@sysprog/linux2024-lab0-e#%E4%BD%95%E6%99%82%E5%90%88%E4%BD%B5)的表格: | count 變化 | count 二進位 | merge | pending 上的節點 | | ----------- | ------------------- | ----------- | ----------------------------------------- | | 0 $\to$ 1 | 0000 $\to$ 0001 | no($2^0$) | 1 | | 1 $\to$ 2 | 0001 $\to$ 0010 | no($2^1$) | 1 $\gets$ 1 | | 2 $\to$ 3 | 0010 $\to$ 001==1== | yes | (2) $\gets$ 1 | | 3 $\to$ 4 | 0011 $\to$ 0100 | no($2^2$) | 2 $\gets$ 1 $\gets$ 1 | | 4 $\to$ 5 | 0100 $\to$ 010==1== | yes | 2 $\gets$ (2) $\gets$ 1 | | 5 $\to$ 6 | 0101 $\to$ 01==1==0 | yes | (4) $\gets$ 1 $\gets$ 1 | | 6 $\to$ 7 | 0110 $\to$ 011==1== | yes | 4 $\gets$ (2) $\gets$ 1 | | 7 $\to$ 8 | 0111 $\to$ 1000 | no($2^3$) | 4 $\gets$ 2 $\gets$ 1 $\gets$ 1 | | 8 $\to$ 9 | 1000 $\to$ 100==1== | yes | 4 $\gets$ 2 $\gets$ (2) $\gets$ 1 | | 9 $\to$ 10 | 1001 $\to$ 10==1==0 | yes | 4 $\gets$ (4) $\gets$ 1 $\gets$ 1 | | 10 $\to$ 11 | 1010 $\to$ 101==1== | yes | 4 $\gets$ 4 $\gets$ (2) $\gets$ 1 | | 11 $\to$ 12 | 1011 $\to$ 1==1==00 | yes | (8) $\gets$ 2 $\gets$ 1 $\gets$ 1 | | 12 $\to$ 13 | 1100 $\to$ 110==1== | yes | 8 $\gets$ 2 $\gets$ (2) $\gets$ 1 | | 13 $\to$ 14 | 1101 $\to$ 11==1==0 | yes | 8 $\gets$ (4) $\gets$ 1 $\gets$ 1 | | 14 $\to$ 15 | 1110 $\to$ 111==1== | yes | 8 $\gets$ 4 $\gets$ (2) $\gets$ 1 | | 15 $\to$ 16 | 1111 $\to$ 10000 | no($2^4$) | 8 $\gets$ 4 $\gets$ 2 $\gets$ 1 $\gets$ 1 | 而整個函式會逐一把原始串列 `list` 的節點移到待處理串列 `pending` , 而 `pending` 中已排序的子串列會以前段提到的規則合併,直到原始 `list` 輸入完,接著在最後的 for 迴圈中會繼續合併 `pending` 中子串列,在這個階段是取最後一個和倒數第二個子串列合併成一個,直到只剩兩個子串列就結束迴圈,執行 `final_merge` 完成全部的合併 ### 閱讀 [< BOTTOM-UP MERGESORT A Detailed Analysis > ](https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.6.5260) Merge sort 可分成 bottom-up 和 top-down , top-down 為課本典型的演算法,將待排序序列分成左右兩個子序列,左右又繼續分裂直到子序列長度是一再合併,適合遞迴處理; bottum-up 則是將待排序序列元素逐一處理,在適當的時機合併,適合非遞迴實作,[commit b5c56e](https://github.com/torvalds/linux/commit/b5c56e0cdd62979dd538e5363b06be5bdf735a09) 屬於 bottom-up 的一種變形,也是逐一將元素讀入,差別在於合併的時機不同。 下表為 bottom-up 和 top-down 的性能較比較: | Case | Best Case | Average Case | Worst Case | |------------|-----------|----------------------|--------------------| | Top-Down | 1 | 2N lg N - 0.146N | N lg N - 1.248N | N lg N - 0.943N | | Bottom-Up | 1 | 2N lg N - 0.146N | N lg N - 0.965N | N lg N - 0.701N | 雖然 bottom-up 的最差情況效能略輸 top-down , 但是 bottom-up 不需要預先知道序列長度,適合 link-list 實作的序列,這點使得它的實用性大於 top-down。 ### 閱讀 [Queue-Mergesort](https://doi.org/10.1016/0020-0190(93)90088-q) 這篇文章主要介紹一種合併排序的變體稱為 Queue Mergesort ,使用佇列存放待合併序列,先將每個元素單獨作為一個待排序序列存放於佇列,之後不斷從頭部取出兩個進行合併後放到尾部,直到佇列中剩下一個序列即完成排序。 作者也證明這種方法是 optiml mergesort ,也就是對任何待排序元素個數 N ,最差情況下比較次數不比其他 mergesort <s>算法</s> 演算法多 :::danger algorithm 是「演算法」,而「算法」是 the way you calculate,也就是你在國中小學數學測驗卷上的「計算過程」,二者不同。 ::: ### 閱讀 [The cost distribution of queue-mergesort, optimal mergesorts, and power-of-two rules](https://doi.org/10.1006/jagm.1998.0986) 當兩個要合併的序列長度比值是2的冪時,也就是使用 power-of-2 rules ,有機會使得整體的時間複雜度隨著 N 變大時線性成長,既使合併兩序列的複雜度是超線性,因此相較於 half-half rules ,使用 power-of-2 rules 可以在 N 很大的時候顯著降低運算時間,而在 [commit b5c56e](https://github.com/torvalds/linux/commit/b5c56e0cdd62979dd538e5363b06be5bdf735a09) 中選用合併序列長度比值上限為 2 是因為不論 N 多少每一步合併都能限制在這個比值以下。 ### 引入 lib/list_sort.c 到 lab0-c 經過了上面的理解,我意識到自己之前實作 `q_sort` 的方法是多麼天真且愚蠢,因此我引入 lib/list_sort.c 的<s>算法</s> 演算法到 lab0-c [(commit 9837691)](https://github.com/sysprog21/lab0-c/commit/98376912f884a6766f0adb9e28eb3de656549ba6)。 過程中做了一些改寫,為了簡化實作,刪去了編譯器優化的部份`__attribute__((nonnull())`, `likely` 和 `unlikely` ,還有以 `strcmp` 取代 `cmp` 函式,因此也刪減了有關函式的傳入參數 `prev`, `cmp` 。