【问题标题】:confusing concurrency and performance issue in GoGo 中令人困惑的并发性和性能问题
【发布时间】:2018-09-30 22:40:03
【问题描述】:

现在我通过观看 this great course 开始学习 Go 语言。需要明确的是,多年来我只写 PHP,并发/并行对我来说是新的,所以我对此有点困惑。

在本课程中,有一个任务是创建一个计算 100 次计算的阶乘程序。我更进一步,为了比较性能,我将其更改为 10000,出于某种原因,顺序程序的工作方式与并发程序相同甚至更快。

这里我将提供 3 个解决方案:我的、老师的和顺序的

我的解决方案:

package main

import (
 "fmt"
)

func gen(steps int) <-chan int{
     out := make(chan int)
     go func() {
         for j:= 0; j <steps; j++ {
             out <- j
         }
         close(out)
      }()
      return out
}

func factorial(in <-chan int) <-chan int {
    out := make(chan int) 
    go func() {
        for n := range in {
            out <-  fact(n)
        }
        close(out)
    }()
    return out
}

func fact(n int) int {
    total := 1
    for i := n;i>0;i-- {
        total *=i
    }
    return total
}

func main() {
    steps := 10000
    for i := 0; i < steps; i++ {
        for n:= range factorial(gen(10)) {
            fmt.Println(n)
        }
     }
}

执行时间:

  • 真正的 0m6,356s
  • 用户 0m3,885s
  • sys 0m0,870s

老师解决方案: 主包

import (
 "fmt"
)

func gen(steps int) <-chan int{
    out := make(chan int)
    go func() {
        for i := 0; i < steps; i++ {
            for j:= 0; j <10; j++ {
                out <- j
            }
        }
        close(out)
    }()
    return out
}

func factorial(in <-chan int) <-chan int {
    out := make(chan int) 
    go func() {
        for n := range in {
            out <-  fact(n)
        }
        close(out)
    }()
    return out
}

func fact(n int) int {
    total := 1
    for i := n;i>0;i-- {
        total *=i
    }
    return total
}

func main() {
    steps := 10000
    for n:= range factorial(gen(steps)) {
        fmt.Println(n)
    }
}

执行时间:

  • 真正的 0m2,836s
  • 用户 0m1,388s
  • sys 0m0,492s

顺序:

package main

import (
 "fmt"
)

func fact(n int) int {
    total := 1
    for i := n;i>0;i-- {
        total *=i
    }
    return total
}

func main() {
    steps := 10000
    for i := 0; i < steps; i++ {
        for j:= 0; j <10; j++ {
            fmt.Println(fact(j))
        }
    }
}

执行时间:

  • 真正的 0m2,513s
  • 用户 0m1,113s
  • sys 0m0,387s

因此,您可以看到顺序解决方案最快,教师解决方案排在第二位,我的解决方案排在第三位。

第一个问题:为什么顺序解决方案最快? 其次,为什么我的解决方案这么慢?如果我在我的解决方案中理解正确,我将在 gen 中创建 10000 个 goroutine,在 factorial 和教师解决方案中创建 10000 个 goroutine,他在 gen 中仅创建 1 个 goroutine,在 factorial 中创建 1 个。我这么慢是因为我创建了太多不需要的 goroutine?

