GO-在-中打造准确的基准测试的最佳实践 (go在中间的英语单词有哪些)
概述
在优化代码时,不应该对性能进行猜测。编写优化时,会有许多因素可能起作用,即使我们对结果有很强的看法,测试这些因素也几乎不会是一个坏主意。但是,编写基准测试并不简单。很容易编写不准确的基准测试,并且基于这些测试得出错误的假设。文章的目标是探讨导致不准确的四个常见和具体陷阱:
通用概念
在讨论这些陷阱之前,让我们简要回顾一下 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() } }
在这种情况下,我们可以在进入循环之前使用
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射线衍射测试法来分析。
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。