写性能测试在Go语言中是很便捷的,go自带的标准工具链就有完善的支持,下面我们来从Go的内部和系统调用方面来详细剖析一下Benchmark这块。

Benchmark

  1. GoBenchmark只要在目录下创建一个_test.go后缀的文件,然后添加下面函数:
  1. func BenchmarkStringJoin1(b *testing.B) {
  2. b.ReportAllocs()
  3. input := []string{"Hello", "World"}
  4. for i := 0; i < b.N; i++ {
  5. result := strings.Join(input, " ")
  6. if result != "Hello World" {
  7. b.Error("Unexpected result: " + result)
  8. }
  9. }
  10. }

调用以下命令:

  1. # go test -run=xxx -bench=. -benchtime="3s" -cpuprofile profile_cpu.out

该命令会跳过单元测试,执行所有benchmark,同时生产一个cup性能描述文件。

注意

·-benchtime 可以控制benchmark的运行时间
·b.ReportAllocs(),在report中包含内存分配信息,例如结果是:

  1. BenchmarkStringJoin1-4 300000 4351 ns/op 32 B/op 2 allocs/op


-4表示4个CPU线程执行,3000000表示总共执行了30万次;4531ns/op,表示每此执行耗时4531纳秒;32B/op表示每次执行分配了32字节内存;2allocs/op表示每次执行分配了2次对象。
根据上面的信息,我们就能对热点路径进行内存对象分配的优化,例如针对上面的程序我们可以小小的优化:

  1. func BenchmarkStringJoin2(b *testing.B) {
  2. b.ReportAllocs()
  3. input := []string{"Hello", "World"}
  4. join := func(strs []string, delim string) string {
  5. if len(strs) == 2 {
  6. return strs[0] + delim + strs[1];
  7. }
  8. return "";
  9. };
  10. for i := 0; i < b.N; i++ {
  11. result := join(input, " ")
  12. if result != "Hello World" {
  13. b.Error("Unexpected result: " + result)
  14. }
  15. }
  16. }

新的Benchmark结果是:

  1. BenchmarkStringJoin2-4 500000 2440 ns/op 16 B/op 1 allocs/op

可以看出来,在减少了内存分配后,性能提升了60%以上!

Cpu Profile

Cpu profile是Go语言工具链中最闪耀的部分之一,掌握了它以及memory、block profile, 那基本上就没有你发现不了的性能瓶颈了。
之前的benchmark同时还生产了一个profile_cpu.out文件,这里我们执行下面的命令:

  1. # go tool pprof app.test profile_cpu.out
  2. Entering interactive mode (type "help" for commands)
  3. (pprof) top10
  4. 8220ms of 10360ms total (79.34%)
  5. Dropped 63 nodes (cum <= 51.80ms)
  6. Showing top 10 nodes out of 54 (cum >= 160ms)
  7. flat flat% sum% cum cum%
  8. 2410ms 23.26% 23.26% 4960ms 47.88% runtime.concatstrings
  9. 2180ms 21.04% 44.31% 2680ms 25.87% runtime.mallocgc
  10. 1200ms 11.58% 55.89% 1200ms 11.58% runtime.memmove
  11. 530ms 5.12% 61.00% 530ms 5.12% runtime.memeqbody
  12. 530ms 5.12% 66.12% 2540ms 24.52% runtime.rawstringtmp
  13. 470ms 4.54% 70.66% 2420ms 23.36% strings.Join
  14. 390ms 3.76% 74.42% 2330ms 22.49% app.BenchmarkStringJoin3B
  15. 180ms 1.74% 76.16% 1970ms 19.02% runtime.rawstring
  16. 170ms 1.64% 77.80% 5130ms 49.52% runtime.concatstring3
  17. 160ms 1.54% 79.34% 160ms 1.54% runtime.eqstring

上面仅仅展示部分函数的信息,并没有调用链路的性能分析,如果需要完整信息,我们要生产svg或pdf图。

  1. # go tool pprof -svg profile_cpu.out > profile_cpu.svg
  2. # go tool pprof -pdf profile_cpu.out > profile_cpu.pdf

下面是profile_cpu.pdf的图:
Go 性能 - 图1

