我已经修改了一些Go代码,以解决与我姐夫玩的电子游戏有关的我的好奇心。
本质上,下面的代码模拟了游戏中与怪物的互动,以及他期望他们在失败后掉落物品的频率。我遇到的问题是,我希望这样的一段代码非常适合并行化,但是当我并发添加时,完成所有模拟所花费的时间往往会使原始代码的速度降低4-6倍没有并发。
为了使您更好地理解代码的工作方式,我有三个主要功能:交互功能,它是玩家和怪物之间的简单交互。如果怪物掉落物品,则返回1,否则返回0。模拟功能运行多个交互,并返回一部分交互结果(即1和0表示成功/不成功的交互)。最后,有一个测试函数,它运行一组模拟并返回一部分模拟结果,这些结果是导致掉落物品的相互作用的总数。这是我试图并行运行的最后一个函数。
现在,我可以理解,如果我为要运行的每个测试创建一个goroutine,为什么代码会变慢。假设我正在运行100个测试,则在MacBook Air的4个CPU上的每个goroutine之间进行上下文切换会降低性能,但是我只创建与我拥有的处理器一样多的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) }
更新(13/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, 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)
谢谢大家对解决我的问题的帮助。我终于得到了我一直在寻找的答案,所以我想这里只对有相同问题的任何人进行总结。
从本质上讲,我有两个主要问题:首先,即使我的代码令人尴尬地是并行的,当我在可用处理器之间分配代码时,它的运行速度也会变慢;其次,该解决方案带来了另一个问题,即我的串行代码运行了两次。与在单个处理器上运行的并发代码一样慢,您可能希望大致相同。在这两种情况下,问题都是随机数生成器函数rand.Float64。基本上,这是rand软件包提供的便利功能。在该程序包中,Rand每个便利功能均创建并使用该结构的全局实例。这个全球Rand实例具有与其关联的互斥锁。由于我使用了此便利函数,因此我无法真正实现代码并行化,因为每个goroutine必须排队才能访问全局Rand实例。解决方案(如下文“系统”所示)是Rand为每个goroutine 创建一个单独的struct 实例。这解决了第一个问题,但创建了第二个问题。
rand.Float64
rand
Rand
第二个问题是我的非并行并发代码(即,我的并发代码仅在单个处理器上运行)的运行速度是顺序代码的两倍。这样做的原因是,即使我仅使用单个处理器和单个goroutine运行,该goroutine仍具有自己Rand创建的结构实例,并且我创建时没有互斥锁。顺序代码仍在使用rand.Float64便捷功能,该功能利用了全局互斥锁保护的Rand实例。获得该锁的成本导致顺序代码的运行速度慢了一倍。
因此,故事的寓意是,每当性能重要时,请确保您创建该Rand结构的实例并从中调用所需的函数,而不要使用程序包提供的便捷函数。
问题似乎来自您对的使用rand.Float64(),它使用了一个共享全局对象并带有Mutex锁。
rand.Float64()
相反,如果为每个CPU创建一个单独的rand.New(),将其传递到interactions(),然后使用它来创建Float64(),则会有很大的改进。
rand.New()
interactions()
Float64()
更新以显示对现在使用的问题中新示例代码的更改rand.New()
该test()函数已修改为使用给定通道或返回结果。
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()功能已更新为可以运行两个测试,并输出定时结果。
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)) }
输出是我收到的:
> CPU数量:2 > >成功的互动:1000 > 1分20.39959秒 > >成功的互动:1000 > 41.392299s