Go语言提供了很多性能分析的工具,性能分析类型有如下几种:

  1. CPU性能分析
  2. 内存性能分析
  3. 阻塞性能分析
  4. 锁性能分析

CPU性能分析

生成profile文件

Go 的运行时性能分析接口都位于 runtime/pprof 包中。只需要调用 runtime/pprof 库即可得到我们想要的数据。

看下面的例子,随机生成了 5 组数据,并且使用冒泡排序法排序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 构造数据源
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 BubbleTest() {
    // 或者直接生成文件
	//f, _ := os.OpenFile("cpu.pprof", os.O_CREATE|os.O_RDWR, 0644)
	//defer f.Close()
    
	// 度量CPU性能
	pprof.StartCPUProfile(os.Stdout)
	defer pprof.StopCPUProfile()

	n := 10
	for i := 0; i < 5; i++ {
		nums := generate(n)
		bubbleSort(nums)
		n *= 10
	}
}

用下面的命令生成文件,或者取消注释上面的两行代码,直接生成文件:

# 手工导出文件
$ go run main.go > cpu.pprof
# 直接生成文件
$ go run main.go

分析数据

用Go自带的分析工具:

$ go tool pprof -http=:1234 cpu.pprof

如果提示 Graphviz 没有安装,则通过 brew install graphviz(MAC) 或 apt install graphviz(Ubuntu) 即可,或者windows下载:http://www.graphviz.org/download/

注意:可能需要重启系统,否则命令报错。

网页浏览器中查看的结果,可以换不通的视图(可以点击Title排序):

image-20210123182425691

除了网页分析数据之外,也可以在命令行中使用交互模式查看:

>go tool pprof cpu.pprof
Type: cpu
Time: Jan 23, 2021 at 6:03pm (CST)
Duration: 10.88s, Total samples = 10.75s (98.83%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 10.70s, 99.53% of 10.75s total
Dropped 1 node (cum <= 0.05s)
      flat  flat%   sum%        cum   cum%
    10.70s 99.53% 99.53%     10.75s   100%  yufa/demo/cpprof.bubbleSort
         0     0% 99.53%     10.75s   100%  main.main
         0     0% 99.53%     10.75s   100%  runtime.main
         0     0% 99.53%     10.75s   100%  yufa/demo/cpprof.BubbleTest

可以看到 main.bubbleSort 是消耗 CPU 最多的函数。

还可以按照 cum (累计消耗)排序:

(pprof) top --cum
Showing nodes accounting for 10.70s, 99.53% of 10.75s total
Dropped 1 node (cum <= 0.05s)
      flat  flat%   sum%        cum   cum%
         0     0%     0%     10.75s   100%  main.main
         0     0%     0%     10.75s   100%  runtime.main
         0     0%     0%     10.75s   100%  yufa/demo/cpprof.BubbleTest
    10.70s 99.53% 99.53%     10.75s   100%  yufa/demo/cpprof.bubbleSort

help 可以查看所有支持的命令和选项:

(pprof) help
  Commands:
    callgrind        Outputs a graph in callgrind format
    comments         Output all profile comments
    disasm           Output assembly listings annotated with samples
    dot              Outputs a graph in DOT format
    eog              Visualize graph through eog
    evince           Visualize graph through evince
    gif              Outputs a graph image in GIF format
    gv               Visualize graph through gv
......

内存性能分析

生成profile

生成长度为 N 的随机字符串,拼接在一起:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import (
	"github.com/pkg/profile"
	"math/rand"
)

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 MemoryDemo() {
	// cpu
	//defer profile.Start().Stop()
    
	defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop()
	concat(100)
}

接下来,我们使用一个易用性更强的库 pkg/profile 来采集性能数据,pkg/profile 封装了 runtime/pprof 的接口,使用起来更简单。当然想度量 concat() 的 CPU 性能数据也是可以的,只需要取消上面一行代码注释即可生成 profile 文件。

运行:go run main.go

会生成相应的分析结果数据,比如我们这里是mem.pprof

分析数据

按照以前的方法,在浏览器中查看内存使用数据报告:

go tool pprof -http=:1234 mem.pprof

image-20210123190518451

从这张图中,我们可以看到 concat 消耗了 524k 内存,randomString 仅消耗了 22k 内存。理论上,concat 函数仅仅是将 randomString 生成的字符串拼接起来,消耗的内存应该和 randomString 一致,但怎么会产生 20 倍的差异呢?这和 Go 语言字符串内存分配的方式有关系。字符串是不可变的,因为将两个字符串拼接时,相当于是产生新的字符串,如果当前的空间不足以容纳新的字符串,则会申请更大的空间,将新字符串完全拷贝过去,这消耗了 2 倍的内存空间。在这 100 次拼接的过程中,会产生多次字符串拷贝,从而消耗大量的内存。

那就改进。

使用 strings.Builder 替换 + 进行字符串拼接,将有效地降低内存消耗。

1
2
3
4
5
6
7
func concat(n int) string {
	var s strings.Builder
	for i := 0; i < n; i++ {
		s.WriteString(randomString(n))
	}
	return s.String()
}

同样的方式查看分析结果数据,如下图:

image-20210123191837891

可以看到,使用 strings.Builder 后,concat 内存消耗降为了原来的 1/11。

benchmark生成profile

benchmark除了直接在命令行中查看测试的结果外,也可以生成 profile 文件,使用 go tool pprof 分析。

testing 支持生成 cpu、memory 和 block 的 profile 文件:

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

例如:go test -bench="Fib$" -cpuprofile=cpu.pprof .

另外

使用 -text 选项可以直接将结果以文本形式打印出来。pprof 支持多种输出格式(图片、文本、Web等),直接在命令行中运行 go tool pprof 即可看到所有支持的选项:

$ go tool pprof

Details:
  Output formats (select at most one):
    -dot             Outputs a graph in DOT format
    -png             Outputs a graph image in PNG format
	-text            Outputs top entries in text form
    -tree            Outputs a text rendering of call graph
    -web             Visualize graph through web browser
	......

比如使用go tool pprof -text cpu.pprof

(完)