讲师中心 微信公众号
AI工具推荐 视频效率加速

Go语言性能基准测试:正确使用testing.B避免误导性结果

雨瑶小哥_4675

雨瑶小哥_4675

发布时间:2025-11-30 14:17:01

|

1004人浏览过

|

来源于php中文网

原创

go语言性能基准测试:正确使用testing.b避免误导性结果

本文旨在深入探讨Go语言中`testing.B`基准测试的正确使用方法,解决因不当实践导致的性能数据偏差问题。核心在于强调被测代码必须在`b.N`循环内执行,并合理利用`b.ResetTimer()`和数据克隆机制,以确保基准测试结果的准确性和代表性,避免出现如“瞬间完成”或“零内存分配”等误导性数据。

在Go语言中,性能基准测试是评估代码效率、识别性能瓶颈的关键工具。通过go test -bench .命令,我们可以方便地运行基准测试并获取操作耗时、内存分配等指标。然而,如果不正确地使用testing.B对象,测试结果可能会产生极大的误导。

1. 常见问题:基准测试结果异常

考虑以下为冒泡排序、选择排序和插入排序实现的基准测试代码:

package child_sort

import (
    "math/rand"
    "testing"
    "time"
)

// 排序算法实现 (BubbleSort, SelectionSort, InsertionSort) 略
// ...

func generate(size int, min, max int) []int {
    rand.Seed(time.Now().UTC().UnixNano()) // 注意:在基准测试中频繁播种随机数可能影响性能
    var xs = make([]int, size, size)
    for i := range xs {
        xs[i] = min + rand.Intn(max-min)
    }
    return xs
}

func BenchmarkBubble(b *testing.B) {
    xs := generate(10000, -100, 100)
    SortBubble(xs) // 问题所在:只调用了一次
}

func BenchmarkSelection(b *testing.B) {
    xs := generate(10000, -100, 100)
    SortSelection(xs) // 问题所在:只调用了一次
}

func BenchmarkInsertion(b *testing.B) {
    xs := generate(10000, -100, 100)
    SortInsertion(xs) // 问题所在:只调用了一次
}

运行上述基准测试可能得到如下异常结果:

立即学习“go语言免费学习笔记(深入)”;

BenchmarkBubble        1    2258469081 ns/op      241664 B/op          1 allocs/op
BenchmarkSelection  1000000000           0.60 ns/op        0 B/op          0 allocs/op
BenchmarkInsertion         1    1180026827 ns/op      241664 B/op          1 allocs/op

其中,BenchmarkSelection的结果尤为突出:操作耗时极短(0.60 ns/op),且内存分配为零。这显然与预期不符,因为选择排序对于10000个元素的数组不可能在如此短的时间内完成,并且会涉及内存读写操作。

2. 深入理解testing.B与b.N

Go语言的基准测试框架通过b *testing.B参数提供了一系列控制方法。其中最核心的是b.N字段,它代表了基准测试函数应该执行的迭代次数。基准测试运行器会动态调整b.N的值,以确保测试在足够长的时间内运行,从而获得统计上稳定的结果。

Go语言(Golang)1.26.0
Go语言(Golang)1.26.0

Go语言(Golang)1.26.0版本官方下载,版本号 1.26.0,适合旧项目维护、兼容性测试和指定版本开发环境搭建。

下载

问题根源: 上述示例代码的根本问题在于,排序函数(如SortSelection(xs))在每个基准测试函数中只被调用了一次。b.N的值虽然会被框架调整,但它并没有被用来重复执行被测代码。因此,实际测量的要么是单次执行的耗时(对于耗时较长的操作),要么是由于被测代码过于简单或输入数据太小,导致Go编译器或运行时进行了激进的优化,使得实际操作的耗时变得微乎其微,甚至被优化掉。对于BenchmarkSelection的0.60 ns/op和0 B/op,很可能是因为输入数组xs在SortSelection(xs)之后没有被使用,或者数组足够小,编译器完全优化掉了排序过程,或者其耗时被计入了初始化或计时器启动的开销中,而实际排序操作被忽略。

3. 正确编写Go语言基准测试

