我正在为 D 编程语言开发一个并行化库。现在我对基本原语(并行 foreach、map、reduce 和任务/未来)非常满意,我开始考虑一些更高级别的并行算法。更明显的并行化候选者是排序。
我的第一个问题是,排序算法的并行版本在现实世界中有用吗,还是它们主要是学术性的?如果它们有用,它们在哪里有用?我个人很少在我的工作中使用它们,因为我通常使用比单个 sort() 调用更粗粒度的并行度将我的所有内核固定在 100%。
其次,对于大型数组来说,快速排序似乎几乎是令人尴尬的并行,但我无法获得我认为应该获得的近线性加速。对于快速排序,唯一固有的串行部分是第一个分区。我尝试通过在每个分区之后并行排序两个子数组来并行化快速排序。在简化的伪代码中:
// I tweaked this number a bunch. Anything smaller than this and the
// overhead is smaller than the parallelization gains.
const smallestToParallelize = 500;
void quickSort(T)(T[] array) {
if(array.length < someConstant) {
insertionSort(array);
return;
}
size_t pivotPosition = partition(array);
if(array.length >= smallestToParallelize) {
// Sort left subarray in a task pool thread.
auto myTask = taskPool.execute(quickSort(array[0..pivotPosition]));
quickSort(array[pivotPosition + 1..$]);
myTask.workWait();
} else {
// Regular serial quick sort.
quickSort(array[0..pivotPosition]);
quickSort(array[pivotPosition + 1..$]);
}
}
即使对于非常大的阵列,第一个分区所花费的时间可以忽略不计,与纯串行版本的算法相比,我只能在双核上获得大约 30% 的加速。我猜瓶颈是共享内存访问。关于如何消除这个瓶颈或瓶颈可能是什么的任何见解?
编辑:我的任务池有固定数量的线程,等于系统中的核心数减去 1(因为主线程也可以工作)。此外,我使用的等待类型是工作等待,即如果任务已启动但尚未完成,则调用线程workWait()
会从池中窃取其他作业并执行它们,直到它正在等待的作业完成。如果任务未启动,则在当前线程中完成。这意味着等待并不是低效的。只要有工作要做,所有线程都会保持忙碌状态。