当前位置:首页 > 数码 > GO-在-中打造准确的基准测试的最佳实践 (go在中间的英语单词有哪些)

GO-在-中打造准确的基准测试的最佳实践 (go在中间的英语单词有哪些)

admin4个月前 (05-09)数码28

概述

在优化代码时,不应该对性能进行猜测。编写优化时,会有许多因素可能起作用,即使我们对结果有很强的看法,测试这些因素也几乎不会是一个坏主意。但是,编写基准测试并不简单。很容易编写不准确的基准测试,并且基于这些测试得出错误的假设。文章的目标是探讨导致不准确的四个常见和具体陷阱:

通用概念

在讨论这些陷阱之前,让我们简要回顾一下 Go 语言中基准测试的工作原理。基准测试的框架大致如下:

func BenchmarkFoo(b testing.B) {
  for i := 0; i < b.N; i++ {
    foo()
  }
}
  
  • 函数名以前缀 Benchmark 开头。
  • 被测试的函数( foo )在循环内被调用。
  • b.N 代表着可变数量的迭代次数。

在运行基准测试时,Go 会尝试使其匹配所请求的基准测试时间。基准测试时间默认设置为 1 秒,可以使用 -benchtime 标志进行更改。从 1 开始;如果基准测试在 1 秒内完成,会增加 b.N ,然后再次运行基准测试,直到大致匹配为止。

$ gotest -bench=.
cpu: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
BenchmarkFoo-47316511228ns/op
  

在这里,基准测试大约花费了 1 秒钟,被执行了 73 次,平均执行时间为 16,511,228 纳秒。我们可以使用 -benchtime 来更改基准测试时间:

$ gotest -bench=. -benchtime=2s
BenchmarkFoo-415015832169ns/op
  

执行次数大约是上一个基准测试的两倍。

陷阱

不重置或暂停计时器

在某些情况下,我们需要在基准测试循环之前执行一些操作。这些操作可能需要相当长的时间(例如,生成一个大型数据切片),可能会对基准测试结果产生显著影响:

func BenchmarkFoo(b testing.B) {
  expensiveSetup()
  for i := 0; i < b.N; i++ {
    functionUnderTest()
  }
}
  
go在中间的英语单词有哪些

在这种情况下,我们可以在进入循环之前使用 ResetTimer 方法:

func BenchmarkFoo(b testing.B) {
  expensiveSetup()
  b.ResetTimer() // Reset the benchmark timer
  for i := 0; i < b.N; i++ {
    functionUnderTest()
  }
}
  

调用 ResetTimer 会将自测试开始以来的经过时间和内存分配计数器归零。这样,一个昂贵的设置步骤可以从测试结果中排除。

如果我们不仅需要在测试中执行一次昂贵的设置步骤,而是需要在每个循环迭代中都执行呢?

func BenchmarkFoo(b testing.B) {
  for i := 0; i < b.N; i++ {
    expensiveSetup()
    functionUnderTest()
  }
}
  

我们不能重置计时器,因为那样会在每次循环迭代期间执行。但是我们可以停止和恢复基准测试计时器,将调用 expensiveSetup 包裹起来:

func BenchmarkFoo(b testing.B) {
  for i := 0; i < b.N; i++ {
    b.StopTimer() // Pause the benchmark timer
    expensiveSetup()
    b.StartTimer() // Resume the benchmark timer
    functionUnderTest()
  }
}
  

在这里,我们暂停基准测试计时器来执行昂贵的设置步骤,然后恢复计时器。注意:关于这种方法有一个需要记住的地方:如果被测试的函数执行速度远远比设置函数要快,那么基准测试可能会花费太长时间才能完成。原因是它需要比更长的时间才能完成。基准测试时间的计算完全基于 functionUnderTest 的执行时间。所以,如果在每个循环迭代中等待了相当长的时间,基准测试就会比 1 秒要慢得多。

如果我们想保持基准测试时间为 1 秒,我们需要将 expensiveSetup 放在基准测试函数外部,并使用 b.StopTimer() b.StartTimer() 方法在每个循环迭代中暂停和恢复计时器:

func BenchmarkFoo(b testing.B) {
  expensiveSetup()
  for i := 0; i < b.N; i++ {
    b.StopTimer()
    // Perform any necessary setup for this iteration
    b.StartTimer()
    functionUnderTest()
  }
}
  

重复的初始化

另一个陷阱是重复的初始化。这是指在基准测试循环中重复初始化变量或资源的情况。这可能会对基准测试结果产生重大影响,因为初始化操作可能非常耗时。

func BenchmarkFoo(b testing.B) {
  for i := 0; i < b.N; i++ {
    var mySlice = make([]int, 10000)
    functionUnderTest(mySlice)
  }
}
  

在这个示例中, make([]int, 10000) 操作在每次循环迭代中都会执行。这可能会对基准测试结果产生重大影响,因为创建切片是一个耗时的操作。要避免重复初始化,我们应该将变量的初始化移到循环外部:

var mySlice []int

