为了账号安全,请及时绑定邮箱和手机立即绑定

为什么添加并发会使该golang代码变慢?

为什么添加并发会使该golang代码变慢?

Go
鸿蒙传说 2021-05-13 13:19:44
我已经修改了一些Go代码,以解决与我姐夫玩的视频游戏有关的我的好奇心。本质上,下面的代码模拟了游戏中与怪物的互动,以及他期望他们在失败后掉落物品的频率。我遇到的问题是,我希望像这样的一段代码对于并行化来说是完美的,但是当我并发添加时,完成所有模拟所花费的时间往往会使原始代码的速度降低4-6倍。没有并发。为了使您更好地理解代码的工作方式,我有三个主要功能:交互功能,它是玩家和怪物之间的简单交互。如果怪物掉落物品,则返回1,否则返回0。仿真功能运行多个交互,并返回一部分交互结果(即1和0表示成功/不成功的交互)。最后,有一个测试函数,它运行一组模拟并返回一部分模拟结果,这些结果是导致掉落物品的相互作用的总数。这是我尝试并行运行的最后一个函数。现在,我可以理解,如果我为要运行的每个测试创建一个goroutine,为什么代码会变慢。假设我正在运行100个测试,则在MacBook Air的4个CPU上的每个goroutine之间进行上下文切换会降低性能,但是我只创建与我拥有的处理器一样多的goroutine,并将测试次数除以goroutines。我希望这实际上可以提高代码的性能,因为我可以并行运行每个测试,但是,当然,我会遇到严重的问题。我很想弄清楚为什么会这样,所以任何帮助将不胜感激。以下是不带go例程的常规代码:package mainimport (    "fmt"    "math/rand"    "time")const (    NUMBER_OF_SIMULATIONS = 1000    NUMBER_OF_INTERACTIONS = 1000000    DROP_RATE = 0.0003)/** * Simulates a single interaction with a monster * * Returns 1 if the monster dropped an item and 0 otherwise */func interaction() int {    if rand.Float64() <= DROP_RATE {        return 1    }    return 0}/** * Runs several interactions and retuns a slice representing the results */func simulation(n int) []int {    interactions := make([]int, n)    for i := range interactions {        interactions[i] = interaction()    }    return interactions}/** * Runs several simulations and returns the results */func test(n int) []int {    simulations := make([]int, n)    for i := range simulations {        successes := 0        for _, v := range simulation(NUMBER_OF_INTERACTIONS) {            successes += v        }        simulations[i] = successes    }    return simulations}func main() {    rand.Seed(time.Now().UnixNano())    fmt.Println("Successful interactions: ", test(NUMBER_OF_SIMULATIONS))}并且,这是带有goroutines的并发代码:package mainimport (    "fmt"    "math/rand"    "time"    "runtime")const (    NUMBER_OF_SIMULATIONS = 1000    NUMBER_OF_INTERACTIONS = 1000000    DROP_RATE = 0.0003)
查看完整描述

3 回答

?
守着一只汪

TA贡献1872条经验 获得超3个赞

在我的Linux四核i7笔记本电脑上测试您的代码我明白了

这是一个Google Spreadsheet

//img1.sycdn.imooc.com//60b44441000185c305620081.jpg

这表明,在Linux下,至少每个内核的缩放比例几乎是线性的。

我认为您可能没有看到这有两个原因。

首先是您的macbook air仅具有2个真正的核心。它有4个超线程,这就是为什么它报告4个最大cpus的原因。通常,超线程仅在单个内核上提供15%的额外性能,而不是您期望的100%。因此,仅在macbook air上基准测试1个或2个CPU!

另一个原因可能是与Linux相比OS X线程的性能。他们使用不同的线程模型,这可能会影响性能。


查看完整回答
反对 回复 2021-05-31
?
MMTTMM

TA贡献1869条经验 获得超4个赞

您的代码正在抽样一个二项式随机变量B(N,p),其中N是试验次数(此处为1M),p是成功进行单个试验的概率(此处为0.0003)。


一种方法是建立一个累积概率表T,其中T [i]包含试验总数小于或等于i的概率。要生成样本,您可以选择一个统一的随机变量(通过rand.Float64),然后在表中找到包含大于或等于该概率的第一个索引。


这里有点复杂,因为您有一个非常大的N和一个相当小的p,因此,如果您尝试构建表,那么数字和算术精度就会非常麻烦。但是您可以构建一个较小的表(假设有1000个大表)并对其进行1000次采样,以进行一百万次试用。


这是完成所有这些工作的一些代码。它不太优雅(1000 是硬编码的),但它在我的旧笔记本电脑上不到一秒钟就生成了 1000 次模拟。通过例如将BinomialSampler的构造从循环中移出,或者通过使用二进制搜索而不是线性扫描来查找表索引,可以很容易地进一步优化。


package main


import (

    "fmt"

    "math"

    "math/rand"

)


type BinomialSampler []float64


func (bs BinomialSampler) Sample() int {

    r := rand.Float64()

    for i := 0; i < len(bs); i++ {

        if bs[i] >= r {

            return i

        }

    }

    return len(bs)

}


func NewBinomialSampler(N int, p float64) BinomialSampler {

    r := BinomialSampler(make([]float64, N+1))

    T := 0.0

    choice := 1.0

    for i := 0; i <= N; i++ {

        T += choice * math.Pow(p, float64(i)) * math.Pow(1-p, float64(N-i))

        r[i] = T

        choice *= float64(N-i) / float64(i+1)

    }

    return r

}


func WowSample(N int, p float64) int {

    if N%1000 != 0 {

        panic("N must be a multiple of 1000")

    }

    bs := NewBinomialSampler(1000, p)

    r := 0

    for i := 0; i < N; i += 1000 {

        r += bs.Sample()

    }

    return r

}


func main() {

    for i := 0; i < 1000; i++ {

        fmt.Println(WowSample(1000000, 0.0003))

    }

}


查看完整回答
反对 回复 2021-05-31
  • 3 回答
  • 0 关注
  • 226 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信