【问题标题】:Most efficient number of goroutines on this machine这台机器上最有效的 goroutine 数量
【发布时间】:2017-11-29 23:08:26
【问题描述】:

所以我确实有一个由我编写的并发快速排序实现。它看起来像这样:

func Partition(A []int, p int, r int) int {
    index := MedianOf3(A, p, r)
    swapArray(A, index, r)
    x := A[r]
    j := p - 1
    i := p
    for i < r {
        if A[i] <= x {
            j++
            tmp := A[j]
            A[j] = A[i]
            A[i] = tmp
        }
        i++
    }
    swapArray(A, j+1, r)
    return j + 1
}


func ConcurrentQuicksort(A []int, p int, r int) {
    wg := sync.WaitGroup{}
    if p < r {
        q := Partition(A, p, r)
        select {
        case sem <- true:
            wg.Add(1)
            go func() {
                ConcurrentQuicksort(A, p, q-1)
                <-sem
                wg.Done()
            }()
        default:
            Quicksort(A, p, q-1)
        }
        select {
        case sem <- true:
            wg.Add(1)
            go func() {
                ConcurrentQuicksort(A, q+1, r)
                <-sem
                wg.Done()
            }()
        default:
            Quicksort(A, q+1, r)
        }
    }
    wg.Wait()
}

func Quicksort(A []int, p int, r int) {
    if p < r {
        q := Partition(A, p, r)
        Quicksort(A, p, q-1)
        Quicksort(A, q+1, r)
    }
}

我有一个 sem 缓冲通道,我用它来限制运行的 goroutine 的数量(如果它达到这个数量,我不会设置另一个 goroutine,我只是对子数组进行正常的快速排序)。首先我从 100 开始,然后我更改为 50、20。基准会稍微好一些。但切换到10后,它开始倒退,时间开始变大。因此,至少对于我的硬件而言,有一些任意数字使算法运行效率最高。

当我实现这个时,我实际上看到了一些关于最好的 goroutine 数量的问题,现在我找不到它(愚蠢的 Chrome 历史实际上并没有保存所有访问过的网站)。你知道如何计算这样的事情吗?如果我不必对其进行硬编码,那将是最好的,只需让程序自己完成即可。

P.S 我有非并发快速排序,它的运行速度比这慢大约 1.7 倍。正如您在我的代码中看到的那样,当运行的 goroutine 的数量超过我之前设置的数量时,我会执行 Quicksort。我想使用ConcurrentQuicksort 怎么样,但不使用go 关键字调用它,只是简单地调用它,也许如果其他goroutine 完成它们的工作,我调用的ConcurrentQuicksort 将开始启动goroutines,加速这个过程(因为你可以看到Quicksort 只会启动递归快速排序,没有 goroutines)。我这样做了,实际上时间比常规快速排序慢了 10%。你知道为什么会这样吗?

【问题讨论】:

  • 我玩过并发排序 (github.com/twotwotwo/sorts)。我认为这里的第一原则没有明确的答案。您只需观察转换成本是多少并调整工作单元的大小,这样您就不会花费太多的资源来处理任务。
  • 好的,谢谢。你知道我在 PS 部分放的东西吗?
  • 这可能是stackoverflow.com/q/8509152/4639336的可能副本
  • @reticentroot 因为问题是什么影响并行快速排序的效率,而不是 Go 运行时的限制,可以说是不同的
  • 因此可能不是我的直接标志,尽管它确实显示了一些有趣的代码来对 goroutines 进行基准测试......另外你是如何进行基准测试的?希望通过 go test =bench 运行一些函数,如果没有看到这篇文章 dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go 在做你可以快速测试并找到“甜蜜”点

标签: go concurrency goroutine


【解决方案1】:

您必须对这些东西进行一些试验,但我认为主要问题不是 goroutines 一次运行。正如@retcentroot 链接到的答案所说,it's not necessarily a problem to run a lot of simultaneous goroutines

我认为您主要关心的应该是 goroutine 启动的总数。当前的实现理论上可以启动一个 goroutine 来对几个项目进行排序,并且该 goroutine 将花费比实际排序更多的时间在启动/协调上。

理想情况是,您只需启动尽可能多的 goroutine,以充分利用所有 CPU。如果您的工作项目大小相同,核心也同样繁忙,那么每个核心启动一项任务是完美的。

在这里,任务 大小不均,因此您可以将排序拆分为 比您拥有的 CPU 更多的任务并分配它们。 (在生产环境中,您通常会使用 worker pool 来分配工作,而无需为每个任务启动一个新的 goroutine,但我认为我们可以在这里跳过它。)

