Go 中令人困惑的并发和性能问题

confusing concurrency and performance issue in Go

现在我通过看this great course开始学习Go语言。多年来我只写 PHP 并且 concurrency/parallelism 对我来说是新的,所以我对此有点困惑。

在本课程中,有一个任务是创建一个用 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)
        }
     }
}

执行时间:


老师解答: 主包

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)
    }
}

执行时间:


顺序:

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))
        }
    }
}

执行时间:


因此,如您所见,顺序解决方案最快,老师的解决方案第二,我的解决方案第三。

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

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

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

此外,即使使用并行内核加速也将取决于工作负载的性质 - google Amdahl 定律的解释。

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

$ 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 时间非常短,即使对于阶乘 0 到 9 的 10,000 次迭代也是如此。

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 for parallel computing. See 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)
        }
    }
}