当前位置:首页 > 文章列表 > Golang > Go教程 > B.Loop 基准测试怎样比较不同缓冲区大小

B.Loop 基准测试怎样比较不同缓冲区大小

来源:17golang原创 2026-10-09 09:18:21 0浏览 收藏

比较不同缓冲区大小时,最稳妥的结构是:用 b.Run 为每个大小创建独立子基准,用 for b.Loop() 驱动同一段复制操作,把缓冲区分配放在循环外复用,再用 b.SetBytes 和 b.ReportAllocs 同时观察 MB/s、ns/op、B/op 与 allocs/op。

如果测的是 io.CopyBuffer,还必须避免源实现 io.WriterTo 或目标实现 io.ReaderFrom。官方文档明确说明,只要任一快速路径存在,传入的缓冲区就不会用于复制;此时即使子基准名字不同,也没有真正比较缓冲区大小。

testing.B.Loop 官方文档:https://pkg.go.dev/testing#B.Loop

io.CopyBuffer 官方文档:https://pkg.go.dev/io#CopyBuffer

先固定负载:缓冲区大小之外的条件必须相同

我在看缓冲区基准时,最先检查的不是候选值,而是每个子基准处理的数据是否一样。小缓冲区如果只复制小文件,大缓冲区却复制大文件,最后得到的并不是选型依据,而是两种负载的混合结果。

下面把每次操作的输入固定为 1 MiB,并选择四个常见候选:1 KiB、4 KiB、32 KiB 和 128 KiB。1 MiB 明显大于最大缓冲区,可以让每个候选都经历多次读写;同时所有子基准使用同一份只读 payload,避免数据内容变化。

比较项固定方式为什么重要
输入大小每次 1 MiB保证吞吐分母一致
输入内容共享同一 payload排除内容差异
输出目标统一写入 io.Discard不把磁盘与网络波动混入结果
缓冲区生命周期每个子基准只分配一次比较复用后的搬运效率
计时范围只覆盖 B.Loop 循环体排除候选表、payload 与缓冲区初始化
固定 1 MiB 负载与四种缓冲区候选之间的数据结构静态关系图
图1:基准输入矩阵静态结构图。四个子基准共享同一 1 MiB payload 和输出边界,只让可复用 buffer 的容量变化;这不是运行截图。

完整基准:用 B.Run 隔离候选,用 B.Loop 驱动测量

下面的测试文件可以直接放进包内的 *_test.go。外层循环只负责枚举候选,b.Run 为每个大小建立独立的统计结果;每个子基准内部再使用自己的 b.Loop。

package bufferbench

import (
    "bytes"
    "fmt"
    "io"
    "testing"
)

// readerOnly 只暴露 io.Reader,避免 bytes.Reader 的 WriterTo 快速路径。
type readerOnly struct {
    io.Reader
}

// writerOnly 只暴露 io.Writer,避免目标端的 ReaderFrom 快速路径。
type writerOnly struct {
    io.Writer
}

func BenchmarkCopyBuffer(b *testing.B) {
    // 生成固定 1 MiB 输入;父基准只负责组织子基准,不测量自身。
    payload := bytes.Repeat([]byte("0123456789abcdef"), 64>10)
        b.Run(name, func(b *testing.B) {
            // 每个候选复用独立 Reader、Writer 视图和缓冲区。
            src := bytes.NewReader(payload)
            srcView := readerOnly{Reader: src}
            dstView := writerOnly{Writer: io.Discard}
            buf := make([]byte, size)

            // 记录每次操作处理的字节数,并启用内存分配指标。
            b.SetBytes(int64(len(payload)))
            b.ReportAllocs()

            for b.Loop() {
                // 只重置读取位置,不重新分配 payload 或 buffer。
                src.Reset(payload)
                if _, err := io.CopyBuffer(dstView, srcView, buf); err != nil {
                    b.Fatal(err)
                }
            }
        })
    }
}

B.Loop 第一次调用会重置计时器,返回 false 时会停止计时器,因此 payload、候选表、Reader 和 buffer 的创建不计入结果。这样测到的是“复用既有缓冲区搬运固定数据”的成本,适合连接池、对象池或长生命周期流处理器的选型。

为什么要隐藏 WriterTo 和 ReaderFrom

io.CopyBuffer 并不保证总会使用你传入的 buf。如果源实现 WriterTo,复制会走源端优化;否则如果目标实现 ReaderFrom,会走目标端优化。只有两者都没有时,通用复制路径才会使用自定义缓冲区。

*bytes.Reader 本身实现了 WriteTo。如果直接把它传给 CopyBuffer,四个子基准可能都绕过 buf。示例中的 readerOnly 与 writerOnly 通过接口收窄,只暴露基础的 Read 和 Write 方法,保证比较对象确实是 1 KiB、4 KiB、32 KiB、128 KiB 四个缓冲区。

B.Run 子基准、B.Loop、readerOnly、CopyBuffer 和指标报告之间的静态调用结构图
图2:测量边界静态结构图。候选大小由 B.Run 隔离,B.Loop 内部连接 readerOnly、可复用 buffer、CopyBuffer 与 writerOnly,SetBytes 和 ReportAllocs 位于指标边界;这不是运行截图。

这个处理也揭示了一个重要取舍:如果生产代码实际依赖 WriterTo 或 ReaderFrom 快速路径,那么“强制通用缓冲区路径”的结果并不能代表真实生产性能。此时应分成两组基准:一组测真实类型与真实快速路径,另一组才专门研究通用复制路径的 buffer 大小。

