0%

Go数据分析工具perf试用

Go perf 使用记录

1. 介绍

Go perf在 官方介绍 中的定义为 Go benchmark analysis tools ,即一个 Go benchmark 分析工具合集。它包含用于分析基准测试结果数据的命令行工具。

  • cmd/benchstat 计算 Go 基准的统计摘要和 A/B 比较。

  • cmd/benchfilter 过滤基准测试结果文件的内容。

  • cmd/benchsave 将基准测试结果发布到 perf.golang.org

  • benchfmt 格式化读取和写入 Go 基准测试。

  • benchunit 操作基准测试中的某一项并格式化这些单位中的数字。

  • benchproc 提供了用于过滤、分组和排序基准测试结果的工具。

  • Benchmath 提供了用于计算基准测量分布统计数据的工具。

非常全面,涵盖了从格式化解析、分组、过滤、统计学分析、结果发布等一站式全流程的工具,再次发扬分类技能:

取数据(benchstat、benchunit、benchfmt)->

数据清洗(benchstat、benchfilter、benchproc、benchmath)->

结果发布 (benchsave)

看起来非常专业且全面。

接下来使用一个测试用例对其全流程试用。

2. 使用

2.1 编写基准测试

该基准测试测试了不同方式拼接字符串。

  • stringconcat.go 用了四种不同的字符串拼接方式
1
2
3
4
5
6
7
8
9
10
11
12
// n 表示循环拼接的次数, s表示循环拼接的字符串
// 使用 += 拼接
func concatWithPlus(n int, s string) string {}

// strings.Builder
func concatWithBuilder(n int, s string) string {}

// bytes.Buffer
func concatWithBuffer(n int, s string) string {}

// strings.Join
func concatWithJoin(n int, s string) string {}
  • stringconcat_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 这个文件中测试不同压力下的表现

func BenchmarkConcatPlus100(b *testing.B) {}

func BenchmarkConcatPlus1000(b *testing.B) {}

func BenchmarkConcatBuilder100(b *testing.B) {}

func BenchmarkConcatBuilder1000(b *testing.B) {}

func BenchmarkConcatBuffer100(b *testing.B) {}

func BenchmarkConcatBuffer1000(b *testing.B) {}

func BenchmarkConcatJoin100(b *testing.B) {}

func BenchmarkConcatJoin1000(b *testing.B) {}

2.2 执行测试,保存到文件

我们先试用一下:

  1. benchmark
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    hxy@ubu22:~/app/go/04benchstat
    $ go test -bench=. -benchmem perf
    goos: linux
    goarch: amd64
    pkg: perf
    cpu: AMD Ryzen 7 7840HS w/ Radeon 780M Graphics
    BenchmarkConcatPlus100-8 17325 68958 ns/op 387027 B/op 99 allocs/op
    BenchmarkConcatPlus1000-8 147 8414351 ns/op 38739436 B/op 1001 allocs/op
    BenchmarkConcatBuilder100-8 154357 8133 ns/op 34992 B/op 12 allocs/op
    BenchmarkConcatBuilder1000-8 18228 68162 ns/op 286130 B/op 19 allocs/op
    BenchmarkConcatBuffer100-8 154564 7525 ns/op 29616 B/op 9 allocs/op
    BenchmarkConcatBuffer1000-8 19663 60497 ns/op 264369 B/op 12 allocs/op
    BenchmarkConcatJoin100-8 390640 2972 ns/op 9984 B/op 2 allocs/op
    BenchmarkConcatJoin1000-8 52074 22966 ns/op 90112 B/op 2 allocs/op
    PASS
    ok perf 12.821s

BenchmarkConcatPlus100-8的数据为例解释一下数据的含义:

  • 17325:指被采纳作为统计结果的函数执行次数

    hxy注:这里有个坑,因为这个次数和我规定的次数不同,经查阅资料,Go有一些机制保证统计的内容是足够稳定的结果。这个”一些机制”目前还并没有查到非常详细的解释。

  • 68958 ns/op :每次操作消耗的时间为68958次

  • 387027 B/op :每次操作占用的内存为387027B

  • 99 allocs/op:每次操作申请内存的次数为99次

没有什么问题,保存到文件

1
go test -bench=. -benchmem perf > results.txt
  1. benchfilter
    1
    benchfilter pkg:perf results.txt > filtered.txt