要获得可行的任务数量——足以让所有核心保持忙碌,但又不会太多导致产生大量开销——你可以设置一个最小大小(初始数组大小/100 或其他),并且只拆分关闭比这更大的数组。


稍微详细一点,每次将任务发送到后台都会产生一些成本。对于初学者:

  • 每个 goroutine launch 都会花费一些时间来设置堆栈并进行调度器记账
  • 每个任务切换都会在调度程序中花费一些时间,当两个 goroutine 查看不同的代码或数据时,可能会产生cache misses
  • 您自己的协调码(频道发送和sync ops)需要时间

其他因素可能会阻止理想的加速发生:您可能会达到系统范围的限制,例如正如沃尔克指出的那样,内存带宽会随着您添加内核而增加一些同步成本,有时您可能会遇到various更棘手的issues。但是设置、切换和协调成本是一个很好的起点。

当然,可以超过协调成本的好处是,其他 CPU 可以在原本空闲的情况下完成工作。

我认为,但尚未测试,您在 50 个 goroutine 上的问题是 1)您很久以前就已经达到了几乎完全的利用率,因此添加更多任务会增加更多的协调工作,而不会让事情变得更快,以及 2)您重新为 tiny 排序创建 goroutine,这可能会花费更多的时间来设置和协调,而不是实际进行排序。在 10 个 goroutine 时,您的问题可能是您无法再达到 CPU 的全部利用率。

如果您愿意,您可以通过计算在各种 goroutine 限制(an atomic global counter)下的总 goroutine 启动次数和测量各种限制下的 CPU 利用率(例如通过在 Linux/UNIX 下运行程序 @987654333)来测试这些理论@实用程序)。

对于像这样的分而治之问题,我建议的方法是只为足够大的子问题分叉一个 goroutine(对于快速排序,这意味着足够大的子数组)。您可以尝试不同的限制:也许您只为超过原始数组 1/64 的片段启动 goroutine,或者超过某个静态阈值(如 1000 个项目)的片段。


你的意思是这个排序例程是一种练习,我怀疑,但是你可以做很多事情来让你的排序更快或更健壮地对抗奇怪的输入。 The standard libary sort 回退到小子数组的插入排序,并使用堆排序来处理导致快速排序问题的异常数据模式。

您还可以查看其他算法,例如用于全部或部分排序的基数排序,which I played with。该排序库也是并行的。在我将一个子数组交给其他 goroutine 进行排序之前,我使用了至少 127 个项目的截止值,我使用了an arrangement with a fixed pool of goroutines and a buffered chan to pass tasks between them。这在当时产生了不错的实际加速,尽管它可能不是当时最好的方法,而且我几乎可以肯定它不在今天的 Go 调度程序上。实验很有趣!

【讨论】:

  • 说得好!也许值得说明的是,启动比实际上可以并行执行这些 goroutine 的内核更多的 goroutine 可能不会导致任何加速(正如您所解释的那样,协调开销会增加,而实际排序不会)。还有一点可能值得一提:goroutines 正在竞争内存访问。根据您的排序(int 很小,其他数据结构可能更大甚至在内存中甚至不连续),如果启动了太多的 goroutine,这将产生巨大的影响。
  • 我从事内存带宽方面的工作,一般来说你不能指望线性加速。我修改了 NumCPU 的东西,但很难找到准确的词:显然 >NumCPU 任务并不意味着更多的并行性,但是将一个排序分成 8 个内核上的 24 个任务给调度程序提供了一种更均匀地分配工作的方法尽管各个快速排序子任务的大小分布不均(因为分区不会给你完美的一半),但跨核心。工作池会更好(并提到过),但可以容忍稍微太多的 goroutine 进行平衡。
  • 你是对的。但怀疑 OP 是否可以访问 2048 CPU NUMA 机器:-)
  • 看看这个:play.golang.org/p/lf-0lzUSKi 不管我如何实际限制运行的 goroutines 的数量,ConcSort 总是比NormalSort 慢。是不是很奇怪?
  • 您希望 go func(i int) {sort.Ints(AA[i]); wg.Done() &lt;-sem}(i) -- 使 i 成为您的闭包的显式参数以避免 this issue with var capture&lt;-sem 因为看起来您希望接收您最终发送的那些 true 值.我认为如果您在最新的 Go 上运行并且子数组很大(尝试,例如,每个 100K),这将有所帮助。但同样,并发 goroutine 并不是你想要关注的数量,而是你是否在后台处理足够大的工作是否值得。
【解决方案2】:

如果操作是CPU bounded,我的实验表明最佳的是CPU 数量。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-09-02
    • 1970-01-01
    • 1970-01-01
    • 2012-01-20
    • 1970-01-01
    • 1970-01-01
    • 2010-12-16
    • 1970-01-01
    相关资源
    最近更新 更多