目录

go 如何进行 Benchmark 基准测试

一、为什么要进行基准测试?

开发人员在开发过程中,对一些函数、方法进行性能优化,那么如何知道优化的结论呢?到底优化成功没有?优化了哪些呢?

比如内存少了,耗时少、cpu 占用降低了等等…

本文以【字符串高效拼接】为例,带你入门 go 的Benchmark 基准测试

二、Benchmark 基准测试

(一)准备

性能测试受环境的影响很大,为了保证测试的可重复性,在进行性能测试时,尽可能地保持测试环境的稳定。

  • 机器处于闲置状态,测试时不要执行其他任务,也不要和其他人共享硬件资源。
  • 机器是否关闭了节能模式,一般笔记本会默认打开这个模式,测试时关闭。
  • 避免使用虚拟机和云主机进行测试,一般情况下,为了尽可能地提高资源的利用率,虚拟机和云主机 CPU 和内存一般会超分配,超分机器的性能表现会非常地不稳定。

⚠️:如果你确实没办法保证以上条件,那么只是在比对测试的时候,两次运行时机器环境要尽可能保持一致,确保基准测试的准确性

接下来以【字符串高效拼接:对字符串连续拼接20次】为例,进行基准测试,测试参数设置:

  • -cpu=2,4 : 用2,4核cpu分别进行测试

  • -count=3: 进行 3 轮测试

  • -benchmem:输出内存、cpu测试结论

(二)基准测试

拼接字符串 + 和 strings.Builder 性能对比

基准测试格式:

func Benchmark[测试函数名](b *testing.B){
    for n := 0; n < b.N; n++ {
        // 调用测试的函数
    }
}

1. 拼接字符串 +

package main

import (
    "testing"
)

func BenchmarkStringAdd(b *testing.B) {
    for n := 0; n < b.N; n++ {
        StringAdd()
    }
}

func StringAdd() string {
    var str = "生活是一面镜子。你对它笑 它就对你笑 你对它哭 它也对你哭。"
    for i := 0; i < 100; i++ {
        str += str
    }
    return str
}

运行:

mac@fiveyoboy learn-go % go test -bench='BenchmarkStringAdd'  -cpu=2,4 -count=3 -benchmem .
goos: darwin // 操作系统
goarch: arm64  // cpu 处理器架构(我这里是 mac )
pkg: fiveyoboy/learn-go/ // 测试执行所在的包
cpu: Apple M1 Pro // cpu
// 测试函数-使用CPU核数      执行次数   每次耗时ns(左移9位为秒)       分配内存字节B(左移6位为m) 每次分配内存次数
BenchmarkStringAdd-2         118          10313506 ns/op        176171351 B/op        20 allocs/op
BenchmarkStringAdd-2          99          10152684 ns/op        176171362 B/op        20 allocs/op
BenchmarkStringAdd-2         100          10406031 ns/op        176171354 B/op        20 allocs/op

// 上面使用2核进行测试,测试3次,下面是用4核进行测试,测试 3 次
BenchmarkStringAdd-4         120          10045046 ns/op        176171401 B/op        20 allocs/op
BenchmarkStringAdd-4         100          10109044 ns/op        176171398 B/op        20 allocs/op
BenchmarkStringAdd-4         120           9977272 ns/op        176171405 B/op        20 allocs/op
PASS
ok      fiveyoboy/learn-go       20.302s

完整说明请看之后的内容

2.拼接字符串 strings.Builder

package main

import (
    "testing"
)


func BenchmarkStringBuilderAdd(b *testing.B) {
    for n := 0; n < b.N; n++ {
        StringBuilderAdd()
    }
}

func StringBuilderAdd() string {
    var builder strings.Builder
    var str = "生活是一面镜子。你对它笑 它就对你笑 你对它哭 它也对你哭。"
    builder.WriteString(str)
    for i := 0; i < 20; i++ {
        builder.WriteString(str)
    }
    return builder.String()
}

运行:

mac@fiveyoboy learn-go % go test -bench='BenchmarkStringBuilderAdd'  -cpu=2,4 -count=3 -benchmem .
goos: darwin // 操作系统
goarch: arm64  // cpu 处理器架构(我这里是 mac )
pkg: fiveyoboy/learn-go/ // 测试执行所在的包
cpu: Apple M1 Pro // cpu
// 测试函数-使用CPU核数      执行次数   每次耗时ns(左移9位为秒)       分配内存字节B(左移6位为m) 每次分配内存次数
BenchmarkStringBuilderAdd-2      2170213               546.5 ns/op          4320 B/op          6 allocs/op
BenchmarkStringBuilderAdd-2      2217442               548.2 ns/op          4320 B/op          6 allocs/op
BenchmarkStringBuilderAdd-2      2229522               543.1 ns/op          4320 B/op          6 allocs/op

// 上面使用2核进行测试,测试3次,下面是用4核进行测试,测试 3 次
BenchmarkStringBuilderAdd-4      2068552               563.3 ns/op          4320 B/op          6 allocs/op
BenchmarkStringBuilderAdd-4      2069716               561.8 ns/op          4320 B/op          6 allocs/op
BenchmarkStringBuilderAdd-4      2103303               587.5 ns/op          4320 B/op          6 allocs/op

