我有一些Go代码,我一直在修补,以回答我与我的姐夫玩的视频游戏有关的一点好奇心.
从本质上讲,下面的代码模拟了游戏中与怪物的交互,以及他们在失败时可以预期他们丢弃物品的频率.我遇到的问题是,我希望像这样的代码片段非常适合并行化,但是当我添加并发时,所有模拟所需的时间往往会减慢原始速度的4-6倍没有并发.
为了让您更好地理解代码的工作原理,我有三个主要功能:交互功能,它是玩家和怪物之间的简单交互.如果怪物掉落一个物品则返回1,否则返回0.模拟函数运行多个交互并返回一片交互结果(即,1和0表示成功/不成功的交互).最后,还有一个测试函数,它运行一组模拟并返回一段模拟结果,这些结果是导致项目被删除的交互总数.这是我试图并行运行的最后一个功能.
现在,我可以理解为什么如果我为每个要运行的测试创建一个goroutine,代码会变慢.假设我正在运行100次测试,我的MacBook Air上的4个CPU之间的每个goroutine之间的上下文切换会导致性能下降,但我只创建了与处理器一样多的goroutine并将测试次数除以够程.我希望这实际上加快了代码的性能,因为我并行运行每个测试,但是,当然,我的主要是减速.
我很想知道为什么会这样,所以任何帮助都会非常感激.
以下是没有go例程的常规代码:
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))
}
Run Code Online (Sandbox Code Playgroud)
而且,这是与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)
}
Run Code Online (Sandbox Code Playgroud)
更新(2013年12月1日18:05)
我在下面添加了一个新版本的并发代码,根据下面的"系统"建议为每个goroutine创建一个新的Rand实例.与串行版本的代码相比,我现在看到的速度非常快(总体时间缩短了大约15-20%).我很想知道为什么我没有看到更接近75%的时间缩短,因为我将工作量分散到MBA的4核心上.有没有人有任何可以提供帮助的进一步建议?
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)
}
Run Code Online (Sandbox Code Playgroud)
更新(2013年1月13日17:58)
感谢大家帮忙解决我的问题.我终于得到了我正在寻找的答案,所以我想我会在这里总结一下那些有同样问题的人.
基本上我有两个主要问题:首先,即使我的代码令人尴尬地并行,但是当我在可用处理器之间拆分它时运行速度较慢,其次,解决方案打开了另一个问题,这是我的串行代码运行两次单个处理器上运行的并发代码速度很慢,你可能会大致相同.在这两种情况下,问题都是随机数生成器功能rand.Float64.基本上,这是rand包提供的便利功能.在该包中,Rand每个便捷函数都会创建并使用结构的全局实例.此全局Rand实例具有与之关联的互斥锁.由于我正在使用这个便捷功能,因此我无法真正能够并行化我的代码,因为每个goroutine都必须排队以访问全局Rand实例.解决方案(如下面的"系统"所示)是Rand为每个goroutine 创建一个单独的struct 实例.这解决了第一个问题,但创建了第二个问题.
第二个问题是我的非并行并发代码(即我的并发代码只运行一个处理器)的运行速度是顺序代码的两倍.这样做的原因是,即使我只使用单个处理器和单个goroutine运行,该goroutine也有自己Rand创建的结构实例,并且我创建了它而没有互斥锁.顺序代码仍然使用rand.Float64了使用全局互斥保护Rand实例的便捷功能.获取该锁的成本导致顺序代码运行速度慢两倍.
因此,故事的寓意是,只要性能很重要,请确保创建Rand结构的实例并从中调用所需的功能,而不是使用包提供的便利功能.
the*_*tem 42
问题似乎来自您的使用rand.Float64(),它使用带有互斥锁的共享全局对象.
相反,如果为每个CPU创建一个单独的rand.New(),将其传递给interactions(),并使用它来创建Float64(),则会有很大的改进.
更新以显示现在使用的问题中新示例代码的更改 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
}
Run Code Online (Sandbox Code Playgroud)
该main()函数已更新为运行两个测试,并输出定时结果.
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))
}
Run Code Online (Sandbox Code Playgroud)
输出是我收到的:
> Number of CPUs: 2 > > Successful interactions: 1000 > 1m20.39959s > > Successful interactions: 1000 > 41.392299s
在我的Linux四核i7笔记本电脑上测试你的代码我得到了这个

这表明在Linux下至少每个核心的扩展非常接近线性.
我认为你可能有两个原因没有看到这一点.
首先,你的macbook air只有2个真核.它有4个超线程,但这就是为什么它报告4为最大cpus.超线程通常只能在单个内核上提供额外15%的性能,而不是您预期的100%.所以坚持只在macbook air上对1或2个CPU进行基准测试!
与Linux相比,另一个原因可能是OS X线程性能.他们使用不同的线程模型,这可能会影响性能.