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

Concurrency 在并行快速排序实现中使用go例程时性能较差,concurrency,go,parallel-processing,quicksort,goroutine,Concurrency,Go,Parallel Processing,Quicksort,Goroutine,注:“Go-lang并行段运行速度比串行段慢”的问题涉及比赛条件,这一问题还有另一个问题,因此我认为这不是重复问题 我试图为以下情况找到解释: 使用go例程运行并行快速排序会导致运行时间显著延长 基准测试在代码之后: package c9sort import ( "time" ) var runInParllel bool func Quicksort(nums []int, parallel bool) ([]int, int) { started := time.No

注:“Go-lang并行段运行速度比串行段慢”的问题涉及比赛条件,这一问题还有另一个问题,因此我认为这不是重复问题

我试图为以下情况找到解释: 使用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)
}
包c9sort
进口(
“时间”
)
变量runInParllel bool
func快速排序(nums[]int,parallel bool)([]int,int){
开始:=时间。现在()
ch:=制造(成交量)
runInParllel=并行
快速排序(nums,ch)
排序:=make([]整数,len(nums))
i:=0
对于下一个:=范围ch{
已排序的[i]=下一个
我++
}
返回排序,int(时间.自(开始).纳秒()/1000000)
}
func快速排序(nums[]整数,ch chan整数){
//选择第一个数字作为轴
枢轴:=nums[0]
//准备二次切片
smallerThanPivot:=make([]整数,0)
largerThanPivot:=make([]整数,0)
//除枢轴外的切片
nums=nums[1:]
//检查切片并分类
对于u,i:=范围nums{
开关{
案例一:
largerThanPivot=追加(largerThanPivot,i)
}
}
var ch1 chan int
var ch2 chan int
//现在对这两片做同样的处理
如果len(小枢轴)>1{
ch1=制造(成片内、透镜(小轴))
如果运行inparllel{
go快速排序(小数据透视,ch1)
}否则{
快速排序(小数据透视,ch1)
}
}
如果len(大于枢轴)>1{
ch2=制造(成交量、长度(大于枢轴))
如果运行inparllel{
go快速排序(大于枢轴,ch2)
}否则{
快速排序(大于枢轴,ch2)
}
}
//等待较小切片的排序完成
如果len(小枢轴)>1{
对于i:=范围ch1{

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

对于像quicksort这样的分而治之算法,并行化的方法可能很有趣。一般来说:当您递归时,如果goroutine足够大,您可以在goroutine中启动“排序数组的一半”任务。“如果足够大”部分是如何减少协调开销的

更详细地说,这看起来像:

  • 编写一个非并行的
    qsort(data[]int)

  • qsort
    更改为可选地采取一个我们将调用
    wg
    的方法,以发出它已完成的信号。如果wg!=nil{wg.done()}
  • 在它的每个
    返回之前添加

  • qsort
    递归的地方,让它检查要排序的数据的一半是否很大(比如,超过500项?),以及

    • 如果它很大,则启动另一个任务:
      wg.Add(1);go qsort(half,wg)
    • 如果不是,请立即排序:
      qsort(half,nil)
  • 包装
    qsort
    以向用户隐藏并行机器:例如,让
    quicksort(data)
    do
    wg:=new(sync.WaitGroup)
    ,进行初始
    wg.Add(1)
    ,调用
    qsort(data,wg)
    ,并执行
    wg.Wait()
    以等待所有后台排序完成

  • 这不是一个最佳策略,因为即使在有足够的任务让核心保持忙碌的情况下,它也会不断地派生新的goroutine。而且,围绕如何将某些任务转移到后台,人们可以做很多调整。但重要的是,仅对大型子任务使用另一个线程是一种并行化快速排序而不必进行任何调整的方法这是由头顶上的协调造成的

    这里有一个(您已经在本地运行了它以获得计时)——bug的好机会,在已经排序的数据上肯定是二次的;关键是要获得并行分治的思想

    还有一种自下而上的策略——先对片段进行排序,然后合并——例如,在中使用。不过,包使用的就地合并代码很棘手

    (如果尚未设置GOMAXPROCS,请在
    main
    中使用类似于
    runtime.GOMAXPROCS(runtime.numpu())
    的内容进行设置)


    最后,.有很多代码,但很清楚,一旦你得到了它,你就可以用一个“真实的”,充实的排序实现来做你的实验。

    结果证明它非常简单。因为我在一台新机器上,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
    
    设置为核心数:

    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 16 goroutines
    Ran 100 times
    
    Non parallel average - 3817
    Parallel average - 2012
    

    可能的重复显然有相似之处,但是(在另一个q上写下接受的答案之后!)我不确定这是重复,因为对于像quicksort这样的递归对象,修复看起来与平面“对数组中的每个项进行一些分析”截然不同任务。你的GOMAXPROCS是什么?发布了一个策略,希望在我有时间的时候用示例代码来充实它。(为了清晰起见,我想从一个最小的序列
    qsort
    开始,所以这需要一点时间。)是我写的一个并行快速排序,只是为了好玩,它滥用了go并发机制…在检查了所有内容后,原因很简单,不在算法本身。此外,我不使用排序包的原因是我在提供go课程,这是一个挑战。我想了解这个类的问题。你知道吗不只是想。根据每个程序设置它,或者对于某些程序,它可能适合使用(可能通过程序的
    -cpu
    -ncpu
    标志)。我不确定我是否同意,因为“这