Concurrency 为什么添加并发会降低这个golang代码的速度?
我有一点围棋代码,我一直在修补,以回答我的一个小好奇有关的视频游戏,我的姐夫玩 本质上,下面的代码模拟了游戏中与怪物的交互,以及他希望怪物在失败后掉落物品的频率。我遇到的问题是,我希望这样的代码对于并行化来说是完美的,但是当我添加并发时,执行所有模拟所需的时间往往会比没有并发时的原始时间慢4-6倍 为了让您更好地理解代码的工作原理,我有三个主要功能:交互功能,它是玩家和怪物之间的简单交互。如果怪物掉落物品,则返回1,否则返回0。simulation函数运行多个交互并返回交互结果的一部分(即1和0表示成功/不成功的交互)。最后,还有一个test函数,它运行一组模拟,并返回模拟结果的一部分,模拟结果是导致删除项的交互总数。这是我试图并行运行的最后一个函数 现在,我可以理解,如果我为每个要运行的测试创建一个goroutine,为什么代码会变慢。假设我正在运行100个测试,MacBook Air的4个CPU上的每个Goroutine之间的上下文切换会降低性能,但我只创建与处理器数量相同的Goroutine,并在Goroutine之间划分测试数量。我希望这实际上会提高代码的性能,因为我正在并行运行我的每个测试,但是,当然,我的速度反而会大大降低 我很想弄清楚为什么会发生这种情况,所以任何帮助都将不胜感激 以下是不带go例程的常规代码:Concurrency 为什么添加并发会降低这个golang代码的速度?,concurrency,go,Concurrency,Go,我有一点围棋代码,我一直在修补,以回答我的一个小好奇有关的视频游戏,我的姐夫玩 本质上,下面的代码模拟了游戏中与怪物的交互,以及他希望怪物在失败后掉落物品的频率。我遇到的问题是,我希望这样的代码对于并行化来说是完美的,但是当我添加并发时,执行所有模拟所需的时间往往会比没有并发时的原始时间慢4-6倍 为了让您更好地理解代码的工作原理,我有三个主要功能:交互功能,它是玩家和怪物之间的简单交互。如果怪物掉落物品,则返回1,否则返回0。simulation函数运行多个交互并返回交互结果的一部分(即1和0
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))
}
主程序包
进口(
“fmt”
“数学/兰德”
“时间”
)
常数(
模拟的数量=1000
交互次数=1000000
下降率=0.0003
)
/**
*模拟与怪物的单一交互
*
*如果怪物掉落物品,则返回1,否则返回0
*/
func interaction()int{
如果rand.Float64()问题似乎来自于您使用的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
}
我收到的输出是:
> Number of CPUs: 2
>
> Successful interactions: 1000
> 1m20.39959s
>
> Successful interactions: 1000
> 41.392299s
>CPU数量:2
>
>成功互动:1000
>1m20.39959s
>
>成功互动:1000
>41.392299s
我的结果显示,与1个CPU相比,4个CPU具有相当大的并发性:
英特尔酷睿2四处理器Q8300@2.50GHz x 4
源代码:更新(01/12/13 18:05)
在我的Linux四核i7笔记本电脑上测试你的代码
这是一个
这表明,在Linux下,至少每个内核的伸缩性非常接近线性
我认为你没有看到这一点可能有两个原因
首先,您的macbook air只有2个真正的内核。但它有4个,这就是为什么它报告4个为最大CPU的原因。与您可能期望的100%性能相比,超线程通常只提供额外的15%性能。因此,请坚持只在macbook air上对1或2个CPU进行基准测试
另一个原因可能是与Linux相比OS X线程的性能。它们使用不同的线程模型,这可能会影响性能。您的代码正在采样一个二项式随机变量,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))
}
}
主程序包
进口(
“fmt”
“数学”
“数学/兰德”
)
类型BinomialSampler[]float64
func(bs BinomialSampler)Sample()int{
r:=rand.Float64()
对于i:=0;i=r{
返回i
}
}
返回透镜(bs)
}
func NewBinomialSampler(N int,p float64)BinomialSampler{
r:=BinomialSampler(make([]浮点64,N+1))
T:=0.0
选择:=1.0
对于i:=0;感谢您的提示,我更新了代码,为每个goroutine创建了一个Rand实例,并将其传递给交互
函数,它似乎确实加快了并发代码的速度。不过,我仍然没有得到大幅度的加速。我有点希望看到时间缩短近4倍(因为我的机器上有4个内核)但是,我只看到时间减少了1.2倍。我继续添加了新代码和你的建议
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
$ 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
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))
}
}