要获得准确的基准测试结果,必须将被测代码放置在一个循环中,并以b.N作为循环次数。此外,还需要考虑以下两个关键点:

  1. 排除设置时间: 任何不属于被测操作本身的初始化或数据准备工作,都应该在计时器启动之前完成。b.ResetTimer()方法用于重置计时器,确保只测量核心操作的性能。
  2. 数据隔离: 如果被测函数会修改其输入数据(例如排序函数),那么在每次循环迭代中都应该提供一份新鲜的、未被修改的输入数据副本。否则,后续迭代将对一个已经排序或部分修改的数据进行操作,导致结果失真。

以下是修正后的基准测试示例:

package child_sort

import (
    "math/rand"
    "testing"
    "time"
)

// 排序算法实现 (BubbleSort, SelectionSort, InsertionSort)
func SortBubble(xs []int) {
    for i := range xs {
        swapped := false
        for j := 1; j < len(xs)-i; j++ {
            if xs[j-1] > xs[j] {
                xs[j-1], xs[j] = xs[j], xs[j-1]
                swapped = true
            }
        }
        if !swapped {
            break
        }
    }
}

func SortSelection(xs []int) {
    for i := range xs {
        min_i := i
        for j := i + 1; j < len(xs); j++ {
            if xs[j] < xs[min_i] {
                min_i = j
            }
        }
        if min_i != i {
            xs[i], xs[min_i] = xs[min_i], xs[i]
        }
    }
}

func SortInsertion(xs []int) {
    for i := 1; i < len(xs); i++ {
        for j := i; j > 0; j-- {
            if xs[j] < xs[j-1] {
                xs[j], xs[j-1] = xs[j-1], xs[j]
            }
        }
    }
}

// 辅助函数:生成随机整数切片
func generate(size int, min, max int) []int {
    // 更好的做法是在测试开始时只播种一次,或者在Benchmark函数外部播种
    // rand.Seed(time.Now().UTC().UnixNano())
    // 考虑到多次调用generate可能导致重复播种,这里可以简化或将播种逻辑移出
    // 为确保每次测试数据略有不同,可以考虑使用固定的种子或在包级别初始化一次
    r := rand.New(rand.NewSource(time.Now().UnixNano())) // 使用局部随机数生成器
    xs := make([]int, size)
    for i := range xs {
        xs[i] = r.Intn(max-min) + min
    }
    return xs
}

func BenchmarkBubbleCorrected(b *testing.B) {
    // 1. 数据准备 (在计时器重置前完成)
    originalXs := generate(10000, -100, 100)

    b.ResetTimer() // 2. 重置计时器,排除数据准备时间

    // 3. 在 b.N 循环中执行被测代码
    for i := 0; i < b.N; i++ {
        // 4. 为每次迭代创建数据副本,确保排序操作总是在原始的、未排序的数据上进行
        xs := make([]int, len(originalXs))
        copy(xs, originalXs)
        SortBubble(xs)
    }
}

func BenchmarkSelectionCorrected(b *testing.B) {
    originalXs := generate(10000, -100, 100)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        xs := make([]int, len(originalXs))
        copy(xs, originalXs)
        SortSelection(xs)
    }
}

func BenchmarkInsertionCorrected(b *testing.B) {
    originalXs := generate(10000, -100, 100)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        xs := make([]int, len(originalXs))
        copy(xs, originalXs)
        SortInsertion(xs)
    }
}

运行修正后的基准测试,将会得到更合理且有意义的结果,例如:

BenchmarkBubbleCorrected-8        1000           1234567 ns/op      80000 B/op          1 allocs/op
BenchmarkSelectionCorrected-8     1000           234567 ns/op       80000 B/op          1 allocs/op
BenchmarkInsertionCorrected-8     1000           345678 ns/op       80000 B/op          1 allocs/op

(注:上述结果为示例,实际数值会根据机器性能和算法效率有所不同,但会比之前的异常结果更合理。)

现在,每个排序算法的ns/op(每次操作纳秒数)和B/op(每次操作字节数)都反映了实际的性能开销。80000 B/op是因为每次迭代都复制了一个包含10000个int(每个int 8字节)的切片,即10000 * 8 = 80000字节。

