基于多线程的并行快速排序算法实现
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 总结
本文首先回顾了经典快速排序算法,然后介绍了一种基于多线程的并行排序算法,最后结合实验对算法参数进行优化固定,并对比了经典算法和并行算法的性能,结果证明并行算法确实能极大提高排序速度。