结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
goos: linux
goarch: amd64
pkg: perf
cpu: AMD Ryzen 7 7840HS w/ Radeon 780M Graphics

BenchmarkConcatPlus100-8 16501 68975 ns/op 387027 B/op 99 allocs/op
BenchmarkConcatPlus1000-8 169 7.939114e+06 ns/op 3.8739447e+07 B/op 1002 allocs/op
BenchmarkConcatBuilder100-8 157008 8399 ns/op 34992 B/op 12 allocs/op
BenchmarkConcatBuilder1000-8 18838 56281 ns/op 286130 B/op 19 allocs/op
BenchmarkConcatBuffer100-8 172484 6617 ns/op 29616 B/op 9 allocs/op
BenchmarkConcatBuffer1000-8 22144 66326 ns/op 264369 B/op 12 allocs/op
BenchmarkConcatJoin100-8 393951 2982 ns/op 9984 B/op 2 allocs/op
BenchmarkConcatJoin1000-8 52179 26365 ns/op 90112 B/op 2 allocs/op
  1. benchstat
1
benchstat filtered.txt > stat.txt

结果为:

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
37
38
39
40
41
42
goos: linux
goarch: amd64
pkg: perf
cpu: AMD Ryzen 7 7840HS w/ Radeon 780M Graphics
│ filtered.txt │
│ sec/op │
ConcatPlus100-8 68.98µ ± ∞ ¹
ConcatPlus1000-8 7.939m ± ∞ ¹
ConcatBuilder100-8 8.399µ ± ∞ ¹
ConcatBuilder1000-8 56.28µ ± ∞ ¹
ConcatBuffer100-8 6.617µ ± ∞ ¹
ConcatBuffer1000-8 66.33µ ± ∞ ¹
ConcatJoin100-8 2.982µ ± ∞ ¹
ConcatJoin1000-8 26.37µ ± ∞ ¹
geomean 41.58µ
¹ need >= 6 samples for confidence interval at level 0.95

│ filtered.txt │
│ B/op │
ConcatPlus100-8 378.0Ki ± ∞ ¹
ConcatPlus1000-8 36.94Mi ± ∞ ¹
ConcatBuilder100-8 34.17Ki ± ∞ ¹
ConcatBuilder1000-8 279.4Ki ± ∞ ¹
ConcatBuffer100-8 28.92Ki ± ∞ ¹
ConcatBuffer1000-8 258.2Ki ± ∞ ¹
ConcatJoin100-8 9.750Ki ± ∞ ¹
ConcatJoin1000-8 88.00Ki ± ∞ ¹
geomean 174.9Ki
¹ need >= 6 samples for confidence interval at level 0.95

│ filtered.txt │
│ allocs/op │
ConcatPlus100-8 99.00 ± ∞ ¹
ConcatPlus1000-8 1.002k ± ∞ ¹
ConcatBuilder100-8 12.00 ± ∞ ¹
ConcatBuilder1000-8 19.00 ± ∞ ¹
ConcatBuffer100-8 9.000 ± ∞ ¹
ConcatBuffer1000-8 12.00 ± ∞ ¹
ConcatJoin100-8 2.000 ± ∞ ¹
ConcatJoin1000-8 2.000 ± ∞ ¹
geomean 17.73
¹ need >= 6 samples for confidence interval at level 0.95

2.3 结果解析

stat.txt结果有三个维度

  • 时间性能sec/op

    第一行的68.98µ ± ∞ 表示平均时间为68.98微秒,后面的正负无穷表示置信区间,由于我们只做了一次实验,当结果超过6个时,才会有置信区间的值。

    而最后一行的geomean表示benchmark的几何平均耗时,用于整体性能的体现。

  • 内存使用B/op

​ 378.0ki 指的就是每次操作分配了378KiB内存。

  • 内存分配次数allocs/op

​ 99.00 指每次操作分配了99次内存

我们把置信区间也搞出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 以下是之前的命令使用流程
go test -bench=. -benchmem perf > results.txt
benchfilter pkg:perf results.txt > filtered.txt
benchstat filtered.txt > stat.txt

# 以下是一个shell脚本,多次实验
for i in {1..10}; do
go test -bench=. -benchmem perf > ./out/result.$i.txt
done

