基本上,下面的代码模拟了与游戏中的怪物的交互,以及他可以期望他们在失败时丢弃物品的频率。我所遇到的问题是,我希望像这样的代码片段是完美的并行化,但当我并发添加时间,它需要做所有的模拟趋向于减慢原来的4-6倍无并发。
为了让你更好地理解代码的工作原理,我有三个主要功能:交互功能,这是一个简单的交互的玩家和一个怪物。如果怪物掉落一个项目,它返回1,否则返回0。模拟函数运行若干交互并返回交互结果的片段(即,表示成功/不成功交互的1和0)。最后,有一个测试函数,它运行一组模拟,并返回一个模拟结果片段,这是导致丢弃项目的交互的总数。这是我试图并行运行的最后一个功能。
现在,我可以理解为什么代码会减慢,如果我为我想运行的每个测试创建一个goroutine。假设我正在运行100个测试,在每个goroutine之间的上下文切换在4个cpu我的MacBook Air已经杀了性能,但我只创建与我有处理器和许多goroutine, goroutines。我希望这实际上加快了代码的性能,因为我运行我的每个测试并行,但当然,我得到了一个大的减速而不是。
我很想知道为什么这是发生,所以任何帮助将非常感激。
下面是没有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) }
更新(01/12/13 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,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: ",results) }
更新(01/13/13 17:58)
感谢大家帮我找出我的问题。我终于得到了我正在寻找的答案,所以我想我只是总结在这里对于有同样问题的任何人。
基本上我有两个主要的问题:第一,即使我的代码是embarrassingly parallel,它是运行较慢,当我拆分它在可用的处理器,其次,解决方案打开另一个问题,这是我的序列代码运行两次慢作为在单处理器上运行的并发代码,这将是期望是大致相同的。在这两种情况下,问题是随机数生成器函数rand.Float64。基本上,这是rand包提供的一个方便的函数。在该包中,Rand结构的全局实例由每个方便函数创建和使用。这个全局Rand实例具有与其相关联的互斥锁。因为我使用这个方便的函数,我不能真正地并行化我的代码,因为每个goroutine必须排队访问全局兰德实例。解决方案(下面的“系统”建议)是为每个goroutine创建一个Rand结构的单独实例。这解决了第一个问题,但创建了第二个问题。
第二个问题是我的非并行并发代码(即我的并发代码只运行一个处理器)运行的速度是顺序代码的两倍。这样做的原因是,即使我只运行一个处理器和一个goroutine,goroutine有自己的Rand结构的实例,我创建,我已经创建它没有互斥锁。顺序代码仍然使用rand.Float64方便的函数,它使用全局互斥保护的Rand实例。获取该锁的成本导致顺序代码运行两次慢。
所以,故事的道德是,每当性能重要时,确保你创建一个Rand结构的实例,并调用你需要的函数,而不是使用包提供的方便的功能。
相反,如果为每个cpu创建一个单独的rand.New(),传递给交互(),并使用它来创建Float64(),有一个巨大的改进。
更新以显示对现在使用rand.New()的问题中的新示例代码的更改
func test(n int,c chan []int) []int { source := rand.NewSource(time.Now().UnixNano()) generator := rand.New(source) simulations := make([]int,generator) { simulations[i] += v } } if c == nil { return simulations } c <- simulations return nil }
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,<-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