簡體   English   中英

Golang:為什么使用goroutines並行化調用會導致速度變慢?

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM