之前在写 Java 的文章的时候,如果想在本地进行某段代码的性能测试(通常是对比另外一段或者几段),就会用到基准测试框架 JMH ,也的确非常好用。虽然我学习 Go 语言有一段时间了,对于基准测试还没有涉猎,下面就分享 Go 语言的基准测试入门实践。
什么是基准测试
基准测试(Benchmarking)是一种通过执行预定任务来测量系统或代码性能的测试方法,主要目的是评估软件在特定负载和条件下的表现,常常关注运行时间、内存消耗、吞吐量和延迟等指标。在开发过程中,基准测试用于量化代码的性能差异,帮助开发者识别性能瓶颈并为优化提供有力的数据支持。
它的主要用途在于评估性能、监测性能回归、指导优化以及比较不同技术或方案。通过基准测试,开发者可以准确了解不同实现方案的性能表现,确保优化过程中不会引入性能退化的问题,确保代码在实际场景下的表现符合预期。基准测试的结果为找到性能瓶颈提供了数据依据,避免盲目优化的风险。
在性能优化中,基准测试的作用尤为重要。它不仅能提供清晰的定量分析,让优化决策基于真实的数据,而非直觉或经验,还能帮助快速发现代码中的性能瓶颈,如某个函数或 I/O 操作的效率低下。此外,基准测试也可以验证优化后的效果,通过对比前后的数据,判断所做的更改是否真正提升了性能。同时,基准测试还可以融入到持续集成和持续交付的流程中,确保系统在长期演进中的性能稳定性。
Go 语言的基准测试
Go 语言的基准测试用于评估代码的性能表现,尤其是函数和操作的执行时间。它通过多次运行相同的代码片段,计算平均耗时,提供准确的测试结果。基准测试常用于比较不同实现方案、验证优化效果,确保代码在不同场景下的效率,并能够检测性能回归。
此外,Go 的基准测试支持设置不同的输入条件,模拟实际负载,帮助开发者全面评估代码性能。它还能追踪内存分配情况,发现潜在的内存问题。基准测试生成的报告为优化提供了有力的数据支持,使开发者能更有效地识别瓶颈并进行改进。
testing.B 是 Go 语言中的一个结构体,用于基准测试,专门用于测量代码的性能。它提供了执行基准测试的必要工具,例如控制测试运行的次数、记录执行时间、追踪内存分配等。在使用 testing.B 进行基准测试时,Go 会自动多次运行测试代码,以确保结果的稳定性和准确性,帮助开发者准确评估代码的性能表现并发现优化机会。
基准测试实践
编写规范
在开始之前,我们先来看看 Go 语言的基准测试用例编写过程中,需要遵循的一些规范。在 Go 语言中,编写基准测试时遵循一定的规范是非常重要的,这有助于确保测试的准确性、可读性和可维护性。以下是一些基准测试编写的基本规范:
- 命名规范:基准测试函数应以
Benchmark
开头,后跟被测试功能的描述。命名应清晰明了,以便他人理解其测试的内容。例如:BenchmarkFunctionName
。 - 使用
testing.B
:基准测试的参数必须是*testing.B
类型,利用其提供的方法来执行性能测试。 - 循环测试:在基准测试中,使用
b.N
来控制循环次数,确保基准测试运行足够多次,以获取稳定的性能数据。b.N
的值由 Go 的测试框架自动管理,以避免人为干预。 - 避免副作用:基准测试应避免具有副作用的操作,确保每次运行的结果不受外部因素影响。确保测试函数中的代码是可重复执行的。
- 资源清理:如果基准测试中使用了需要清理的资源(如文件、网络连接等),应在测试结束后确保资源得到妥善处理,尽量使用 defer 语句来保证清理操作的执行。
代码实践
下面我通过一些简单的例子展示 Go 语言基准测试的 code,我用了两种字符串拼接的方法来演示。
package test
import (
"strings"
"testing")
// 基准测试示例:测试使用 + 运算符连接字符串的性能
func BenchmarkStringConcat(b *testing.B) {
setConfig(b)
str1 := "Hello"
str2 := "FunTester"
for i := 0; i < b.N; i++ {
_ = str1 + str2
}
}
// 基准测试示例:测试使用 strings.Builder 连接字符串的性能
func BenchmarkStringBuilder(b *testing.B) {
setConfig(b)
str1 := "Hello"
str2 := "FunTester"
for i := 0; i < b.N; i++ {
var builder strings.Builder
builder.WriteString(str1)
builder.WriteString(str2)
_ = builder.String()
}
}
func setConfig(b *testing.B) {
b.SetParallelism(4) // 设置并行测试的 goroutine 数量
b.ReportAllocs() // 开启内存分配跟踪
b.SetBytes(1024) // 设置每次操作处理的字节数
}
我们可以直接在 IDE 中执行,也可以通过以下命令行执行 benchmark 基准测试。
go test -bench . -cpu 1,2,4,8,12
控制台输出:
goos: darwin
goarch: arm64
pkg: tempProject/test
BenchmarkStringConcat-2 100000000 11.61 ns/op 88166.09 MB/s 0 B/op 0 allocs/op
BenchmarkStringConcat-4 100000000 11.71 ns/op 87425.82 MB/s 0 B/op 0 allocs/op
BenchmarkStringConcat-8 100000000 11.62 ns/op 88098.01 MB/s 0 B/op 0 allocs/op
BenchmarkStringBuilder-2 34372545 33.96 ns/op 30155.42 MB/s 24 B/op 2 allocs/op
BenchmarkStringBuilder-4 35140291 33.75 ns/op 30341.89 MB/s 24 B/op 2 allocs/op
BenchmarkStringBuilder-8 34851384 33.99 ns/op 30129.57 MB/s 24 B/op 2 allocs/op
PASS
ok tempProject/test 7.951s
测试报告解读
测试报告的解读可以从以下几个方面进行分析:
环境信息:
- goos: darwin:表示操作系统是 macOS(Darwin 是 macOS 的核心)。
- goarch: arm64:表示运行的架构是 ARM64(常见于苹果的 M1 和 M2 芯片)。
- pkg: tempProject/test:表示基准测试运行在 tempProject/test 包中。
基准测试结果:
每一行结果的格式如下:
BenchmarkName-N 总运行次数 平均耗时 吞吐量 内存使用 内存分配次数
基准测试:
- BenchmarkStringConcat-2:使用 + 运算符连接字符串,运行 100,000,000 次,平均耗时 11.61 ns/op,吞吐量 88166.09 MB/s,内存使用 0 B/op,内存分配次数 0 allocs/op。
- BenchmarkStringConcat-4:相似的性能,平均耗时 11.71 ns/op,吞吐量稍低 87425.82 MB/s。
- BenchmarkStringConcat-8:表现相近,平均耗时 11.62 ns/op,吞吐量 88098.01 MB/s。
测试结论
- 性能对比:BenchmarkStringConcat 的性能明显优于 BenchmarkStringBuilder,适合用于简单字符串连接的场景。
- 内存管理:BenchmarkStringConcat 没有进行任何内存分配,而 BenchmarkStringBuilder 则有一定的内存使用和分配次数,显示出在性能敏感的环境中,选择合适的字符串连接方式至关重要。
- 测试时长:整个测试运行的时间为 7.951s,这个时间在基准测试中是合理的,表明测试的复杂度和工作量适中。
通过以上测试,可以帮助我们在实际项目中选择合适的字符串操作方式,以优化性能和内存使用。
testing.B 常用 API
在 Go 语言的基准测试中,*testing.B
提供了一系列常用的 API,帮助我们进行高效的性能测试和评估。以下是一些常用的 *testing.B
方法及其功能:
-
b.N
:这是一个整数值,表示基准测试要执行的循环次数。开发者在基准测试中通常使用b.N
来控制代码的执行次数,以获取稳定的性能数据。 -
b.ResetTimer()
:在基准测试中,可以调用此方法重置计时器。这在需要进行一些初始化操作后,确保不将这些操作的时间计入基准测试时非常有用。 -
b.StopTimer()
:此方法用于停止基准测试的计时。可以在测试中使用此方法来排除某些不需要计入测试时间的操作,例如初始化或清理操作。 -
b.StartTimer()
:用于重新启动计时器。当需要停止计时并进行某些不希望计入测试的操作后,可以通过此方法恢复计时。 -
b.ReportAllocs()
:调用此方法可以在基准测试结束时报告内存分配情况。如果希望监测内存的使用情况,可以在基准测试开始时调用此方法。 -
b.Log(args ...interface{})
:该方法允许开发者在基准测试中记录信息,类似于fmt.Println
。它可以用于调试或输出测试过程中获取的某些特定数据。 -
b.SetBytes(n int64)
:此方法用于设置测试中处理的数据大小,以便在报告中显示吞吐量时提供更准确的信息。通常用于计算每个操作处理的字节数。 -
b.Run(name string, f func(b *B))
:这个方法允许在基准测试中运行子基准测试。这样可以组织复杂的基准测试结构,同时提供更好的结果分析。
通过合理利用这些 API,开发者可以更灵活、高效地编写基准测试,从而准确评估和优化代码性能。
FunTester 原创精华