1.2 pporf 性能分析

Kesa...大约 4 分钟golang

Benchmark 可以用于测试某个函数的性能,若直到性能的瓶颈的位置,可以使用 benchmark 进行测试。

若面对未知的程序则需要使用 pprof 来分析性能的瓶颈,pprofopen in new window包含两个部分:

  1. runtime/pprof
  2. go tool pprof 工具

1. 性能分析类型

1.1 CPU 性能分析

CPU 性能分析(CPU profiling) 是最常见的性能分析类型。

启动 CPU 分析时,运行时(runtime) 将每隔 10ms 中断一次,记录此时正在运行的协程(goroutines) 的堆栈信息。

程序运行结束后,可以分析记录的数据找到最热代码路径(hottest code paths)。

Compiler hot paths are code execution paths in the compiler in which most of the execution time is spent, and which are potentially executed very often.

What’s the meaning of “hot codepath”open in new window

一个函数在性能分析数据中出现的次数越多,说明执行该函数的代码路径(code path)花费的时间占总运行时间的比重越大。

1.2 内存性能分析

内存性能分析(Memory profiling) 记录堆内存分配时的堆栈信息,忽略栈内存分配信息。

内存性能分析启用时,默认每1000次采样1次,这个比例是可以调整的。因为内存性能分析是基于采样的,因此基于内存分析数据来判断程序所有的内存使用情况是很困难的。

1.3 阻塞性能分析

阻塞性能分析(block profiling) 是 Go 特有的。

阻塞性能分析用来记录一个协程等待一个共享资源花费的时间。在判断程序的并发瓶颈时会很有用。阻塞的场景包括:

  • 在没有缓冲区的信道上发送或接收数据。
  • 从空的信道上接收数据,或发送数据到满的信道上。
  • 尝试获得一个已经被其他协程锁住的排它锁。

一般情况下,当所有的 CPU 和内存瓶颈解决后,才会考虑这一类分析。

1.4 锁性能分析

锁性能分析(mutex profiling) 与阻塞分析类似,但专注于因为锁竞争导致的等待或延时。

2. CPU 性能分析

记录性能数据会对程序的性能产生影响,建议一次只记录一类数据。

2.1 生成 profile

下例中,生成了5组数据并时使用冒泡排序:

func generate(n int) []int {
	rand.Seed(time.Now().UnixNano())
	nums := make([]int, 0)
	for i := 0; i < n; i++ {
		nums = append(nums, rand.Int())
	}
	return nums
}
func bubbleSort(nums []int) {
	for i := 0; i < len(nums); i++ {
		for j := 1; j < len(nums)-i; j++ {
			if nums[j] < nums[j-1] {
				nums[j], nums[j-1] = nums[j-1], nums[j]
			}
		}
	}
}

func main() {
    f, _ := os.OpenFile("cpu.pprof", os.O_CREATE|os.O_RDWR, 0644)
	defer f.Close()
    
    pprof.StartCPUProfile(f) 
	defer pprof.StopCPUProfile()
    
	n := 10
	for i := 0; i < 5; i++ {
		nums := generate(n)
		bubbleSort(nums)
		n *= 10
	}
}
  • pprof.StartCPUProfile(f):开始CPU性能分析,并输出到文件中
  • defer pprof.StopCPUProfile():停止CPU性能分析

2.2 分析数据

使用 go tool pprof 进行分析:

$ go tool pprof -http=:9999 cpu.pprof
  • -http:启动 web 服务查看

也可以使用 CLI 查看:

$ go tool pprof cpu.pprof
(pprof) top
Showing nodes accounting for 7.96s, 98.39% of 8.09s total
Dropped 31 nodes (cum <= 0.04s)
      flat  flat%   sum%        cum   cum%
     7.94s 98.15% 98.15%      7.95s 98.27%  high-performance-go/01-performance-analysis.bubbleSort (inline)
     0.01s  0.12% 98.27%      0.10s  1.24%  runtime.schedule
     0.01s  0.12% 98.39%      0.05s  0.62%  runtime.startm
         0     0% 98.39%      7.95s 98.27%  high-performance-go/01-performance-analysis.Test_mainFunc
         0     0% 98.39%      7.95s 98.27%  high-performance-go/01-performance-analysis.mainFunc

3. 内存性能分析

3.1 生成 profile

下例中,生成长度为 N 的随机字符串,拼接在一起。

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func randomString(n int) string {
	b := make([]byte, n)
	for i := range b {
		b[i] = letterBytes[rand.Intn(len(letterBytes))]
	}
	return string(b)
}

func concat(n int) string {
	s := ""
	for i := 0; i < n; i++ {
		s += randomString(n)
	}
	return s
}

func main() {
	defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop()
	concat(100)
}

可以使用三方库来分析 "github.com/pkg/profile"

import (
	"github.com/pkg/profile"
)

func main() {
	defer profile.Start(profile.MemProfile, profile.ProfilePath("."), profile.MemProfileRate(1)).Stop()

	concat(100)
}

3.2 分析数据

$ go tool pprof .\mem.pprof
(pprof) top
Showing nodes accounting for 608.52kB, 99.60% of 610.98kB total
Dropped 45 nodes (cum <= 3.05kB)
Showing top 10 nodes out of 37
      flat  flat%   sum%        cum   cum%
  524.61kB 85.86% 85.86%   551.78kB 90.31%  high-performance-go/01-performance-analysis.concat
   21.88kB  3.58% 89.44%    27.17kB  4.45%  high-performance-go/01-performance-analysis.randomString
   20.65kB  3.38% 92.82%    20.65kB  3.38%  unicode/utf16.Decode
   18.73kB  3.07% 95.89%    18.73kB  3.07%  syscall.UTF16FromString
    9.64kB  1.58% 97.47%    26.84kB  4.39%  internal/syscall/windows/registry.Key.ReadSubKeyNames

我们可以看到 concat 消耗了 524k 内存,Go 语言字符串是不可变的,因为将两个字符串拼接时,相当于是产生新的字符串,如果当前的空间不足以容纳新的字符串,则会申请更大的空间,将新字符串完全拷贝过去,这消耗了 2 倍的内存空间。在这 100 次拼接的过程中,会产生多次字符串拷贝,从而消耗大量的内存。

4. benchmark 生成 profile

testing 支持生成 CPU、memory 和 block 的 profile 文件。

  • -cpuprofile=$FILE
  • -memprofile=$FILE, -memprofilerate=N 调整记录速率为原来的 1/N。
  • -blockprofile=$FILE

例如:

func fib(n int) int {
	if n == 0 || n == 1 {
		return n
	}
	return fib(n-2) + fib(n-1)
}

func BenchmarkFib(b *testing.B) {
	for n := 0; n < b.N; n++ {
		fib(30) // run fib(30) b.N times
	}
}

go test 添加 -cpuprofile 参数即可生成 BenchmarkFib 对应的 CPU profile 文件:

$ go test -bench 'Fib' -cpuprofile './cpu.pprof'
...
$ go tool pprof -text .\cpu.pprof
Showing nodes accounting for 1.03s, 100% of 1.03s total
      flat  flat%   sum%        cum   cum%
     1.01s 98.06% 98.06%      1.01s 98.06%  high-performance-go/01-performance-analysis.fib
     0.01s  0.97% 99.03%      0.01s  0.97%  runtime.pMask.read (inline)

Reference

  1. https://geektutu.com/post/hpg-pprof.htmlopen in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2