我已经修改了一些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 实例。这解决了第一个问题,但创建了第二个问题。
第二个问题是我的非并行并发代码(即,我的并发代码仅在单个处理器上运行)的运行速度是顺序代码的两倍。这样做的原因是,即使我仅使用单个处理器和单个goroutine运行,该goroutine仍具有自己Rand
创建的结构实例,并且我创建时没有互斥锁。顺序代码仍在使用rand.Float64
便捷功能,该功能利用了全局互斥锁保护的Rand
实例。获得该锁的成本导致顺序代码的运行速度慢了一倍。
因此,故事的寓意是,每当性能重要时,请确保您创建该Rand
结构的实例并从中调用所需的函数,而不要使用程序包提供的便捷函数。
问题似乎来自您对的使用rand.Float64()
,它使用了一个共享全局对象并带有Mutex锁。
相反,如果为每个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))
}
输出是我收到的:
> CPU数量:2
>
>成功的互动:1000
> 1分20.39959秒
>
>成功的互动:1000
> 41.392299s
为什么注释掉for循环的前两行并取消注释第三行会导致42%的加速? 在时间的背后是非常不同的汇编代码:循环中的13条和7条指令。该平台运行的是视窗7。NET 4.0 x64。代码优化已启用,测试应用程序在VS2010之外运行。[更新:重现项目,用于验证项目设置。] 消除中间布尔值是一个基本的优化,是我1980年代龙书时代最简单的优化之一。在生成 CIL 或 JITing x64 机器代码时,优化是
问题内容: 考虑以下功能: 它们应该是等效的。但是存在性能差异: 不带的版本else慢10%。这非常重要。为什么? 问题答案: 对我来说,它们的速度几乎相同:(Debian上的Python 2.6.6) 字节码也非常相似: 唯一的区别是,如果控制到达函数主体的末尾,则else返回包含代码的版本None。
这似乎与对象没有被实例化有关,尽管我不太明白为什么。有人知道出什么事了吗?
(注:问题下方有更新) 看看这个精简的small\u vector基准测试: (使用NOINLINE是因为这是编译器决定内联原始small_vector代码的方式) 如果此代码是用clang 11编译的,那么如果我取消对数组中已注释行的注释,速度会更快(注意,从不执行自由调用)。在我的机器(i7 8750)上,差异为18%。在快速工作台上。com,差异较小,为5.3%。 我知道这是一个微基准测试,
问题内容: 我们有多个线程调用上。 我的理论是,当由两个线程同时调用时,实际上仅将要添加的两个对象之一添加到。这看似合理吗? 如果是这样,您如何解决呢?使用类似吗? 问题答案: 对于ArrayList上的两个线程同时调用add时发生的情况,没有任何保证的行为。但是,根据我的经验,两个对象的添加都很好。与列表相关的大多数线程安全问题在添加/删除时都会处理迭代。尽管如此,我强烈建议不要将Vanilla
问题内容: 通过调试器运行时,某些占用大量CPU的例程会大大降低速度。为什么是这样? 目前,我只是使用IntelliJ逐步执行JBoss中运行的代码。启动JBoss时,使用以下选项: 有没有办法加快执行速度?还是加快我不需要执行的某些方法的执行速度? 更新 :看来我是否不进入CPU密集型例程(即:仅在例程后立即设置一个断点运行),那么执行时间就好像不在调试器中一样。 问题答案: 通过调试器运行时,