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)
}
}
}
执行时间:
- 实际 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,在 [=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)
}
}
}
现在我通过看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)
}
}
}
执行时间:
- 实际 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,在 [=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)
}
}
}