在并行快速排序实现中使用go例程时性能更差

时间:2015-03-20 16:29:32

标签: concurrency go parallel-processing quicksort goroutine

注意:“Go-lang并行片段比系列片段运行速度慢”处理竞争条件的问题,这个问题有另一个问题,所以imho它不是重复的。

我正在尝试找到以下情况的解释: 使用go例程运行并行快速排序会导致运行时间显着延长。

基准在代码之后:

package c9sort

import (
    "time"
)

var runInParllel bool

func Quicksort(nums []int, parallel bool) ([]int, int) {
    started := time.Now()
    ch := make(chan int)
    runInParllel = parallel

    go quicksort(nums, ch)

    sorted := make([]int, len(nums))
    i := 0
    for next := range ch {
        sorted[i] = next
        i++
    }
    return sorted, int(time.Since(started).Nanoseconds() / 1000000)
}

func quicksort(nums []int, ch chan int) {

    // Choose first number as pivot
    pivot := nums[0]

    // Prepare secondary slices
    smallerThanPivot := make([]int, 0)
    largerThanPivot := make([]int, 0)

    // Slice except pivot
    nums = nums[1:]

    // Go over slice and sort
    for _, i := range nums {
        switch {
        case i <= pivot:
            smallerThanPivot = append(smallerThanPivot, i)
        case i > pivot:
            largerThanPivot = append(largerThanPivot, i)
        }
    }

    var ch1 chan int
    var ch2 chan int

    // Now do the same for the two slices
    if len(smallerThanPivot) > 1 {
        ch1 = make(chan int, len(smallerThanPivot))
        if runInParllel {
            go quicksort(smallerThanPivot, ch1)
        } else {
            quicksort(smallerThanPivot, ch1)
        }
    }
    if len(largerThanPivot) > 1 {
        ch2 = make(chan int, len(largerThanPivot))
        if runInParllel {
            go quicksort(largerThanPivot, ch2)
        } else {
            quicksort(largerThanPivot, ch2)
        }
    }

    // Wait until the sorting finishes for the smaller slice
    if len(smallerThanPivot) > 1 {
        for i := range ch1 {
            ch <- i
        }
    } else if len(smallerThanPivot) == 1 {
        ch <- smallerThanPivot[0]
    }
    ch <- pivot

    if len(largerThanPivot) > 1 {
        for i := range ch2 {
            ch <- i
        }
    } else if len(largerThanPivot) == 1 {
        ch <- largerThanPivot[0]
    }

    close(ch)
}

随机烫发500000个整数的基准:

跑了100次

非平行平均值 - 1866ms

平行平均值 - 2437毫秒

任何解释都将不胜感激。我知道goroutines对于这种并行性可能不是最好的,但我试图理解其中的原因。

提前谢谢。

2 个答案:

答案 0 :(得分:6)

一般的答案是线程间协调有成本,因此将任务发送到另一个goroutine只能在任务至少达到一定大小时加快速度。所以不要发送单品。

对于像quicksort这样的分而治之算法,并行化的方法可能很有趣。一般来说:当你递归时,你可以开始对阵列的一半进行排序&#34;如果它足够大,可以在goroutine中完成任务。 &#34;如果它足够大&#34;部分是如何减少协调开销。

更详细地说,看起来像:

  1. 撰写非平行qsort(data []int)

  2. 更改qsort以选择性地接听*sync.WaitGroup我们会致电wg以表示已完成此操作。在每个if wg != nil { wg.Done() }之前添加return

  3. qsort递归,让它检查它要排序的数据的一半是不是很大(比如超过500项?),

    • 如果它很大,则启动另一项任务:wg.Add(1); go qsort(half, wg)
    • 如果不是,请在此处排序:qsort(half, nil)
  4. 包装qsort以隐藏用户的并行机制:例如,让quicksort(data)执行wg := new(sync.WaitGroup),执行初始wg.Add(1),调用{{1然后执行qsort(data, wg)等待所有后台排序完成。

  5. 这不是一个最佳策略,因为即使在有足够的任务来保持核心忙碌之后,它也会继续分离新的goroutine。只有如何将一些任务交给后台,才能做很多调整。但重要的是,使用另一个线程仅用于大型子任务是一种并行化快速排序的方法,而不会受到协调开销的影响。

    这里有一个demo with an embarrassingly slapdash quicksort(你已经在本地运行以获得时间) - 很有可能出现错误,绝对是已排序数据的二次方;关键是要实现并行分治的想法。

    还有一种自下而上的策略 - 排序然后合并 - 例如,用于this parallel sort package。但是,包使用的就地合并代码很棘手。

    (如果您还没有设置GOMAXPROCS,请在wg.Wait()中使用runtime.GOMAXPROCS(runtime.NumCPU())设置它。)

    最后,look at Go's sort package。有很多代码,但它非常清楚,一旦你得到它,你就可以用真正的&#34;,充实的排序实现进行实验。

答案 1 :(得分:2)

原来这很简单。当我在新机器上时,GOMAXPROCS变量没有设置。

正如预测的那样,新基准有利于并行实施: 设置为核心数量的两倍:

Using 16 goroutines
Ran 100 times

Non parallel average - 1980
Parallel average - 1133

设置为核心数:

Using 8 goroutines
Ran 100 times

Non parallel average - 2004
Parallel average - 1197

顺便说一句,这是相当一致的。内核数量增加一倍的平均值总是好一点。

更大集合的基准(1000000):

Using 8 goroutines
Ran 100 times

Non parallel average - 3748
Parallel average - 2265

双击:

Using 16 goroutines
Ran 100 times

Non parallel average - 3817
Parallel average - 2012