为什么添加并发性降低了这个golang代码?
我已经有了一些Go代码,我一直在琢磨着回答我的一个小小的好奇心,这个代码跟我的姐夫在玩的video游戏有关。
本质上,下面的代码模拟了与游戏中的怪物的交互,以及他多久可以期望他们在失败时丢弃物品。 我遇到的问题是,我期望这样的一段代码对于并行是完美的,但是当我添加并发时,所有模拟的时间往往会减慢4-6倍没有并发。
为了让你更好地理解代码是如何工作的,我有三个主要function:交互function,它是玩家和怪物之间的简单交互。 如果怪物掉落物品则返回1,否则返回0。 模拟function运行多个交互并返回一片交互结果(即1和0表示成功/不成功的交互)。 最后,有一个testing函数,它运行一系列模拟,并返回一个模拟结果片断,这个结果是导致一个丢失项目的交互总次数。 这是我试图并行运行的最后一个function。
现在,我可以理解为什么如果我为每个要运行的testing创build一个goroutine,代码就会变慢。 假设我正在运行100个testing,在我的MacBook Air的4个CPU之间的每个goroutine之间的上下文切换将会导致性能下降,但是我只创build了多个goroutines,因为我拥有处理器,并将testing次数够程。 我希望这可以加快代码的性能,因为我并行地运行了每个testing,但是,当然,我正在慢慢地减速。
我很想弄清楚为什么会这样,所以任何帮助将不胜感激。
下面是没有执行例程的常规代码:
package main import ( "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 main import ( "fmt" "math/rand" "time" "runtime" ) 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, c chan []int) { simulations := make([]int, n) for i := range simulations { for _, v := range simulation(NUMBER_OF_INTERACTIONS) { simulations[i] += v } } c <- simulations } func main() { rand.Seed(time.Now().UnixNano()) nCPU := runtime.NumCPU() runtime.GOMAXPROCS(nCPU) fmt.Println("Number of CPUs: ", nCPU) tests := make([]chan []int, nCPU) for i := range tests { c := make(chan []int) go test(NUMBER_OF_SIMULATIONS/nCPU, c) tests[i] = c } // Concatentate the test results results := make([]int, NUMBER_OF_SIMULATIONS) for i, c := range tests { start := (NUMBER_OF_SIMULATIONS/nCPU) * i stop := (NUMBER_OF_SIMULATIONS/nCPU) * (i+1) copy(results[start:stop], <-c) } fmt.Println("Successful interactions: ", results) }
更新(01/12/13 18:05)
我在下面添加了一个新版本的并发代码,根据下面的“系统”的build议为每个goroutine创build一个新的Rand实例。 与代码的序列版本相比,我现在看到的速度非常微小(大约花费15-20%的时间)。 我很想知道为什么我没有看到接近75%的时间减less的时间,因为我把工作量分散在我的MBA的4个核心上。 有没有人有任何进一步的build议,可以帮助吗?
package main import ( "fmt" "math/rand" "time" "runtime" ) 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(generator *rand.Rand) int { if generator.Float64() <= DROP_RATE { return 1 } return 0 } /** * Runs several interactions and retuns a slice representing the results */ func simulation(n int, generator *rand.Rand) []int { interactions := make([]int, n) for i := range interactions { interactions[i] = interaction(generator) } return interactions } /** * Runs several simulations and returns the results */ func test(n int, c chan []int) { source := rand.NewSource(time.Now().UnixNano()) generator := rand.New(source) simulations := make([]int, n) for i := range simulations { for _, v := range simulation(NUMBER_OF_INTERACTIONS, generator) { simulations[i] += v } } c <- simulations } func main() { rand.Seed(time.Now().UnixNano()) nCPU := runtime.NumCPU() runtime.GOMAXPROCS(nCPU) fmt.Println("Number of CPUs: ", nCPU) tests := make([]chan []int, nCPU) for i := range tests { c := make(chan []int) go test(NUMBER_OF_SIMULATIONS/nCPU, c) tests[i] = c } // Concatentate the test results results := make([]int, NUMBER_OF_SIMULATIONS) for i, c := range tests { start := (NUMBER_OF_SIMULATIONS/nCPU) * i stop := (NUMBER_OF_SIMULATIONS/nCPU) * (i+1) copy(results[start:stop], <-c) } fmt.Println("Successful interactions: ", results) }
更新(01/13/13 17:58)
感谢大家帮忙解决我的问题。 我终于得到了我正在寻找的答案,所以我想我只是在这里总结任何人有同样的问题。
从本质上讲,我有两个主要问题:第一,即使我的代码是尴尬的并行 ,当我把它分成可用的处理器时,它运行速度较慢,其次,解决scheme打开了另一个问题,这是我的序列代码运行两次与在单处理器上运行的并发代码相比,速度缓慢,您希望它们大致相同。 在这两种情况下,问题是随机数生成器函数rand.Float64
。 基本上,这是rand
包提供的一个方便function。 在那个包中, Rand
结构体的全局实例被创build并被每个便利函数使用。 这个全局Rand
实例有一个与之关联的互斥锁。 由于我正在使用这个便利function,因为每个goroutine都必须排队访问全局Rand
实例,所以我并没有真正能够并行化我的代码。 解决scheme(如下面的“系统”所示)是为每个goroutine创build一个Rand
结构的单独实例。 这解决了第一个问题,但创build了第二个问题。
第二个问题是我的非并行并发代码(即我的并发代码只运行一个处理器)的运行速度是顺序代码的两倍。 原因是,即使我只用一个处理器和一个goroutine运行,那个goroutine也有自己创build的Rand
结构实例,而且我没有创build互斥锁。 顺序代码仍然使用rand.Float64
便利函数,它使用全局互斥保护的Rand
实例。 获取该锁的成本导致顺序代码运行速度降低了一倍。
所以,故事的道理是,无论性能如何,确保您创buildRand
结构的实例,并调用所需的函数,而不是使用包提供的便利function。
这个问题似乎来自您使用rand.Float64()
,它使用一个共享的全局对象与一个互斥锁。
相反,如果为每个CPU创build一个单独的rand.New()
,将它传递给interactions()
,并用它来创buildFloat64()
,则会有很大的改进。
更新以在现在使用rand.New()
的问题中显示新示例代码的更改
test()
函数被修改为使用给定通道,或返回结果。
func test(n int, c chan []int) []int { source := rand.NewSource(time.Now().UnixNano()) generator := rand.New(source) simulations := make([]int, n) for i := range simulations { for _, v := range simulation(NUMBER_OF_INTERACTIONS, generator) { simulations[i] += v } } if c == nil { return simulations } c <- simulations return nil }
main()
函数被更新以运行两个testing,并输出计时结果。
func main() { rand.Seed(time.Now().UnixNano()) nCPU := runtime.NumCPU() runtime.GOMAXPROCS(nCPU) fmt.Println("Number of CPUs: ", nCPU) start := time.Now() fmt.Println("Successful interactions: ", len(test(NUMBER_OF_SIMULATIONS, nil))) fmt.Println(time.Since(start)) start = time.Now() tests := make([]chan []int, nCPU) for i := range tests { c := make(chan []int) go test(NUMBER_OF_SIMULATIONS/nCPU, c) tests[i] = c } // Concatentate the test results results := make([]int, NUMBER_OF_SIMULATIONS) for i, c := range tests { start := (NUMBER_OF_SIMULATIONS/nCPU) * i stop := (NUMBER_OF_SIMULATIONS/nCPU) * (i+1) copy(results[start:stop], <-c) } fmt.Println("Successful interactions: ", len(results)) fmt.Println(time.Since(start)) }
输出是我收到的:
> CPU数量:2 > >成功的互动:1000 > 1m20.39959s > >成功的互动:1000 > 41.392299s
在我的Linux四核i7笔记本电脑上testing你的代码我明白了
这是一个Google Spreadsheet
这表明,在Linux下,每个内核的缩放比例几乎是线性的。
我想可能有两个原因,你为什么没有看到这一点。
首先是你的MacBook Air只有2个真正的核心。 它有4个超线程 ,这就是为什么它报告4最大CPU。 超线程通常只会给单个内核多出15%的性能,而不是您所期望的100%。 所以坚持只在macbook air上对1或2个CPU进行基准testing!
另一个原因可能是OS X的线程性能与Linux相比。 他们使用不同的线程模型,可能会影响性能。
你的代码是对二项随机variablesB(N,p)进行抽样,其中N是试验次数(这里是1M),p是成功的单个试验(这里是0.0003)的概率。
一种方法是build立一个累积概率表T,其中T [i]包含试验总数小于或等于i的概率。 为了产生一个样本,你可以select一个统一的随机variables(通过rand.Float64),并find表中第一个包含大于或等于它的概率的索引。
这里有一点复杂,因为你有一个非常大的N和一个相当小的p,所以如果你试图build立表,你会遇到很小的数字和算术精度的麻烦。 但是你可以build立一个更小的表格(比如说1000个大表格)并且抽样1000次以获得你的100万次试验。
这里有一些代码可以完成这一切。 这不是太优雅(1000是硬编码),但它在我的旧笔记本电脑上不到一秒钟就能产生1000次模拟。 通过将BinomialSampler的结构从循环中提出,或者使用二分search而不是线性扫描来查找表索引,可以很容易地进一步优化。
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(Ni)) r[i] = T choice *= float64(Ni) / 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)) } }
我的结果显示了4个CPU与1个CPU的实质并发:
英特尔酷睿2四核CPU Q8300 @ 2.50GHz x 4
源代码:UPDATE(01/12/13 18:05)
$ go version go version devel +adf4e96e9aa4 Thu Jan 10 09:57:01 2013 +1100 linux/amd64 $ time go run temp.go Number of CPUs: 1 real 0m30.305s user 0m30.210s sys 0m0.044s $ time go run temp.go Number of CPUs: 4 real 0m9.980s user 0m35.146s sys 0m0.204s