基於多線程的並行快速排序算法實現
1. 快速算法(Quick Sort)介紹
快速排序(Quick Sort)是一種經典的排序算法,基於遞歸實現,由於其實現方式簡單可靠、平均時間復雜度為O(nlogn) (最壞情況O(n^2)), 被廣泛采用。一個QuickSort算法實現如下(基於c++):
class Sorter { public: template<typename IterType> static void quicksort(IterType first, IterType last) { auto distance = std::distance(first, last); // #GGG if (distance < 2) { return; } else if (distance == 2 && (*first > *(first+1))) { std::swap(*first, *(first+1)); } // #HHHH auto mid = partition(first, last); // #AAA quicksort(first, mid); // #BBB quicksort(mid+1, last); // #CCC } private: template<typename IterType> static IterType partition(IterType first, IterType last) { // Always pick the last data as pivot, can be optimzed more auto pivot = *(last-1); IterType it1 = first; IterType it2 = first; while (it2+1 != last) { if (*it2 <= pivot) { std::swap(*it1, *it2); it1++; } it2++; } std::swap(*it1, *(last-1)); return it1; } };
下面是利用上述排序算法對整數數組進行排序的示例。
vector<int> nums; // Populate vector with data ... Sorter::quick_sort(nums.begin(), nums.end());
因為算法基於c++模板函數實現,所以可支持的數據類型不僅局限於vector, 還可以是支持迭代器操作的所有容器類型;容器元素類型也不僅局限於int,還可以是支持比較操作符(<=)任一數據類型如double、float, 甚至可以是自定義類型。
2. 並行快速排序算法(Parallel Quick Sort)
我們學習研究算法的目的是盡可能降低時間和空間復雜度,最大程度提高運行效率。當二者不可兼得時,應當根據具體產品/運行環境的側重點不同,采取以空間換時間或時間換空間的方式來確保某一方面的性能。
針對經典快速排序算法,后續有很多優化方法, 例如隨機化標定點(pivot)、雙路快排、三路快排等,但萬變不離其蹤,都是分而治之的思想,有興趣可參考國內外資料,這里我們主要看一下如何利用多線程技術來提升排序的速度。
我們可以注意到#AAA行對所有數據一分為三:小於pivot的數據(part1)/pivot/大於pivot的數據(part2),#BBB行和#CCC行分別對part1和part2進行排序,並且#BBB和#CCC是串行執行,如果能讓它們並行運行,排序的速率將會大大提高。
需要注意的是,我們本不能直接把#BBB或#CCC放入新進程執行,因為快速排序算法本身是基於迭代實現的,其退出條件為數據個數小於等於2(#GGG~#HHH),如果把#BBB或#CCC放入新進程,就意味着每次數據量大於2的迭代就會產生一個新進程,很快就會把系統進程資源耗光。為此我們采用固定線程數來實實現並行排序,具體做法如下:
- 創建公共數據列表,並把要排序數據起始位置放入公共列表;
- 創建固定個數進程並發執行排序函數
- 在排序函數中,互斥訪問公共數據列表, 執行以下邏輯
(偽代碼) while data is not sorted: if public_data_list is not empty: first, last = public_data_list.pop_back() mid = partition(first, last) put(first, mid) into public_data_list put(mid+1, last) into public_data_list else: sleep until got nofitified that public_data_list is not empty
具體實現,請參考parallel_quick_sort.
3. 算法測試
3.1 算法參數說明
3.1.1 threads_num
使用多少個線程進行排序,應該不大於當前機器CPU核數
3.1.2 parallel_gate
啟動多線程排序的最小數據量,如果達不到,則使用傳統快排算法。在數據量較小時,多線程排序並不占優勢,因為:1)傳統算法本身就很快,平均時間復雜度O(n*logn),當數據量小於1000時,現代硬件水平(Intel Core(TM) i5/i7等)可以在秒間完成; 2)線程創建/銷毀、 鎖競爭帶來的開銷相比排序本身時間消耗使得使用多線程有些得不償失。
另外,並行排序算法本質上是以空間換時間,我們最好收集下最大內存消耗,從而衡量下性能提升的代價是否可以接受或者是否適用於目標系統。
3.2 上述並行排序算法涉及的參數,應根據實驗結果進行最優化配置
3.2.1使用下表對parallel_gate進行優化,根據測試結果parallel_gate_取值為5000時,算法性能最優:
thread_num_ | data num | parallel_gate_ | Time |
2 | 5000000 | 100 | 5s923ms150us |
2 | 5000000 | 500 | 5s712ms912us |
2 | 5000000 | 1000 | 5s581ms731us |
2 | 5000000 | 5000 | 5s237ms971us |
2 | 5000000 | 10000 | 5s469ms624us |
**注:** 所有測試結果均在配置為(Interl Core(TM) i7, 4Core CPU, 8GB Mem)的desktop機器上取得
3.2.2使用下表對thread_num進行優化,根據測試結果,thread_num取值為4時,算法性能最優:相比單線程,性能提高了1倍左右
thread_num_ | num | parallel_gate_ | Time |
1 | 5000000 | 5000 | 8s769ms37us |
2 | 5000000 | 5000 | 5s723ms756us |
3 | 5000000 | 5000 | 4s908ms678us |
4 | 5000000 | 5000 | 4s469ms82us |
3.2.3 並行算法的內存開銷
並行排序算法的各線程間需要共享一個公共列表,列表每一項存儲一對vector迭代器,所以我只要知道了運行期間公共列表最大條目數,就能計算出最大內存開銷: Max_Mem_Usage = max_size_of_public_list * sizeof (vector::iterator) * 2
當data_num=10000000, parallel_gate_=5000, thread_num=4時,公共列表在運行期間最大條目數為:204,總共消耗內存16KB。
3.2.4 經典快速排序和並行快速排序算法的性能比較
Algorithm | thread_num_ | data num | |parallel_gate | Time | Memory |
Traiditional Sorting | 4 | 10000000 | 5000 | 13s929ms527us | O(1)* |
Parallel Sorting | 4 | 10000000 | 5000 | 6s855ms574us | 16.8KB |
*: 經典快速排序的內存消耗只有有限個變量。
4 總結
本文首先回顧了經典快速排序算法,然后介紹了一種基於多線程的並行排序算法,最后結合實驗對算法參數進行優化固定,並對比了經典算法和並行算法的性能,結果證明並行算法確實能極大提高排序速度。