写点什么

Go 语言基准测试入门

作者:FunTester
  • 2024-10-14
    河北
  • 本文字数:3569 字

    阅读完需:约 12 分钟

之前在写 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: darwingoarch: arm64pkg: tempProject/testBenchmarkStringConcat-2         100000000               11.61 ns/op     88166.09 MB/s          0 B/op          0 allocs/opBenchmarkStringConcat-4         100000000               11.71 ns/op     87425.82 MB/s          0 B/op          0 allocs/opBenchmarkStringConcat-8         100000000               11.62 ns/op     88098.01 MB/s          0 B/op          0 allocs/opBenchmarkStringBuilder-2        34372545                33.96 ns/op     30155.42 MB/s         24 B/op          2 allocs/opBenchmarkStringBuilder-4        35140291                33.75 ns/op     30341.89 MB/s         24 B/op          2 allocs/opBenchmarkStringBuilder-8        34851384                33.99 ns/op     30129.57 MB/s         24 B/op          2 allocs/opPASSok      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,开发者可以更灵活、高效地编写基准测试,从而准确评估和优化代码性能。

发布于: 刚刚阅读数: 5
用户头像

FunTester

关注

公众号:FunTester,800篇原创,欢迎关注 2020-10-20 加入

Fun·BUG挖掘机·性能征服者·头顶锅盖·Tester

评论

发布
暂无评论
Go 语言基准测试入门_FunTester_InfoQ写作社区