我已经得到了一些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))
}
并且,这是与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)
}
更新(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)
}
更新(2013年1月13日17:58)
感谢大家帮忙解决我的问题。我终于得到了我正在寻找的答案,所以我想我会在这里总结一下那些有同样问题的人。
基本上我有两个主要问题:首先,即使我的代码是embarrassingly parallel,当我在可用的处理器中拆分它时运行速度较慢,其次,解决方案打开另一个问题,这是我的串行代码的运行速度是单处理器上运行的并发代码的两倍,您可能会大致相同。在这两种情况下,问题都是随机数生成器函数rand.Float64
。基本上,这是rand
包提供的便利功能。在该包中,每个便捷函数都创建并使用Rand
结构的全局实例。此全局Rand
实例具有与之关联的互斥锁。由于我使用了这个便捷功能,因此我无法真正能够并行化我的代码,因为每个goroutine都必须排队访问全局Rand
实例。解决方案(如下面的“系统”所示)是为每个goroutine创建一个单独的Rand
结构实例。这解决了第一个问题,但创建了第二个问题。
第二个问题是我的非并行并发代码(即我的并发代码只运行一个处理器)的运行速度是顺序代码的两倍。这样做的原因是,即使我只使用单个处理器和单个goroutine运行,该goroutine也有自己创建的Rand
结构的实例,并且我创建了它而没有互斥锁。顺序代码仍然使用rand.Float64
便利函数,该函数使用全局互斥保护Rand
实例。获取该锁的成本导致顺序代码运行速度慢两倍。
因此,故事的寓意是,只要性能很重要,请确保创建Rand
结构的实例并调用所需的函数,而不是使用包提供的便捷函数。
答案 0 :(得分:41)
问题似乎来自您使用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
}
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))
}
我收到了输出:
> Number of CPUs: 2 > > Successful interactions: 1000 > 1m20.39959s > > Successful interactions: 1000 > 41.392299s
答案 1 :(得分:7)
在我的Linux四核i7笔记本电脑上测试你的代码我得到了这个
这表明在Linux下至少每个核心的扩展几乎是线性的。
我认为可能有两个原因导致您没有看到这一点。
首先,你的macbook air只有2个真核。它有4 hyperthreads,这就是为什么它报告4为最大cpus。超线程通常只能在单个内核上提供额外15%的性能,而不是您预期的100%。所以坚持只在macbook air上对1或2个CPU进行基准测试!
与Linux相比,另一个原因可能是OS X线程性能。他们使用不同的线程模型,这可能会影响性能。
答案 2 :(得分:3)
您的代码正在对二项式随机变量B(N,p)进行抽样,其中N是试验次数(此处为1M),p是成功个体试验的概率(此处为0.0003)。
这样做的一种方法是建立累积概率的表T,其中T [i]包含试验总数小于或等于i的概率。然后生成一个样本,你可以选择一个统一的随机变量(通过rand.Float64),并在表中找到包含大于或等于它的概率的第一个索引。
这里有点复杂,因为你有一个非常大的N和一个相当小的p,所以如果你试图建立表,你会遇到很小的数字和算术精度的问题。但是你可以建造一个较小的桌子(比如1000个大)并对它进行1000次取样以获得100万次试验。
这是完成所有这些的一些代码。它不是太优雅(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))
}
}
答案 3 :(得分:1)
我的结果显示4个CPU与1个CPU的实质并发:
Intel Core 2 Quad 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