运行方式:重复测量,不要只看一次最快值

建议固定机器负载后运行多轮,并保留内存指标:

# 只运行 CopyBuffer 基准,固定每轮测量时间,并重复 5 次观察波动。
go test -run='^$' -bench='BenchmarkCopyBuffer' -benchmem -benchtime=1s -count=5

每个子基准会形成独立名称,例如 BenchmarkCopyBuffer/1KiB。判断时不要只挑某一轮最低的 ns/op,而应同时看:

  • MB/s:相同 payload 下越高,单位时间搬运的数据越多。
  • ns/op:完成一次 1 MiB 复制所需时间,越低越好。
  • B/op:每次操作分配的字节数。缓冲区放在循环外后,这一项应主要反映复制路径自身。
  • allocs/op:每次操作的分配次数,用来识别某个候选是否触发额外分配。
  • 重复波动:如果某个大小偶尔第一、偶尔垫底,差异可能小于系统噪声。

b.SetBytes(int64(len(payload))) 告诉测试框架每次操作处理多少字节,因此输出会增加 MB/s。它不会改变循环次数,也不会自动判断哪个大小最好。

缓冲区并非越大越好:把单次速度放回并发预算

单个 goroutine 的最优值不一定是服务端的最优值。假设每个活跃请求都独占一个 128 KiB buffer,几千个并发请求会放大常驻内存和 GC 压力;32 KiB 即使在微基准中略慢,也可能因为内存占用更稳而更适合高并发。

我通常按下面的约束做选择,而不是直接接受“最大缓冲区最快”:

负载特征优先观察候选方向
大量小消息allocs/op、尾部浪费从 1 KiB 或 4 KiB 起测
持续大流MB/s、系统调用频率重点比较 32 KiB 与 128 KiB
高并发连接每连接占用 × 峰值并发避免只按单线程吞吐选最大值
对象池复用池命中、保留内存、尺寸分层考虑少量固定档位
真实网络或磁盘端到端延迟与阻塞增加集成基准,不只依赖内存复制

文章里的内存复制基准适合筛掉明显不合适的候选,却不能替代真实文件系统、TLS、网络带宽和连接并发下的端到端测试。推荐先用它缩小到两个候选,再把两个候选放进真实负载复测。

两种测量目标不要混在一起

上面的代码把 make([]byte, size) 放在 B.Loop 外,回答的是“复用缓冲区时哪个大小搬运更合适”。如果业务每次请求都会新建 buffer,那么分配成本本身就是负载的一部分,应该另写一个基准,把分配放进循环体,而不是悄悄修改现有基准。

func BenchmarkBufferAllocation(b *testing.B) {
    for _, size := range []int{1 >10), func(b *testing.B) {
            b.ReportAllocs()
            for b.Loop() {
                // 此基准只测每次新建缓冲区的成本,不与复制吞吐混合。
                buf := make([]byte, size)
                _ = buf
            }
        })
    }
}

拆开以后,结果含义清楚得多:一个基准评估复用后的复制效率,另一个基准评估按请求分配的成本。生产方案若使用 sync.Pool,还应单独测试池命中和并发争用,不能把单纯的 make 结果当成池化结论。

落地检查清单

  • 所有子基准使用相同 payload、目标 Writer 和错误处理。
  • payload 大于最大候选缓冲区,避免一次读写就完成。
  • 确认源和目标不会通过 WriterTo/ReaderFrom 绕过 buffer。
  • 缓冲区分配位置与真实生命周期一致:复用就放循环外,每次分配就另建基准。
  • 调用 SetBytes 报告吞吐,调用 ReportAllocs 报告分配。
  • 至少重复多轮,结合波动和内存预算选择,而不是只取一次最快值。
  • 用真实磁盘、网络、TLS 与峰值并发对最终两个候选复测。

常见问题

为什么四种缓冲区的结果几乎一样?

先检查 CopyBuffer 是否走了 WriterTo 或 ReaderFrom 快速路径;再检查 payload 是否太小,导致不同容量都只需一次读写。

缓冲区应该在 B.Loop 外还是里面创建?

取决于问题。比较复用缓冲区的吞吐时放在外面;评估每次请求都分配缓冲区的总成本时放在里面,并最好拆成独立基准。

只比较 ns/op 可以吗?

不够。固定数据量时还要看 MB/s,并结合 B/op、allocs/op 和多轮波动,避免用少量时间差换来过高的并发内存占用。

为什么使用 B.Run 而不是在一个 B.Loop 里轮换大小?

每个 b.Run 都有独立的计时和指标,结果名称也清晰。若在同一循环里轮换大小,缓存状态、分支和工作量会混在一个平均值中,无法知道哪种容量真正更合适。

这套方法的核心不是找一个对所有场景都成立的“最佳缓冲区”,而是让每个候选在相同边界下被测量,再把单次吞吐放回并发内存、真实 I/O 和生命周期约束中做决策。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
多模态模型输入图片过大时如何控制视觉令牌多模态模型输入图片过大时如何控制视觉令牌
上一篇
多模态模型输入图片过大时如何控制视觉令牌
Go 小对象专用分配优化为何能降低运行时开销
下一篇
Go 小对象专用分配优化为何能降低运行时开销
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    386次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    468次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    475次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    415次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    241次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码