PASS
ok      fiveyoboy/learn-go       9.302s 

3.结论

对比两种基准测试结果,我相信你也能看出来结论了,无论是从内存分配、执行耗时来看,相同环境下,string.Builder 的性能要远远高于 “+”

三、执行参数说明

// 执行基准测试,-cpu 指定 cpu 核数,-count 执行轮数,-benchmem 输出cpu、内存测试结论, 点 . 表示在执行测试的目录为当前目录
go test -bench='BenchmarkStringBuilderAdd' -cpu=2,4 -count=3 -benchmem .
执行参数 简介 备注
-bench=‘Fib$’ 可传入正则,匹配用例 如只运行以 Fib 结尾的 benchmark 用例
-cpu=2,4 可改变 CPU 核数 GOMAXPROCS,CPU核数,默认机器的核数
-benchtime=5s
-benchtime=50x
可指定执行时间或具体次数 benchmark 的默认时间是 1s,决定了b.N的次数,测试时间
-benchtime的值除了是时间外,还可以是具体的次数。
例如,执行 30 次可以用-benchtime=30x
-count=3 可设置 benchmark 轮数 参数可以用来设置 benchmark 的轮数,默认是1轮
-benchmem 可查看内存分配量和分配次数 参数可以度量内存分配的次数,添加此参数后会在结果之后显示 n allocs/op
也就是内存分配了n次,m B/op,
总共分配了 m 字节8003641 B/op == 8 003 641 B=7.63M
-cpuprofile=./cpu.prof 生成CPU性能分析 执行 CPU profiling,并把结果保存在 cpu.prof
-memprofile=./mem.prof 生成内存性能分析 执行 Mem profiling,并把结果保存在 cpu.prof 文件中

四、执行结果说明

mac@fiveyoboy learn-go % go test -bench='BenchmarkStringBuilderAdd'  -cpu=2,4 -count=3 -benchmem .

goos: darwin // 操作系统
goarch: arm64  // cpu 处理器架构(我这里是 mac )
pkg: fiveyoboy/learn-go/ // 测试执行所在的包
cpu: Apple M1 Pro // cpu

// 测试函数-使用CPU核数      执行次数   每次耗时ns(左移9位为秒)       分配内存字节B(左移6位为m) 每次分配内存次数
BenchmarkStringBuilderAdd-2      2170213               546.5 ns/op          4320 B/op          6 allocs/op
BenchmarkStringBuilderAdd-2      2217442               548.2 ns/op          4320 B/op          6 allocs/op
BenchmarkStringBuilderAdd-2      2229522               543.1 ns/op          4320 B/op          6 allocs/op

// 上面使用2核进行测试,测试3次,下面是用4核进行测试,测试 3 次
BenchmarkStringBuilderAdd-4      2068552               563.3 ns/op          4320 B/op          6 allocs/op
BenchmarkStringBuilderAdd-4      2069716               561.8 ns/op          4320 B/op          6 allocs/op
BenchmarkStringBuilderAdd-4      2103303               587.5 ns/op          4320 B/op          6 allocs/op

PASS
ok      fiveyoboy/learn-go       9.302s 
结果参数 简介
b.N benchmark的次数b.N,默认是-benchtime=1s内执行的次数,
b.N 从 -benchtime=1s开始,如果该用例能够在 1s 内完成,
b.N 的值大概以 1, 2, 3, 5, 10, 20, 30, 50, 100 这样的序列递增,越到后面,增加得越快
goos 操作系统
goarch cpu处理器
pkg 所在的包
BenchmarkGenerate-2
BenchmarkGenerate-4
BenchmarkGenerate-8
【测试函数名】-【cpu核数】BenchmarkGenerate-2,
表示使用2核CPU执行BenchmarkGenerate测试函数
ns/op 每次耗时多少毫秒
B/op 每次分配内存多少字节Bytes
allocs/op 每次分配多少次内存

其他

为了提高基准测试的准确度(如果基准测试在循环前需要一些耗时的配置,则可以先重置定时器):

  1. StartTimer:开始对测试进行计时。该方法会在基准测试开始时自动被调用,我们也可以在调用 StopTimer 之后恢复计时;
  2. StopTimer:停止对测试进行计时。当你需要执行一些复杂的初始化操作,并且你不想对这些操作进行测量时,就可以使用这个方法来暂时地停止计时;
  3. ResetTimer:对已经逝去的基准测试时间以及内存分配计数器进行清零。对于正在运行中的计时器,这个方法不会产生任何效果。本节开头有使用示例。
func BenchmarkStringBuilderAdd(b *testing.B) {
    before() // 需要执行,但是又不希望参与基准测试耗时计算
    b.ResetTimer() // 从现在开始计时
    for n := 0; n < b.N; n++ {
        StringBuilderAdd()
    }
}

关于 go 如何高效的拼接字符串,请移步文章:

go 如何高效的拼接字符串)ng)fiveyoboy.com/articles/go-concat-string/)