我有两个版本的合并排序实现。第一个是“正常”版本,第二个使用goroutines
,它在递归的每个步骤中并行化对切片的每个子集所做的工作。
可以假设能够并行化这项工作会使并发实现更快:如果我需要处理切片A和切片B,那么同时处理它们应该比同步执行它们更快。
现在我假设我的理解实现有问题,因为我的并发版本最终比同步版本慢13-14倍。
任何人都可以指出我正确的方向,我错过了什么?
“正常”(同步实施):
// MergeSort sorts the slice s using Merge Sort Algorithm
func MergeSort(s []int) []int {
if len(s) <= 1 {
return s
}
n := len(s) / 2
var l []int
var r []int
l = MergeSort(s[:n])
r = MergeSort(s[n:])
return merge(l, r)
}
“并发”版本:
// MergeSortMulti sorts the slice s using Merge Sort Algorithm
func MergeSortMulti(s []int) []int {
if len(s) <= 1 {
return s
}
n := len(s) / 2
wg := sync.WaitGroup{}
wg.Add(2)
var l []int
var r []int
go func() {
l = MergeSortMulti(s[:n])
wg.Done()
}()
go func() {
r = MergeSortMulti(s[n:])
wg.Done()
}()
wg.Wait()
return merge(l, r)
}
两者都使用相同的merge
函数:
func merge(l, r []int) []int {
ret := make([]int, 0, len(l)+len(r))
for len(l) > 0 || len(r) > 0 {
if len(l) == 0 {
return append(ret, r...)
}
if len(r) == 0 {
return append(ret, l...)
}
if l[0] <= r[0] {
ret = append(ret, l[0])
l = l[1:]
} else {
ret = append(ret, r[0])
r = r[1:]
}
}
return ret
}
这是我的基准代码:
package msort
import "testing"
var a []int
func init() {
for i := 0; i < 1000000; i++ {
a = append(a, i)
}
}
func BenchmarkMergeSortMulti(b *testing.B) {
for n := 0; n < b.N; n++ {
MergeSortMulti(a)
}
}
func BenchmarkMergeSort(b *testing.B) {
for n := 0; n < b.N; n++ {
MergeSort(a)
}
}
它揭示了并发版本比普通同步版本慢很多:
BenchmarkMergeSortMulti-8 1 1711428093 ns/op
BenchmarkMergeSort-8 10 131232885 ns/op
答案 0 :(得分:3)
这是因为你在调用wg.Wait()
时会产生大量的goroutine。调度程序不知道选择哪一个,它可以选择随机阻止的那个,直到它遇到一个最终可以返回并解除另一个。当我限制MergeSortMulti的并发调用次数时,它变得比同步版本快大约3倍。
这段代码并不美观,但这是一个证明。
// MergeSortMulti sorts the slice s using Merge Sort Algorithm
func MergeSortMulti(s []int) []int {
if len(s) <= 1 {
return s
}
n := len(s) / 2
wg := sync.WaitGroup{}
wg.Add(2)
var l []int
var r []int
const N = len(s)
const FACTOR = 8 // ugly but easy way to limit number of goroutines
go func() {
if n < N/FACTOR {
l = MergeSort(s[:n])
} else {
l = MergeSortMulti(s[:n])
}
wg.Done()
}()
go func() {
if n < N/FACTOR {
r = MergeSort(s[n:])
} else {
r = MergeSortMulti(s[n:])
}
wg.Done()
}()
wg.Wait()
return merge(l, r)
}
您的计算机上的结果会有所不同,但是:
因素= 4:
BenchmarkMergeSortMulti-8 50 33268370 ns/op
BenchmarkMergeSort-8 20 91479573 ns/op
因素= 10000
BenchmarkMergeSortMulti-8 20 84822824 ns/op
BenchmarkMergeSort-8 20 103068609 ns/op
因子= N / 4
BenchmarkMergeSortMulti-8 3 352870855 ns/op
BenchmarkMergeSort-8 10 129107177 ns/op
加分:你也可以使用信号量来限制我机器上慢一点的goroutine数量(选择用于避免死锁):
var sem = make(chan struct{}, 100)
// MergeSortMulti sorts the slice s using Merge Sort Algorithm
func MergeSortMulti(s []int) []int {
if len(s) <= 1 {
return s
}
n := len(s) / 2
wg := sync.WaitGroup{}
wg.Add(2)
var l []int
var r []int
select {
case sem <- struct{}{}:
go func() {
l = MergeSortMulti(s[:n])
<-sem
wg.Done()
}()
default:
l = MergeSort(s[:n])
wg.Done()
}
select {
case sem <- struct{}{}:
go func() {
r = MergeSortMulti(s[n:])
<-sem
wg.Done()
}()
default:
r = MergeSort(s[n:])
wg.Done()
}
wg.Wait()
return merge(l, r)
}
它产生:
BenchmarkMergeSortMulti-8 30 36741152 ns/op
BenchmarkMergeSort-8 20 90843812 ns/op
答案 1 :(得分:1)
您的假设不正确:
人们会认为能够并行化这项工作 并发实现更快:如果我需要处理切片A和 切片B,然后同时处理它们应该比做更快 如此同步。
所有并行软件属于Amdahl定律(on Wikipedia),我可以将其解释为“顺序设置不是免费的”。
当仅使用单个 CPU内核时,这当然尤其如此。但是,即使有多个核心,它仍然很重要,如果要实现高性能,则需要通过来考虑核心工作单元的编排和分布。幸运的是,kopiczko的答案在具体案例中提供了一些很好的建议。
几十年来,这一直是一个研究课题:例如, Tidmus和Chalmers在“实际并行处理:并行解决问题的介绍”中的一个旧的(但仍然相关)的交易技巧摘要。