Golang多协程版本性能变慢问题探讨

Golang多协程版本性能变慢问题探讨 我编写了一个生成曼德博集合图像的程序。 单协程版本大约需要20秒。 多协程版本大约需要30秒。 两者产生的结果完全相同。 在单协程版本中,我看到一个核心被完全占用;而在多协程版本中,所有核心都被完全占用。 我已经检查过,这不是垃圾回收(GC)的问题(使用 GODEBUG=gctrace=1 go run main.go),因为在初始化设置之后就没有垃圾产生了。 程序中没有使用 math.rand

我应该从哪些方面着手,来找出它为什么没有运行得更快呢?

谢谢

2 回复

嗯……又是一个先提问后找到答案的情况!(这个世界真奇妙!😊)

无论如何,我的答案是…… go run -race main.go

我之前有一个全局变量,它在实际的工作例程中统计迭代次数。移除这个变量后,使用多个goroutine的版本在4秒左右就完成了。太好了。

那么新的问题是……当Go编译器发现某些竞态条件时,它会默默地添加一些同步工作吗? 答案看起来显然是肯定的……但也许还有其他原因?

更多关于Golang多协程版本性能变慢问题探讨的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


首先,需要检查是否存在竞态条件或锁竞争。可以使用 -race 标志运行程序来检测竞态条件:

go run -race main.go

如果检测到竞态条件,需要修复数据同步问题。

其次,检查是否因协程间任务分配不均导致负载不均衡。以下是一个简单的负载均衡示例,使用工作池模式:

package main

import (
	"image"
	"image/color"
	"image/png"
	"math/cmplx"
	"os"
	"runtime"
	"sync"
)

func mandelbrot(z complex128) color.Color {
	const iterations = 200
	const contrast = 15

	var v complex128
	for n := uint8(0); n < iterations; n++ {
		v = v*v + z
		if cmplx.Abs(v) > 2 {
			return color.Gray{255 - contrast*n}
		}
	}
	return color.Black
}

func worker(jobs <-chan int, img *image.RGBA, wg *sync.WaitGroup) {
	defer wg.Done()
	for y := range jobs {
		for x := 0; x < 1024; x++ {
			zx := float64(x)/1024*3.5 - 2.5
			zy := float64(y)/1024*3.5 - 1.75
			img.Set(x, y, mandelbrot(complex(zx, zy)))
		}
	}
}

func main() {
	const width, height = 1024, 1024
	img := image.NewRGBA(image.Rect(0, 0, width, height))

	numWorkers := runtime.NumCPU()
	jobs := make(chan int, height)
	var wg sync.WaitGroup

	for w := 0; w < numWorkers; w++ {
		wg.Add(1)
		go worker(jobs, img, &wg)
	}

	for y := 0; y < height; y++ {
		jobs <- y
	}
	close(jobs)

	wg.Wait()

	f, _ := os.Create("mandelbrot.png")
	png.Encode(f, img)
}

第三,检查是否存在伪共享(false sharing)问题。如果多个协程频繁写入同一缓存行中的不同变量,会导致性能下降。可以使用填充(padding)来避免:

type PixelData struct {
	R, G, B, A uint8
	_padding   [60]byte // 填充到64字节,避免伪共享
}

第四,使用性能分析工具定位瓶颈:

go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof

在pprof中使用toplist命令查看热点函数。

第五,检查是否因协程创建过多导致调度开销。适当控制协程数量,通常与CPU核心数相同或稍多即可。

第六,确保没有不必要的内存分配。使用go build -gcflags '-m'检查逃逸分析,减少堆分配。

最后,检查算法本身是否有优化空间,例如使用SIMD指令或更高效的数学计算库。

回到顶部