当前位置: 首页 > 面试题库 >

为什么添加并发会使该golang代码变慢?

唐涛
2023-03-14
问题内容

我已经修改了一些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密集型例程(即:仅在例程后立即设置一个断点运行),那么执行时间就好像不在调试器中一样。 问题答案: 通过调试器运行时,