【问题讨论】:

    标签: go concurrency parallel-processing factorial goroutine


    【解决方案1】:

    让我们从阶乘计算的一些基本基准开始。

    $ go test -run=! -bench=. factorial_test.go 
    goos: linux
    goarch: amd64
    BenchmarkFact0-4            1000000000           2.07 ns/op
    BenchmarkFact9-4            300000000            4.37 ns/op
    BenchmarkFact0To9-4         50000000            36.0 ns/op
    BenchmarkFact10K0To9-4          3000        384069 ns/op
    $ 
    

    CPU 时间非常短,即使是 10,000 次从 0 到 9 的阶乘迭代。

    factorial_test.go:

    package main
    
    import "testing"
    
    func fact(n int) int {
        total := 1
        for i := n; i > 0; i-- {
            total *= i
        }
        return total
    }
    
    var sinkFact int
    
    func BenchmarkFact0(b *testing.B) {
        for N := 0; N < b.N; N++ {
            j := 0
            sinkFact = fact(j)
        }
    }
    
    func BenchmarkFact9(b *testing.B) {
        for N := 0; N < b.N; N++ {
            j := 9
            sinkFact = fact(j)
        }
    }
    
    func BenchmarkFact0To9(b *testing.B) {
        for N := 0; N < b.N; N++ {
            for j := 0; j < 10; j++ {
                sinkFact = fact(j)
            }
        }
    }
    
    func BenchmarkFact10K0To9(b *testing.B) {
        for N := 0; N < b.N; N++ {
            steps := 10000
            for i := 0; i < steps; i++ {
                for j := 0; j < 10; j++ {
                    sinkFact = fact(j)
                }
            }
        }
    }
    

    让我们看看顺序程序的时间。

    $ go build -a sequential.go && time ./sequential
    real    0m0.247s
    user    0m0.054s
    sys     0m0.149s
    

    写入终端显然是一个主要瓶颈。让我们写一个接收器。

    $ go build -a sequential.go && time ./sequential > /dev/null
    real    0m0.070s
    user    0m0.049s
    sys     0m0.020s
    

    对于阶乘计算,它仍然比 0m0.000000384069s 多很多。

    sequential.go:

    package main
    
    import (
        "fmt"
    )
    
    func fact(n int) int {
        total := 1
        for i := n; i > 0; i-- {
            total *= i
        }
        return total
    }
    
    func main() {
        steps := 10000
        for i := 0; i < steps; i++ {
            for j := 0; j < 10; j++ {
                fmt.Println(fact(j))
            }
        }
    }
    

    尝试将并发用于如此微不足道的并行工作很可能会失败。 Go goroutines 和 channels 很便宜,但它们不是免费的。此外,即使在写入接收器时,单个通道和单个终端也是瓶颈和限制因素。有关并行计算,请参阅 Amdahl's Law。见Concurrency is not parallelism

    $ go build -a teacher.go && time ./teacher > /dev/null
    real    0m0.123s
    user    0m0.123s
    sys     0m0.022s
    
    $ go build -a student.go && time ./student > /dev/null
    real    0m0.135s
    user    0m0.113s
    sys     0m0.038s
    

    teacher.go:

    package main
    
    import (
        "fmt"
    )
    
    func gen(steps int) <-chan int {
        out := make(chan int)
        go func() {
            for i := 0; i < steps; i++ {
                for j := 0; j < 10; j++ {
                    out <- j
                }
            }
            close(out)
        }()
        return out
    }
    
    func factorial(in <-chan int) <-chan int {
        out := make(chan int)
        go func() {
            for n := range in {
                out <- fact(n)
            }
            close(out)
        }()
        return out
    }
    
    func fact(n int) int {
        total := 1
        for i := n; i > 0; i-- {
            total *= i
        }
        return total
    }
    
    func main() {
        steps := 10000
        for n := range factorial(gen(steps)) {
            fmt.Println(n)
        }
    }
    

    student.go:

    package main
    
    import (
        "fmt"
    )
    
    func gen(steps int) <-chan int {
        out := make(chan int)
        go func() {
            for j := 0; j < steps; j++ {
                out <- j
            }
            close(out)
        }()
        return out
    }
    
    func factorial(in <-chan int) <-chan int {
        out := make(chan int)
        go func() {
            for n := range in {
                out <- fact(n)
            }
            close(out)
        }()
        return out
    }
    
    func fact(n int) int {
        total := 1
        for i := n; i > 0; i-- {
            total *= i
        }
        return total
    }
    
    func main() {
        steps := 10000
        for i := 0; i < steps; i++ {
            for n := range factorial(gen(10)) {
                fmt.Println(n)
            }
        }
    }
    

    【讨论】:

    • 谢谢,这正是我的想法。创建 goroutine 太简单了
    【解决方案2】:

    这是并发和并行之间的区别——你的、老师的和顺序的在设计上逐渐减少并发,但它们的并行程度取决于 CPU 内核的数量,并且存在与并发相关的设置和通信成本。代码中没有异步调用,因此只有并行性才能提高速度。

    这个值得一看:https://blog.golang.org/concurrency-is-not-parallelism

    此外,即使使用并行内核加速也将取决于工作负载的性质 - 谷歌阿姆达尔定律进行解释。

    【讨论】:

    • 是的,我知道并发和并行是不一样的。顺便说一句,我知道当前版本的 go 默认使用多个内核,不是吗?如果是这样,我认为它应该工作得更快,不是吗?
    • 是的,如果设置 goroutines 并在它们之间进行通信的成本相对于并行计算的好处而言很高,那么它在多个内核上仍然可能会变慢
    • 我不能更具体地与代码相关,因为问题中的前两个代码示例似乎是相同的 :)
    • 嗯,非常感谢,我想我需要一些时间和更多的练习来弄清楚何时何地更好地使用并行性。而且我认为这个例子的主要思想只是展示程序如何同时工作而不是展示性能提升?但是老师解释问题why we need goroutines to calculate factorials?是为了让程序运行得更快,需要大量的计算,这在这种情况下是不正确的,这很奇怪。
    • 嗯,它们不一样,你可以看到我们需要多少时间来执行它们。一个耗时 6 秒,另一个耗时 3 秒
    猜你喜欢
    • 1970-01-01
    • 2011-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-11
    • 2011-03-09
    • 1970-01-01
    相关资源
    最近更新 更多