一次errors的benchmark记录
本文代码由我和AI携手编写,如有问题全是我菜逼导致的(
package errorsbench
import (
"errors"
"fmt"
"os"
"testing"
)
// makeErrorChain 构造指定深度的错误链
// depth=1: 只有底层错误
// depth=3: 底层错误 + 2层包装
// depth=5: 底层错误 + 4层包装
func makeErrorChain(depth int) error {
base := &os.PathError{
Op: "open",
Path: "/tmp/test.txt",
Err: os.ErrNotExist,
}
err := error(base)
for i := 0; i < depth-1; i++ {
err = fmt.Errorf("layer %d: %w", i, err)
}
return err
}
// benchmarkOldAs 测试标准库 errors.As(反射版)
func benchmarkOldAs(b *testing.B, chainDepth int) {
err := makeErrorChain(chainDepth)
b.ResetTimer()
for i := 0; i < b.N; i++ {
var target *os.PathError
if errors.As(err, &target) {
_ = target.Path // 防止编译器优化掉
}
}
}
// benchmarkNewAsType 测试 errors.AsType(泛型版)
func benchmarkNewAsType(b *testing.B, chainDepth int) {
err := makeErrorChain(chainDepth)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if target, ok := errors.AsType[*os.PathError](err); ok {
_ = target.Path // 防止编译器优化掉
}
}
}
// 不同深度的对比测试
func BenchmarkAs_Depth1(b *testing.B) { benchmarkOldAs(b, 1) }
func BenchmarkAsType_Depth1(b *testing.B) { benchmarkNewAsType(b, 1) }
func BenchmarkAs_Depth3(b *testing.B) { benchmarkOldAs(b, 3) }
func BenchmarkAsType_Depth3(b *testing.B) { benchmarkNewAsType(b, 3) }
func BenchmarkAs_Depth5(b *testing.B) { benchmarkOldAs(b, 5) }
func BenchmarkAsType_Depth5(b *testing.B) { benchmarkNewAsType(b, 5) }
// 测试内联效果:小函数包装
func checkWithAs(err error) bool {
var target *os.PathError
return errors.As(err, &target)
}
func checkWithAsType(err error) bool {
_, ok := errors.AsType[*os.PathError](err)
return ok
}
func BenchmarkAs_Inlined(b *testing.B) {
err := makeErrorChain(3)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = checkWithAs(err)
}
}
func BenchmarkAsType_Inlined(b *testing.B) {
err := makeErrorChain(3)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = checkWithAsType(err)
}
}
// 正确性验证
func TestCorrectness(t *testing.T) {
err := makeErrorChain(3)
// 旧 API
var oldTarget *os.PathError
oldOk := errors.As(err, &oldTarget)
// 新 API
newTarget, newOk := errors.AsType[*os.PathError](err)
if oldOk != newOk {
t.Fatalf("ok mismatch: old=%v, new=%v", oldOk, newOk)
}
if oldOk && oldTarget.Path != newTarget.Path {
t.Fatalf("path mismatch: old=%v, new=%v", oldTarget.Path, newTarget.Path)
}
t.Logf("Both APIs return: ok=%v, path=%s", oldOk, oldTarget.Path)
}
// 测试指令:go test -bench=Benchmark -benchmem -v 结果(32核CPU)
PS D:\GoLand\errorsbench> go test -bench=Benchmark -benchmem -v
=== RUN TestCorrectness
main_test.go:104: Both APIs return: ok=true, path=/tmp/test.txt
--- PASS: TestCorrectness (0.00s)
goos: windows
goarch: amd64
pkg: errorsbench
cpu: 13th Gen Intel(R) Core(TM) i9-13900HX
BenchmarkAs_Depth1
BenchmarkAs_Depth1-32 14721577 76.50 ns/op 8 B/op 1 allocs/op
BenchmarkAsType_Depth1
BenchmarkAsType_Depth1-32 451059481 2.728 ns/op 0 B/op 0 allocs/op
BenchmarkAs_Depth3
BenchmarkAs_Depth3-32 10217434 115.4 ns/op 8 B/op 1 allocs/op
BenchmarkAsType_Depth3
BenchmarkAsType_Depth3-32 107737664 11.46 ns/op 0 B/op 0 allocs/op
BenchmarkAs_Depth5
BenchmarkAs_Depth5-32 7936659 147.8 ns/op 8 B/op 1 allocs/op
BenchmarkAsType_Depth5
BenchmarkAsType_Depth5-32 70322662 17.79 ns/op 0 B/op 0 allocs/op
BenchmarkAs_Inlined
BenchmarkAs_Inlined-32 10316660 113.7 ns/op 8 B/op 1 allocs/op
BenchmarkAsType_Inlined
BenchmarkAsType_Inlined-32 100000000 11.07 ns/op 0 B/op 0 allocs/op
PASS
ok errorsbench 12.265s| 场景 | errors.As | errors.AsType | 加速比 | 内存差异 |
|---|---|---|---|---|
| Depth1 | 76.50 ns | 2.728 ns | 28x | 8B vs 0B |
| Depth3 | 115.4 ns | 11.46 ns | 10x | 8B vs 0B |
| Depth5 | 147.8 ns | 17.79 ns | 8.3x | 8B vs 0B |
| Inlined | 113.7 ns | 11.07 ns | 10x | 8B vs 0B |
核心机制:testing.B 的工作方式
func BenchmarkXxx(b *testing.B) {
// 准备代码(不计时)
data := prepare()
b.ResetTimer() // ← 重置计时器,从这里开始算时间
for i := 0; i < b.N; i++ { // ← b.N 是框架自动调整的
// 被测代码(循环执行 b.N 次)
operation(data)
}
}b.N 是什么?
- 不是固定的,是 Go 测试框架自动调整的
- 框架会先跑几次,估算时间,然后调整
b.N让总运行时间 ≈ 1 秒 - 你的结果里
b.N就是实际执行次数(如451059481次)
逐函数拆解
数据准备:makeErrorChain
func makeErrorChain(depth int) error {
base := &os.PathError{...} // 底层错误
err := error(base)
for i := 0; i < depth-1; i++ {
err = fmt.Errorf("layer %d: %w", i, err) // 用 %w 包装
}
return err
}depth=1:PathError(无包装)depth=3:fmt.Errorf→fmt.Errorf→PathError(两层包装)depth=5: 四层包装 + 底层
测旧 API:benchmarkOldAs
func benchmarkOldAs(b *testing.B, chainDepth int) {
err := makeErrorChain(chainDepth) // 准备数据(不计时)
b.ResetTimer() // ← 从这里开始计时
for i := 0; i < b.N; i++ {
var target *os.PathError
if errors.As(err, &target) { // ← 反射遍历错误链
_ = target.Path // ← 防止编译器优化掉整个 if
}
}
}发生了什么:
errors.As内部用 reflect 遍历错误链- 每层都查类型表、判断可赋值性
- 找到匹配后通过指针写回
target - 每次都有反射对象分配(你的结果:8B/1allocs)
测新 API:benchmarkNewAsType
func benchmarkNewAsType(b *testing.B, chainDepth int) {
err := makeErrorChain(chainDepth)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if target, ok := errors.AsType[*os.PathError](err); ok { // ← 泛型错误链
_ = target.Path
}
}
}发生了什么:
errors.AsType[*os.PathError]编译期确定类型- 内部直接
err.(*os.PathError)类型断言 - 无反射、无动态遍历、无内存分配
- 可被内联(你的
Inlined测试验证了这点)(内联:把函数调用直接替换成函数体代码,消除调用开销)
func checkWithAsType(err error) bool {
_, ok := errors.AsType[*os.PathError](err)
return ok
}
// _, ok := errors.AsType[*os.PathError](err)
// 等价于直接写
// _, ok := err.(*os.PathError) // 直接展开,无函数调用func checkWithAs(err error) bool {
var target *os.PathError
return errors.As(err, &target) // 反射调用,要去判断类型,无法直接断言类型展开调用
}内联测试:checkWithAs vs checkWithAsType
func checkWithAs(err error) bool {
var target *os.PathError
return errors.As(err, &target)
}
func BenchmarkAs_Inlined(b *testing.B) {
err := makeErrorChain(3)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = checkWithAs(err) // ← 测函数调用 + 内部逻辑
}
}设计意图:
- 把小函数抽出来,看编译器能否内联
checkWithAsType能被内联 → 性能 ≈ 直接写循环checkWithAs因反射无法内联 → 有函数调用开销
正确性验证:TestCorrectness
func TestCorrectness(t *testing.T) {
err := makeErrorChain(3)
oldOk := errors.As(err, &oldTarget)
newOk, newTarget := errors.AsType[*os.PathError](err)
// 确保两个 API 返回相同结果
if oldOk != newOk || oldTarget.Path != newTarget.Path {
t.Fatal("mismatch")
}
}为什么需要:
- Benchmark 只测性能,不测正确性
- 防止"跑得快但结果是错的"情况
执行流程总结
go test -bench=Benchmark -benchmem -v
↓
发现所有 BenchmarkXxx 函数
↓
对每个函数:
1. 先跑 1 次,估算时间(用于估算1s用时的次数)
2. 调整 b.N(如从 1 调到 451059481,用时接近1s)
3. 正式跑 b.N 次,计时
4. 计算:总时间 / b.N = ns/op
5. 统计内存分配(-benchmem)
↓
输出结果行:
BenchmarkAsType_Depth1-32 451059481 2.728 ns/op 0 B/op 0 allocs/op
↑ ↑ ↑ ↑ ↑
函数名-CPU数 执行次数 每次耗时 内存/次 分配次数/次关键设计技巧
| 技巧 | 代码体现 | 目的 |
|---|---|---|
b.ResetTimer() | 准备数据后调用 | 排除构造时间 |
_ = target.Path | 使用返回值 | 防止编译器优化掉整个操作 |
b.N 循环 | for i := 0; i < b.N; i++ | 让框架决定跑多少次 |
| 辅助函数 | benchmarkOldAs(b, depth) | 复用逻辑,参数化场景 |
| 小函数包装 | checkWithAs | 测试内联效果 |
RoLingG | 博客
评论(0)