4. 更多基准测试注意事项

  • 避免副作用: 确保基准测试函数是幂等的,即多次运行不会产生不同的外部效果。
  • 防止编译器优化: 如果被测函数的返回值未被使用,Go编译器可能会将其优化掉。为了防止这种情况,可以将结果赋值给一个全局变量或使用_ = result来“使用”它。对于原地修改的排序算法,这不是一个常见问题,但对于返回新值的函数需要注意。
  • 并发基准测试: 对于需要测试并发性能的场景,可以使用b.RunParallel(func(pb *testing.PB))来并行运行基准测试。
  • 随机数生成: 在generate函数中,每次调用rand.Seed(time.Now().UTC().UnixNano())都会以当前时间作为种子,这可能导致在短时间内生成相同的序列,或者在基准测试中引入额外的开销。更好的做法是在包初始化时只播种一次,或者为每个Benchmark函数使用一个独立的rand.New(rand.NewSource(...))实例。
  • 输入数据规模: 调整输入数据的规模(如切片大小)可以帮助你理解算法在不同负载下的性能表现。

总结

Go语言的基准测试是一个强大的性能分析工具,但其有效性严重依赖于正确的实现方式。核心原则是将被测代码置于b.N循环内,并在循环外部处理一次性设置,同时利用b.ResetTimer()和数据克隆机制来隔离计时和确保每次迭代的输入数据一致性。遵循这些最佳实践,可以获得准确、可靠的性能数据,从而为代码优化提供坚实的基础。

相关文章

数码产品性能查询
数码产品性能查询

该软件包括了市面上所有手机CPU,手机跑分情况,电脑CPU,电脑产品信息等等,方便需要大家查阅数码产品最新情况,了解产品特性,能够进行对比选择最具性价比的商品。

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
切问学术

切问学术是一款AI论文写作工具,复旦大学NLP团队推出的AI学术智能体。

Seko
Seko Hot

一款AI视频创作工具,主要用于商汤科技推出的创编一体的AI短视频创作Agent,适合需要提升相关任务效率的用户。

二狗PPT
二狗PPT Hot

一款AI演示文稿工具,主要用于专为中式职场打造的AI PPT生成工具,适合需要提升相关任务效率的用户。

PixTV
PixTV Hot

PixTV是一款面向AIGC内容创作的AI视频生成工具。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

火山引擎

火山引擎是一款面向企业的云计算与AI服务平台。

蛙蛙写作

一款AI论文写作工具,主要用于超级AI智能写作助手,适合需要提升相关任务效率的用户。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

相关专题

更多
全局变量怎么定义
全局变量怎么定义

本专题整合了全局变量相关内容,阅读专题下面的文章了解更多详细内容。

4525

2025.09.18

python 全局变量
python 全局变量

本专题整合了python中全局变量定义相关教程,阅读专题下面的文章了解更多详细内容。

1870

2025.09.18

c++ 全局变量
c++ 全局变量

本专题整合了c++全局变量的使用、定义、作用域等等内容,阅读专题下面的文章了解更多详细内容。

199

2026.03.17

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

6339

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

3065

2024.08.29

c++怎么把double转成int
c++怎么把double转成int

本专题整合了 c++ double相关教程,阅读专题下面的文章了解更多详细内容。

3968

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

2745

2025.08.29

Go中Type关键字的用法
Go中Type关键字的用法

Go中Type关键字的用法有定义新的类型别名或者创建新的结构体类型。本专题为大家提供Go相关的文章、下载、课程内容,供大家免费下载体验。

2869

2023.09.06

OpenJDK JVM参数怎么调优
OpenJDK JVM参数怎么调优

Java应用运行缓慢,如何调整JVM参数?本专题介绍OpenJDK JVM性能调优的基本流程,涵盖性能指标确认、运行数据采集、内存配置和垃圾回收分析,帮助你先定位瓶颈,再通过对照测试验证效果,避免直接套用参数而影响应用稳定性。

60

2026.10.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn