# 2017 q3 Homework08 - Natural merge sort contributed by <`Zixin`, `kevin550029`> :::danger **TODO**: * natural merge sort 的應用情境 ::: ## 目標 * 延續 [Natural merge sort 在特定硬體的加速](https://hackmd.io/s/B1_V03Vlg),釐清之前的議題。需要完整的技術報告和錄影 ## 先備知識 ### Mergesort #### 參考資料 * [ALGORITHMS - MERGE SORT](http://www.bogotobogo.com/Algorithms/mergesort.php) * [Real-world sorting](http://www.cse.chalmers.se/edu/year/2016/course/DIT960_Data_structures/slides/8.pdf) #### 範例一 ![](https://i.imgur.com/MstWopF.png) * Partition: 對半切, 切到每個 array 只有一個元素 * Merge: 兩兩比大小進行合併, 每次比較皆由第一個元素開始比 (該 array 最小的數) 取最小的放在新的 array, 在原本的 array 中刪除該數字 再繼續比較兩個 array 的第一個數, 直到把兩個 array 中的數字都比完 #### 範例二 ![](https://i.imgur.com/jndUUgT.png) ![](https://i.imgur.com/bhrFAjE.png) ```clike= vector<int> mergeSort(vector<int> m) //傳入一組數列 { if (m.size() <= 1) //當傳入的數列長度小於等於一時,回傳數列本身 return m; vector<int> left, right, result; int middle = ((int)m.size()+ 1) / 2; //取得傳入數列的中間位置 //將中間位置之前的數字 push 進 left 的 vector for (int i = 0; i < middle; i++) { left.push_back(m[i]); } //將中間位置之後的數字 push 進 right 的 vector for (int i = middle; i < (int)m.size(); i++) { right.push_back(m[i]); } //遞回執行, 直到傳入的數列長度為一的時候中止, 並回傳數列本身(長度為一的數列) left = mergeSort(left); right = mergeSort(right); // 比較第一個數的大小, 合併兩個 vectors result = merge(left, right); return result; } ``` ## Natural Merge Sort 介紹 :::danger difference between two merge sort ( deal with odd number input ) [merge sort wiki - varient](https://en.wikipedia.org/wiki/Merge_sort) -> try to reducing space complexity & cost of copying use figures to explain ::: * Idea: Exploit pre-existing order by identifying naturally-occurring runs * 傳統的 Merge Sort 在進行合併之前,會先把數列的 list 切割成單一個 element ,而 Natural Merge Sort 則會將 list 切成一個一個 run (有序數列) * 簡而言之,Natural Merge Sort 是利用數據中**本身存在的有序數列 (升序或降序) 片段**, 來 **減少切割的次數** ![](https://i.imgur.com/CMnJcUG.png) 以上面的圖當例子," 1 3 6 8 "是一個在原數列中,已排序好的子數列,在 Natural Merge Sort 中,這個數列可已被切割成一個單一的 run :::danger 我們觀察到兩種 Natural Merge Sort 的實作方式,下面會用兩個例子解釋 ::: ### 範例 第一個例子是來自於 [wiki - Natural merge sort](https://en.wikipedia.org/wiki/Merge_sort#Natural_merge_sort) 這個方法會先探訪一遍數列,並將有序數列切成多個 runs 再做合併 ``` Start : 3--4--2--1--7--5--8--9--0--6 Select runs : 3--4 2 1--7 5--8--9 0--6 Merge : 2--3--4 1--5--7--8--9 0--6 Merge : 1--2--3--4--5--7--8--9 0--6 Merge : 0--1--2--3--4--5--6--7--8--9 ``` 第二個例子是來自於 [淺談 merge sort](https://blog.kuoe0.tw/posts/2013/03/06/sort-about-merge-sort/) ,下方用他提供的演算法來解釋 * Algorithm 描述: 傳入要排序的 A 數列,宣告兩個數列空間 B 與 C,並設定數列 B 為使用中數列( CUR ) * 依序探訪原數列元素,並將元素放入使用中數列 B * 當目前的元素小於上一個元素時,並將該元素放入另一個數列 C * 不斷的重複上述步驟,直到所有元素都被探訪過,並分配到子數列 ```clike= function naturalMergeSort( A ): B, C are empty sequence //宣告空的兩個數列空間, B 與 C CUR refer to B //設定 B 為使用中數列 LAST = A[ 0 ] //設定 CUR 中最後一個數字為 A 的第一個數字 // first( A ) 與 LAST 比較, 較大則將 first( A ) 存入 CUR // 較小將 CUR 指向另一個 不在使用中 的數列空間, 並將 first( A ) 存入 CUR while A is not empty if first( A ) >= LAST append first(A) to CUR LAST = first( A ) A = rest( A ) else if CUR refer to B CUR refer to C else CUR refer to B LAST = first( A ) if B is empty or C is empty return // 當 A 被切割完成後, 傳入 B 和 C 繼續進行切割 // 直到其中一個傳入數列已變成有序數列 (不用再切割) naturalMergeSort( B ) naturalMergeSort( C ) A = merge( B, C ) ``` 假如 input 數列 A 為 1 3 4 2 7 5 8 0 9 第一輪切割後,B 和 C 分別為 B: 1 3 4 5 8 C: 2 7 0 9 再將 B 和 C 分別傳入 naturalMergeSort() 繼續進行切割 ## 比較 Natural Merge Sort 利用有序數列來減少傳統 Merge Sort 所需要的切割數量, 藉此來加速排序過程,但他在切割 run 時必須進行比較,這個過程也會耗費額外的時間。 在 [淺談 merge sort](https://blog.kuoe0.tw/posts/2013/03/06/sort-about-merge-sort/) 的介紹中有一個測試實驗,比較不同資料數量下 Natural Merge Sort 和 傳統 Merge Sort 的執行時間 ![](https://raw.githubusercontent.com/KuoE0/blog-assets/master/content-photos/2013-03-06-sort-about-merge-sort-1.jpg) 測試資料皆為 100 組,單位為秒 (second), 有星號的測試資料表示數列本身是**由多組有序數列組成** 可以觀察在一般的資料底下 Natural Merge Sort 並不一定比傳統 Merge Sort 快, 反而花的時間比較多 但在經過設計,有較多有序數列的資料底下,Natural Merge Sort 的效能就有明顯得提升 ## 前人進度 :::danger put some reference of previous work restate previous work point out problems ::: * 嘗試使用硬體提供的 clz 指令來改寫,但 mergesort 當中 clz 運算的次數很小, 所以沒有效能沒有提升很多 * 利用 __builtin_clz 指令, 及減少些微執行時間 ![](https://i.imgur.com/jymCAYb.png) * 最耗效能的部分在 merge,因此可以先偵測數字順序連續小於的區塊,減少 memcpy 的次數 ```c int conti_times_right = 0, conti_times_left = 0; while (left + conti_times_left < left_bound && right + conti_times_right < right_bound) { if (cmp(((char*) source) + size * (right + conti_times_right), ((char*) source) + size * (left + conti_times_left)) < 0) { //cmp() means? //A: user-defined comparasion function if (conti_times_left) { memcpy(((char*) target) + size * target_index, ((char*) source) + size * left, size * conti_times_left); left += conti_times_left; target_index += conti_times_left; conti_times_left = 0; } conti_times_right++; } else { if (conti_times_right) { memcpy(((char*) target) + size * target_index, ((char*) source) + size * right, size * conti_times_right); right += conti_times_right; target_index += conti_times_right; conti_times_right = 0; } conti_times_left++; } } if (conti_times_left) { memcpy(((char*) target) + size * target_index, ((char*) source) + size * left, size * conti_times_left); left += conti_times_left; target_index += conti_times_left; } else if (conti_times_right) { memcpy(((char*) target) + size * target_index, ((char*) source) + size * right, size*conti_times_right); right += conti_times_right; target_index += conti_times_right; } ``` 利用 conti_times_left, conti_times_right 來紀錄遞增數列的長度, 直到不再遞增, 再做 memcpy, 如此可以大大減少 memcpy 的次數 * Inline cmp 減少 overhead * 使用 branch predictor 的概念,先不判斷而是先作如果式子成立的結果,再來判斷如果式子不成立的結果 override (這部分還沒加入 natural mergesort) * 改變測試資料的型態,分別測試 (random, sorted, nearly sorted, reversed) * 兩篇參考文章 * 先分幾個 block, 分別排序好再 merge (與 natural mergesort 的不同: 此篇是隨機分 block, natural mergesort 是依照遞減情況分 block) * Nvidia 怎麼在 GPU 上加速 * [Github](https://github.com/petermouse/natural-mergesort) * calculate.c: 計算平均執行時間 * gen_testcase.c: 生成資料 * main.c: 輸出 ans.txt, runtime.txt * natural-mergesort.c * natural-mergesort_change.c ## Eliminating Branches with Prediction :::danger observe, assume, experiment design make an assumption, design experiment to verify it select representive measurement: ex: micro-benchmark ::: ## Introduction to branch prediction ![](https://i.imgur.com/dbSmQV0.png) Branches 會造成 pipeline 的困難, 當處理器抓到 branch 指令, 就會去抓下一步的指令, 但下一步有兩種可能, 處理器就會等待 branch 指令執行完再抓取下一步的指令, 這樣會造成 pipeline 中止。 而現代處理器會避免等待 branch 執行完再去抓取下一步指令, 因此會試圖預測下一步, 提早抓取預測的指令。當預測錯誤, 處理器就會丟棄該指令, 重新執行正確的指令。 For example, ``` int SumArray(int[] array) { if (array == null) throw new ArgumentNullException("array"); int sum=0; for(int i=0; i<array.Length; i++) sum += array[i]; return sum; } ``` 在這個例子中, (array == null) 和 (i<array.Length) 這兩個 branch, 前者當輸入錯誤時執行, 所以執行機會很小, 後者執行頻繁, 所以是很好預測的例子。 然而相對 branch 較少的情況, 若出現較多 branch, 處理器勢必更能預測出正確的下一步指令, 因此接下來我們嘗試減少 mergesort 裡 branch 的數量 ## eliminate branches orig code ```clike= while (left < left_bound && right < right_bound) { if (cmp(((char*) source) + size * right, ((char*) source) + size * left) < 0) { /* right is smaller */ memcpy(((char*) target) + size * target_index, ((char*) source) + size * right, size); ++right; } else { memcpy(((char*) target) + size * target_index, ((char*) source) + size * left, size); ++left; } ++target_index; } ``` 從以上程式碼, 我們可以看出來 branch 的其中一支是當右邊的值小於左邊的值的情況, 而另一支則相反, 因為兩者發生的機率從資料來看是剛好各 50%, 也就是預測錯誤的機會也是 50%, 因此我們嘗試減少 branch opt code ```clike= while (left < left_bound && right < right_bound) { char* v1 = ((char*) source) + size * right; char* v2 = ((char*) source) + size * left; int take = cmp(v1, v2) < 0; char* v = take ? v1 : v2; memcpy(((char*) target) + size * target_index, v, size); right = take ? right + 1 : right; left = take ? left : left + 1; ++ target_index; } ``` testcase: random, 100 numbers, range from 1~5000 ``` Performance counter stats for './natural-mergesort' (1000 runs): 0.623910 task-clock (msec) # 0.566 CPUs utilized ( +- 1.61% ) 1 context-switches # 0.827 K/sec ( +- 5.61% ) 0 cpu-migrations # 0.005 K/sec ( +- 57.68% ) 53 page-faults # 0.086 M/sec ( +- 0.06% ) 87,6290 cycles # 1.405 GHz ( +- 0.43% ) 87,0591 instructions # 0.99 insn per cycle ( +- 0.05% ) 17,2126 branches # 275.882 M/sec ( +- 0.04% ) 6800 branch-misses # 3.95% of all branches ( +- 0.26% ) 0.001103096 seconds time elapsed ( +- 5.59% ) ``` ``` Performance counter stats for './natural-mergesort_opt' (1000 runs): 0.524626 task-clock (msec) # 0.596 CPUs utilized ( +- 1.03% ) 0 context-switches # 0.810 K/sec ( +- 6.43% ) 0 cpu-migrations # 0.008 K/sec ( +- 49.92% ) 54 page-faults # 0.102 M/sec ( +- 0.05% ) 87,4084 cycles # 1.666 GHz ( +- 0.40% ) 87,6883 instructions # 1.00 insn per cycle ( +- 0.05% ) 17,3504 branches # 330.719 M/sec ( +- 0.04% ) 6704 branch-misses # 3.86% of all branches ( +- 0.24% ) 0.000880129 seconds time elapsed ( +- 1.56% ) ``` ![](https://i.imgur.com/QALhbsL.png) testcase: sorted, 100 numbers, range from 1~5000 ![](https://i.imgur.com/vr0yaKJ.png) testcase: nearly-sorted, 100 numbers, range from 1~5000 ![](https://i.imgur.com/R2wEvXY.png) testcase: reversed, 100 numbers, range from 1~5000 ![](https://i.imgur.com/NXupMVc.png) 實驗結果呈現的是, 效能沒有明顯提昇, 所以我發現去掉 if, else, 用其他判斷式, 如 C ? A : B, 並沒有降低在 branch 上消耗的 cost 在預測錯誤的情況下, 原始的程式碼是先抓取到錯誤的指令, 直到 branch 指令結束才發現預測錯誤, 丟棄原本執行的結果, 再重新執行。 而更新的程式碼是直接執行某一個 branch, 當發現預測錯誤再跳回執行另一個 branch, 所以其實是一樣的。 回頭看原始程式碼, 可以知道 branch 預測成本在於資料 random 分佈的情況, 預測準確度只有 50%, 但如果是 sorted 或 reversed 的資料, 因會傾向執行某一分支, 所以預測也會較準確。 ## Timsort 參考 Timsort,思考混合兩種以上 stable sorting algorithm 的機會 * 結合了 merge sort 和 insertion sort * 關鍵特性: 利用數據中的某些共同特性(已排序) * 方法概述 * 先將 input array 切割成許多個 sub-arrays (稱為 runs) * 利用 Insertion sort 排序各個 runs * 將排序好的 runs 利用 merge sort 合併 ### Algorithm * Step 1. Splitting input into Runs * 將 base address 設為 input array 起始 * 從目前的位置開始探訪, 尋找有序數列 (run) * 當找到的 run 長度小於 minrun 使用 insertion sort 持續將接下來的元素加入該 run 直到其長度滿足 minrun ![](https://www.infopulse.com/wp-content/uploads/2015/08/timsort-algorythm-2nd-screenshot.png) * Step 2. Merge * 結束上個步驟後,會有許多不同的 runs * 假設 input data 是 random 的,每個 run 的長度會接近於 minrun * 而當 input data 是偏向 sorted 的, run 的長度會遠超過 minrun * merge 的條件 * 當Timsort算法找到一個run時, 會將該run在數列中的起始位置和長度放入 stack 然後根據先前放入 stack 中的run决定是否合併 * 以下圖為例 X, Y, Z 分別是三個 run 的長度 當不滿足下列兩狀況時, X, Y 會被合併 (1) X>Y+Z (2) Y>Z ![](https://www.infopulse.com/wp-content/uploads/2015/08/timsort-algorythm-3rd-screenshot.png) * merge 方法 * 宣告一個臨時儲存空間,來合併兩個連續的 runs * 先將較小的 run 複製到這個臨時儲存空間 * 用原先儲存這兩個 runs 的空間来儲存合併後的 run ![](https://www.infopulse.com/wp-content/uploads/2015/08/timsort-algorythm-4th-screenshot.png) ## 參考資料 [淺談 merge sort](https://blog.kuoe0.tw/posts/2013/03/06/sort-about-merge-sort/) [Branch elimination](http://cellperformance.beyond3d.com/articles/2006/04/benefits-to-branch-elimination.html) [Fast and slow if-statements](http://igoro.com/archive/fast-and-slow-if-statements-branch-prediction-in-modern-processors/) [TIMSORT SORTING ALGORITHM](https://www.infopulse.com/blog/timsort-sorting-algorithm/)