cat ./out/result.*.txt > ./out/all_results.txt

benchfilter pkg:perf ./out/all_results.txt > ./out/all_filtered.txt
benchstat ./out/all_filtered.txt > ./out/all_stat.txt

结果如下:

image-20251014175002812

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
37
38
39
goos: linux
goarch: amd64
pkg: perf
cpu: AMD Ryzen 7 7840HS w/ Radeon 780M Graphics
│ ./out/all_filtered.txt │
│ sec/op │
ConcatPlus100-8 72.91µ ± 7%
ConcatPlus1000-8 7.416m ± 9%
ConcatBuilder100-8 7.628µ ± 9%
ConcatBuilder1000-8 60.46µ ± 3%
ConcatBuffer100-8 6.976µ ± 7%
ConcatBuffer1000-8 56.42µ ± 3%
ConcatJoin100-8 3.262µ ± 8%
ConcatJoin1000-8 25.65µ ± 5%
geomean 41.14µ

│ ./out/all_filtered.txt │
│ B/op │
ConcatPlus100-8 378.0Ki ± 0%
ConcatPlus1000-8 36.94Mi ± 0%
ConcatBuilder100-8 34.17Ki ± 0%
ConcatBuilder1000-8 279.4Ki ± 0%
ConcatBuffer100-8 28.92Ki ± 0%
ConcatBuffer1000-8 258.2Ki ± 0%
ConcatJoin100-8 9.750Ki ± 0%
ConcatJoin1000-8 88.00Ki ± 0%
geomean 174.9Ki

│ ./out/all_filtered.txt │
│ allocs/op │
ConcatPlus100-8 99.00 ± 0%
ConcatPlus1000-8 1.002k ± 0%
ConcatBuilder100-8 12.00 ± 0%
ConcatBuilder1000-8 19.00 ± 0%
ConcatBuffer100-8 9.000 ± 0%
ConcatBuffer1000-8 12.00 ± 0%
ConcatJoin100-8 2.000 ± 0%
ConcatJoin1000-8 2.000 ± 0%
geomean 17.73

经过对比,不同的String拼接的性能对比如下:

image-20251014184950263

我们可以看出:

  • strings.Join 是最优选择:最快、最省内存、最少分配
  • += 的方式性能较差:性能差、内存爆炸、GC 压力大
  • strings.Builder / bytes.Buffer 适合流式拼接,性能优秀
  • 所有方法在 N=1000 时性能差距更明显,说明问题随规模放大

3. 总结

经过以上内容,我们理解了一套使用benchstat的流程,这一套流程可以对benchmark的结果进行专业的对比分析全流程。对以程序员的角度理解程序非常有帮助。

这个流程本质上是取数据 -> 数据清洗 -> 可视化展示的过程

  1. 通过go test -bench -benchmem的方式获取性能数据。
  2. 通过benchfilterbenchstat 对数据进行一定的整理。
  3. 拿到结果数据之后对数据进行可视化展示,本文的可视化展示是通过 python + numpy + matplotlib 来实现的,通过对图例和维度的调整可以清晰地展示结果。

这套流程的主要优点是,它给了一套通用的性能测试方法论,以及一套非常科学的统计维度和计算方式。从方法论上来讲,取数据 -> 数据清洗 -> 可视化展示这个大方向的流程非常科学。从统计维度上来讲,这套工具是由一群来自谷歌的非常有经验的性能测试工程师给出的非常典型的,包含平均执行时间、内存占用、内存申请次数的统计维度,这些维度精准地代表了程序的性能表现。而从计算方式来讲,它告诉了我们应该对拿到的数据选取什么样的计算方式,比如运行多次取平均值,仅取稳定的数据,多次试验取几何平均值,多次实验得到置信区间等。


hxy注:这套流程的主要缺点我认为也很明显,

  1. 数据清洗的过程有些简单,仅包含一些简单的filter,也许是我还没有测试更多的包,但大概率还是使用python自写脚本进行数据处理更加自由和定制化。
  2. 虽然维度选择非常经典,但是有时候就是会需要更加全面的测试维度。

我们取其精华即可。


4. 下步计划

继续完善工具学习试用计划。

使用TMA脚本,以系统的视角来继续理解性能测试与优化。