可以看到图里包含了多个benchmark的合集,但我们只关心性能最差的那个benchmark,因此需要过滤:

  1. go test -run=xxx -bench=BenchmarkStringJoin2B$ -cpuprofile profile_2b.out
  2. go test -run=xxx -bench=BenchmarkStringJoin2$ -cpuprofile profile_2.out
  3. go tool pprof -svg profile_2b.out > profile_2b.svg
  4. go tool pprof -svg profile_2.out > profile_2.svg

Go 性能 - 图2
根据图片展示,benchmark自身的函数runtime.concatstrings触发了内存对象的分配,造成了耗时,但是跟踪到这里,我们已经无法继续下去了,因此下面就需要flame graphs了。
Go 性能 - 图3

如果想详细查看,你主要点击这些矩形块就好了。
Go 性能 - 图4

生产这些图,我们需要uber/go-torch这个库,这个库使用了https://github.com/brendangregg/FlameGraph,下面是一个自动下载依赖,然后生成frame graph的脚本,读者可以根据需要,自己实现。

  1. #!/bin/bash
  2. # install flamegraph scripts
  3. if [ ! -d "/opt/flamegraph" ]; then
  4. echo "Installing flamegraph (git clone)"
  5. git clone --depth=1 https://github.com/brendangregg/FlameGraph.git /opt/flamegraph
  6. fi
  7. # install go-torch using docker
  8. if [ ! -f "bin/go-torch" ]; then
  9. echo "Installing go-torch via docker"
  10. docker run --net=party --rm=true -it -v $(pwd)/bin:/go/bin golang go get github.com/uber/go-torch
  11. # or if you have go installed locally: go get github.com/uber/go-torch
  12. fi
  13. PATH="$PATH:/opt/flamegraph"
  14. bin/go-torch -b profile_cpu.out -f profile_cpu.torch.svg

至此,我们的benchmark之路就告一段落,但是上面所述的cpu profile不仅仅能在benchmark中,还能直接在线debug生产环境的应用性能。

完整源码

  1. package main
  2. import "testing"
  3. import "strings"
  4. func BenchmarkStringJoin1(b *testing.B) {
  5. b.ReportAllocs()
  6. input := []string{"Hello", "World"}
  7. for i := 0; i < b.N; i++ {
  8. result := strings.Join(input, " ")
  9. if result != "Hello World" {
  10. b.Error("Unexpected result: " + result)
  11. }
  12. }
  13. }
  14. func BenchmarkStringJoin1B(b *testing.B) {
  15. b.ReportAllocs()
  16. for i := 0; i < b.N; i++ {
  17. input := []string{"Hello", "World"}
  18. result := strings.Join(input, " ")
  19. if result != "Hello World" {
  20. b.Error("Unexpected result: " + result)
  21. }
  22. }
  23. }
  24. func BenchmarkStringJoin2(b *testing.B) {
  25. b.ReportAllocs()
  26. input := []string{"Hello", "World"}
  27. join := func(strs []string, delim string) string {
  28. if len(strs) == 2 {
  29. return strs[0] + delim + strs[1];
  30. }
  31. return "";
  32. };
  33. for i := 0; i < b.N; i++ {
  34. result := join(input, " ")
  35. if result != "Hello World" {
  36. b.Error("Unexpected result: " + result)
  37. }
  38. }
  39. }
  40. func BenchmarkStringJoin2B(b *testing.B) {
  41. b.ReportAllocs()
  42. join := func(strs []string, delim string) string {
  43. if len(strs) == 2 {
  44. return strs[0] + delim + strs[1];
  45. }
  46. return "";
  47. };
  48. for i := 0; i < b.N; i++ {
  49. input := []string{"Hello", "World"}
  50. result := join(input, " ")
  51. if result != "Hello World" {
  52. b.Error("Unexpected result: " + result)
  53. }
  54. }
  55. }
  56. func BenchmarkStringJoin3(b *testing.B) {
  57. b.ReportAllocs()
  58. input := []string{"Hello", "World"}
  59. for i := 0; i < b.N; i++ {
  60. result := input[0] + " " + input[1];
  61. if result != "Hello World" {
  62. b.Error("Unexpected result: " + result)
  63. }
  64. }
  65. }
  66. func BenchmarkStringJoin3B(b *testing.B) {
  67. b.ReportAllocs()
  68. for i := 0; i < b.N; i++ {
  69. input := []string{"Hello", "World"}
  70. result := input[0] + " " + input[1];
  71. if result != "Hello World" {
  72. b.Error("Unexpected result: " + result)
  73. }
  74. }
  75. }

Go 性能 - 图5