Go 语言基准测试入门

科技   2024-10-14 08:00   河北  

之前在写 Java 的文章的时候,如果想在本地进行某段代码的性能测试(通常是对比另外一段或者几段),就会用到基准测试框架 JMH ,也的确非常好用。虽然我学习 Go 语言有一段时间了,对于基准测试还没有涉猎,下面就分享Go 语言的基准测试入门实践。

什么是基准测试

基准测试(Benchmarking)是一种通过执行预定任务来测量系统或代码性能的测试方法,主要目的是评估软件在特定负载和条件下的表现,常常关注运行时间、内存消耗、吞吐量和延迟等指标。在开发过程中,基准测试用于量化代码的性能差异,帮助开发者识别性能瓶颈并为优化提供有力的数据支持。

它的主要用途在于评估性能、监测性能回归、指导优化以及比较不同技术或方案。通过基准测试,开发者可以准确了解不同实现方案的性能表现,确保优化过程中不会引入性能退化的问题,确保代码在实际场景下的表现符合预期。基准测试的结果为找到性能瓶颈提供了数据依据,避免盲目优化的风险。

在性能优化中,基准测试的作用尤为重要。它不仅能提供清晰的定量分析,让优化决策基于真实的数据,而非直觉或经验,还能帮助快速发现代码中的性能瓶颈,如某个函数或 I/O 操作的效率低下。此外,基准测试也可以验证优化后的效果,通过对比前后的数据,判断所做的更改是否真正提升了性能。同时,基准测试还可以融入到持续集成和持续交付的流程中,确保系统在长期演进中的性能稳定性。

Go 语言的基准测试

Go 语言的基准测试用于评估代码的性能表现,尤其是函数和操作的执行时间。它通过多次运行相同的代码片段,计算平均耗时,提供准确的测试结果。基准测试常用于比较不同实现方案、验证优化效果,确保代码在不同场景下的效率,并能够检测性能回归。

此外,Go 的基准测试支持设置不同的输入条件,模拟实际负载,帮助开发者全面评估代码性能。它还能追踪内存分配情况,发现潜在的内存问题。基准测试生成的报告为优化提供了有力的数据支持,使开发者能更有效地识别瓶颈并进行改进。

testing.B 是 Go 语言中的一个结构体,用于基准测试,专门用于测量代码的性能。它提供了执行基准测试的必要工具,例如控制测试运行的次数、记录执行时间、追踪内存分配等。在使用 testing.B 进行基准测试时,Go 会自动多次运行测试代码,以确保结果的稳定性和准确性,帮助开发者准确评估代码的性能表现并发现优化机会。

基准测试实践

编写规范

在开始之前,我们先来看看 Go 语言的基准测试用例编写过程中,需要遵循的一些规范。在 Go 语言中,编写基准测试时遵循一定的规范是非常重要的,这有助于确保测试的准确性、可读性和可维护性。以下是一些基准测试编写的基本规范:

  1. 命名规范:基准测试函数应以 Benchmark 开头,后跟被测试功能的描述。命名应清晰明了,以便他人理解其测试的内容。例如:BenchmarkFunctionName
  2. 使用 testing.B:基准测试的参数必须是 *testing.B 类型,利用其提供的方法来执行性能测试。
  3. 循环测试:在基准测试中,使用 b.N 来控制循环次数,确保基准测试运行足够多次,以获取稳定的性能数据。b.N 的值由 Go 的测试框架自动管理,以避免人为干预。
  4. 避免副作用:基准测试应避免具有副作用的操作,确保每次运行的结果不受外部因素影响。确保测试函数中的代码是可重复执行的。
  5. 资源清理:如果基准测试中使用了需要清理的资源(如文件、网络连接等),应在测试结束后确保资源得到妥善处理,尽量使用 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 方法及其功能:

  1. b.N:这是一个整数值,表示基准测试要执行的循环次数。开发者在基准测试中通常使用 b.N 来控制代码的执行次数,以获取稳定的性能数据。
  2. b.ResetTimer():在基准测试中,可以调用此方法重置计时器。这在需要进行一些初始化操作后,确保不将这些操作的时间计入基准测试时非常有用。
  3. b.StopTimer():此方法用于停止基准测试的计时。可以在测试中使用此方法来排除某些不需要计入测试时间的操作,例如初始化或清理操作。
  4. b.StartTimer():用于重新启动计时器。当需要停止计时并进行某些不希望计入测试的操作后,可以通过此方法恢复计时。
  5. b.ReportAllocs():调用此方法可以在基准测试结束时报告内存分配情况。如果希望监测内存的使用情况,可以在基准测试开始时调用此方法。
  6. b.Log(args ...interface{}):该方法允许开发者在基准测试中记录信息,类似于 fmt.Println。它可以用于调试或输出测试过程中获取的某些特定数据。
  7. b.SetBytes(n int64):此方法用于设置测试中处理的数据大小,以便在报告中显示吞吐量时提供更准确的信息。通常用于计算每个操作处理的字节数。
  8. b.Run(name string, f func(b *B)):这个方法允许在基准测试中运行子基准测试。这样可以组织复杂的基准测试结构,同时提供更好的结果分析。

通过合理利用这些 API,开发者可以更灵活、高效地编写基准测试,从而准确评估和优化代码性能。

FunTester
FunTester 原创精华


FunTester
万粉千文|百无一用
 最新文章