所以我确实有一个由我编写的并发quicksort实现。它看起来像这样:
au FileType join(filetypesWithTag, ',') EmmetInstall
我有一个func Partition(A []int, p int, r int) int {
index := MedianOf3(A, p, r)
swapArray(A, index, r)
x := A[r]
j := p - 1
i := p
for i < r {
if A[i] <= x {
j++
tmp := A[j]
A[j] = A[i]
A[i] = tmp
}
i++
}
swapArray(A, j+1, r)
return j + 1
}
func ConcurrentQuicksort(A []int, p int, r int) {
wg := sync.WaitGroup{}
if p < r {
q := Partition(A, p, r)
select {
case sem <- true:
wg.Add(1)
go func() {
ConcurrentQuicksort(A, p, q-1)
<-sem
wg.Done()
}()
default:
Quicksort(A, p, q-1)
}
select {
case sem <- true:
wg.Add(1)
go func() {
ConcurrentQuicksort(A, q+1, r)
<-sem
wg.Done()
}()
default:
Quicksort(A, q+1, r)
}
}
wg.Wait()
}
func Quicksort(A []int, p int, r int) {
if p < r {
q := Partition(A, p, r)
Quicksort(A, p, q-1)
Quicksort(A, q+1, r)
}
}
缓冲通道,我用来限制运行的goroutines的数量(如果它达到那个数字,我不设置另一个goroutine,我只是在子阵列上执行正常的快速排序)。首先我从100开始,然后我改为50,20。基准测试会稍微好一些。但在切换到10后,它开始回归,时间开始变大。所以有一些任意数字,至少对我的硬件而言,使算法运行效率最高。
当我实现这个时,我实际上看到了一些关于最好的goroutines数量的问题,现在我找不到它(愚蠢的Chrome历史记录实际上并没有保存所有访问过的网站)。你知道如何计算这样的东西吗?如果我不必对其进行硬编码,那将是最好的,只需让程序自己完成。
P.S我有非并发的Quicksort,比这个慢约1.7倍。正如你在我的代码中看到的那样,当运行的goroutine的数量超过我之前设置的数量时,我会sem
。我想过如何使用Quicksort
,而不是用ConcurrentQuicksort
关键字调用它,只是简单地调用它,也许如果其他goroutines完成了他们的工作,我调用的go
将开始启动goroutines,加快进程(因为你可以看到ConcurrentQuicksort
只会启动递归快速排序,而不需要goroutines)。我做到了,实际上时间比常规Quicksort慢10%。你知道为什么会这样吗?
答案 0 :(得分:4)
你必须对这些东西进行一些实验,但我不认为主要关注的是 goroutines一次运行。正如答案@reticentroot所说,it's not necessarily a problem to run a lot of simultaneous goroutines。
我认为您的主要关注点应该是 goroutine启动的总数。理论上,当前的实现可以启动一个goroutine来排序几个项目,并且goroutine将花费更多的时间在启动/协调上而不是实际排序。
理想的情况是,您只需启动尽可能多的goroutine,以便充分利用所有CPU。如果您的工作项目大小相等且核心数量相等,则每个核心启动一项任务是完美的。
此处,任务的大小不均匀,因此您可以将排序拆分为稍微多于的任务,而不是拥有CPU并分发它们。 (在制作中,您通常会使用worker pool来分发作品,而无需为每项任务启动新的goroutine,但我认为我们可以在此处跳过它。)
要获得可执行数量的任务 - 足以让所有核心保持忙碌,但不要太多以至于创造了大量开销 - 您可以设置最小大小(初始数组大小/ 100或其他),并且只能分割各种大于此的阵列。
稍微详细一点,每次将任务发送到后台时都会产生一些成本。对于初学者:
sync
操作)需要时间其他因素可能会阻止理想的加速发生:您可以达到全系统限制,例如: Volker指出,内存带宽会随着你添加内核而增加一些同步成本,有时你会遇到various棘手的issues。但设置,转换和协调成本是一个很好的起点。
当然,其他CPU可以在闲置时完成工作,这可以超过协调成本。
我认为,但尚未经过测试,50个goroutines的问题是1)你很久以前已经达到了近乎完全的利用率,所以添加更多的任务会增加更多的协调工作而不会让事情变得更快,2)你们为 tiny 排序创建goroutines,这可能会花费更多的时间来设置和协调,而不是实际排序。在10个goroutines,您的问题可能是您不再实现完全CPU利用率。
如果您愿意,可以通过计算各种goroutine限制(在an atomic global counter中)的总goroutine启动次数并在各种限制下测量CPU利用率来测试这些理论(例如,通过在Linux / UNIX下运行程序) time
实用程序)。
我建议像这样的分而治之的问题的方法是只为一个足够大的子问题分离goroutine (对于quicksort,这意味着足够大的子阵列)。您可以尝试不同的限制:也许您只能为超过原始数组的1/64的碎片启动goroutines,或者超过某些静态阈值(如1000个项目)。
你怀疑,这意味着这种例程是一种练习,但是你可以做各种各样的事情来使你的排序更快或更强大,以防止奇怪的输入。 The standard libary sort回退到小子阵列的插入排序,并使用heapsort来处理导致快速排序问题的异常数据模式。
您还可以查看其他算法,例如全局或部分排序的基数排序which I played with。那个排序库也是平行的。在我将一个子阵列交给其他goroutine进行排序之前,我最终截止了127个项目,我使用了an arrangement with a fixed pool of goroutines and a buffered chan to pass tasks between them。这在当时产生了不错的实际加速,尽管我很难确定我找到了最佳方法,而且自从我编写包之后,Go的调度程序已经发展。实验很有意思!
答案 1 :(得分:0)
如果操作是CPU 有界,我的实验表明最优的是 CPU 的数量。