[英]Golang: why using goroutines to parallelize calls ends up being slower?
我有兩種版本的合並排序實現。 第一個是“常規”版本,第二個使用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
這是因為您會生成大量goroutine,這些goroutine在調用wg.Wait()
時會搶占。 調度程序不知道該選擇哪個,它可以選擇隨機阻止的對象,直到遇到一個最終可以返回並取消阻止另一個對象的對象為止。 當我限制同時調用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的數量,這在我的機器上會稍慢一些(使用select可以避免死鎖):
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
您的假設不正確:
人們會假設能夠並行化這項工作將使並發實現更快:如果我需要處理片A和片B,那么並發處理它們應該比同步處理要快。
所有並行軟件均受阿姆達爾定律( 在Wikipedia上 )的限制,我可以將其解釋為“ 順序設置不是免費的 ”。
當然,僅使用單個 CPU內核時尤其如此。 但是,即使對於多個內核,這仍然很重要,如果要實現高性能,則需要考慮跨內核的工作編排和分配。 幸運的是,kopiczko的答案為有關特定案例提供了一些很好的技巧。
數十年來,這一直是一個研究主題:例如,參見Tidmus和Chalmers撰寫的“ 實用並行處理:並行解決問題的簡介 ”中有關交易技巧的舊的(但仍然相關)摘要。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.