func BenchmarkFoo(b testing.B) {
  mySlice = make([]int, 10000)
  for i := 0; i < b.N; i++ {
    functionUnderTest(mySlice)
  }
}
  

goroutine 泄漏

goroutine 泄漏是指在基准测试循环中启动 goroutine,但未正确关闭的情况。这可能会导致内存泄漏和性能问题。

func BenchmarkFoo(b testing.B) {
  for i := 0; i < b.N; i++ {
    go func() {
      // Perform some work
    }()
  }
}
  

在这个示例中,每次循环迭代都会启动一个新的 goroutine。但是,这些 goroutine 永远不会关闭。这会导致 goroutine 泄漏和内存泄漏。要避免 goroutine 泄漏,我们应该确保在基准测试循环中正确关闭所有 goroutine:

func BenchmarkFoo(b testing.B) {
  for i := 0; i < b.N; i++ {
    wg := sync.WaitGroup{}
    wg.Add(1)
    go func() {
      defer wg.Done()
      // Perform some work
    }()
    wg.Wait()
  }
}
  

并行化基准测试

Go 语言允许我们将基准测试并行化,以便在多个 CPU 核上同时运行它们。这可以提高基准测试的性能,但它也可能会导致不准确的结果。

go test -bench=. -count=1000 -parallel=4
  

在这个示例中,我们使用 -parallel=4 标志将基准测试并行化为 4 个 CPU 核。这可能会提高基准测试的性能,但它也有可能导致不准确的结果,因为并行执行可能会导致竞争条件或其他问题。

如果我们不确定基准测试是否可以安全地并行化,那么最好避免并行化基准测试。我们可以通过设置


如何看待go语言泛型的最新设计?

Go 由于不支持泛型而臭名昭著,但最近,泛型已接近成为现实。Go 团队实施了一个看起来比较稳定的设计草案,并且正以源到源翻译器原型的形式获得关注。本文讲述的是泛型的最新设计,以及如何自己尝试泛型。

例子

假设你要创建一个先进先出堆栈。没有泛型,你可能会这样实现:

typeStack[]interface{}func(sStack)Peek()interface{}{

returns[len(s)-1]

func(s*Stack)Pop(){

len(*s)-1]

func(s*Stack)Push(valueinterface{}){

append(*s,value)

但是,这里存在一个问题:每当你 Peek 项时,都必须使用类型断言将其从 interface{} 转换为你需要的类型。如果你的堆栈是 *MyObject 的堆栈,则意味着很多 ().(*MyObject)这样的代码。这不仅让人眼花缭乱,而且还可能引发错误。比如忘记 * 怎么办?或者如果您输入错误的类型怎么办?(MyObject{})` 可以顺利编译,而且你可能不会发现到自己的错误,直到它影响到你的整个服务为止。

通常,使用 interface{} 是相对危险的。使用更多受限制的类型总是更安全,因为可以在编译时而不是运行时发现问题。

泛型通过允许类型具有类型参数来解决此问题:

typeStack(typeT)[]Tfunc(sStack(T))Peek()T{

returns[len(s)-1]

func(s*Stack(T))Pop(){

len(*s)-1]

func(s*Stack(T))Push(valueT){

append(*s,value)

这会向 Stack 添加一个类型参数,从而完全不需要 interface{}。现在,当你使用 Peek() 时,返回的值已经是原始类型,并且没有机会返回错误的值类型。这种方式更安全,更容易使用。(译注:就是看起来更丑陋,^-^)

此外,泛型代码通常更易于编译器优化,从而获得更好的性能(以二进制大小为代价)。如果我们对上面的非泛型代码和泛型代码进行基准测试,我们可以看到区别:

typeMyObjectstruct{

varsinkMyObjectfuncBenchmarkGo1(b*testing.B){

fori:=0;i<b.N;i++{

(MyObject{})

(MyObject{})

sink=().(MyObject)

funcBenchmarkGo2(b*testing.B){

fori:=0;i<b.N;i++{

varsStack(MyObject)

(MyObject{})

(MyObject{})

结果:

BenchmarkGo1BenchmarkGo1-7.0ns/op48B/op2allocs/opBenchmarkGo2BenchmarkGo2-1.9ns/op24B/op2allocs/op

在这种情况下,我们分配更少的内存,同时泛型的速度是非泛型的两倍。

合约(Contracts)

上面的堆栈示例适用于任何类型。但是,在许多情况下,你需要编写仅适用于具有某些特征的类型的代码。例如,你可能希望堆栈要求类型实现 String() 函数

go的xrd测不出峰怎么办

这是因为go在xrd中不能准确的定位到晶界所致,可以用x射线衍射测试法来分析。

免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。

标签: GO

“GO-在-中打造准确的基准测试的最佳实践 (go在中间的英语单词有哪些)” 的相关文章

GO从0到1实战微服务版抢红包系统 (从0到1英语怎么说)

GO从0到1实战微服务版抢红包系统 (从0到1英语怎么说)

Go,又称为Golang,是由谷歌公司设计并开发的静态强类型、编译型和并发型语言,于2009年正式对外发布。它由Robert Griesemer、Rob Pike和Ken Thompson三位计算...