bigywater.tech
Go · 39

reflect 的成本:反射调用与直接调用差多少

“反射很慢”到底慢多少、慢在哪里?这篇逐项量:读写字段、调用函数与方法、类型判断、比较、创建,并数每一项的分配次数,看看哪些写法可以把反射的成本压下来(缓存字段下标与方法)。

按名字 vs 按下标
FieldByName 29.61 ns,按下标 Field(3) 2.26 ns:查找名字占了绝大部分
62 倍
reflect.Value.Call 调函数(121.20 ns)与直接调用(1.96 ns)的差距
0 次
读字段与 TypeOf().Kind() 不分配内存;Call 要分配 2 到 3 次

字段访问

实测 · 本机执行输出
  直接读 t.D                              中位 1.90 ns/op(最小 1.89 / 最大 1.92)
  reflect Field(3).Int()(预先取好 Value)   中位 2.26 ns/op(最小 2.25 / 最大 2.35)
  reflect ValueOf(t).Elem().Field(3).Int() 中位 3.61 ns/op(最小 3.61 / 最大 3.63)
  reflect FieldByName("D").Int()       中位 29.61 ns/op(最小 29.56 / 最大 29.71)
  Type.FieldByName("D")(只查字段信息)        中位 26.48 ns/op(最小 26.40 / 最大 27.39)

反射读写字段本身只比直接访问慢一点(预先取好 Value、按下标:2–4 ns),昂贵的是按名字查找(约 30 ns,其中 26 ns 是在类型信息里查名字)。所以序列化库等会在第一次遇到某个类型时算好每个字段的下标并缓存,之后只按下标访问。这是对数字的解释,没有读具体库的代码。

写字段

实测 · 本机执行输出
  直接写 t.D = i                          中位 0.30 ns/op(最小 0.30 / 最大 0.30)
  reflect Field(3).SetInt(i)           中位 3.02 ns/op(最小 3.01 / 最大 3.09)

直接写那一项只有 0.30 ns,是因为编译器把循环里的重复存储合并了,接近空循环的下限,不能作为“写字段的真实成本”;反射写 SetInt 是 3.02 ns。

调用

实测 · 本机执行输出
  直接调用 add(1, 2)                       中位 1.96 ns/op(最小 1.95 / 最大 2.09)
  reflect Value.Call(fn, [1 2])(预先准备 args) 中位 121.20 ns/op(最小 120.24 / 最大 122.35)
  直接调用 t.Add(1, 2)                     中位 2.32 ns/op(最小 2.28 / 最大 2.42)
  reflect Method 预先取好 + Call           中位 140.11 ns/op(最小 139.31 / 最大 143.48)
  reflect MethodByName("Add") 每次都查 + Call 中位 335.39 ns/op(最小 330.40 / 最大 345.01)

反射调用要把参数装进 []reflect.Value、检查类型、间接调用,再把结果装回 []reflect.Value,所以比直接调用慢约 60 到 70 倍,并且每次分配 2 到 3 次;每次还做 MethodByName 就更贵(335 ns,6 次分配)。能预先取好 Method 就别每次查。

类型信息

实测 · 本机执行输出
  类型断言 x.(*T)                          中位 2.09 ns/op(最小 2.08 / 最大 2.10)
  reflect.TypeOf(x).Kind() == Pointer  中位 1.99 ns/op(最小 1.98 / 最大 2.12)

reflect.TypeOf(x).Kind() 只读类型字,没有分配,与类型断言同一量级(约 2 ns)。只是“看一眼类型”的反射用法并不贵。

DeepEqual

实测 · 本机执行输出
  手写比较(字段逐个 ==,切片用循环)                  中位 4.55 ns/op(最小 4.54 / 最大 4.55)
  reflect.DeepEqual(a1, a2)            中位 180.26 ns/op(最小 178.65 / 最大 184.80)

reflect.DeepEqual 递归比较任意类型,方便但慢(这个 6 字段结构体约 180 ns,手写 4.55 ns,约 40 倍),并且会分配。在热路径里比较已知类型时手写比较,或者用 Go 1.21 以后的 slices.Equal 等(本文没有测)。

创建

实测 · 本机执行输出
  new(T)                               中位 0.30 ns/op(最小 0.30 / 最大 0.30)
  reflect.New(rt).Interface().(*T)     中位 24.41 ns/op(最小 23.88 / 最大 24.84)

reflect.New 约 24 ns 并分配一次;new(T) 在这个微基准里被优化掉了(0.30 ns),不能比较。

分配

实测 · 本机执行输出
  reflect Field(3).Int()                          0 次
  reflect FieldByName("D").Int()                  0 次
  fn.Call(args)(函数,2 个参数)                         2 次
  m.Call(args)(方法)                                3 次
  reflect.MethodByName 每次查找 + Call                6 次
  reflect.DeepEqual(a1, a2)                       2 次
  reflect.TypeOf(x).Kind()                        0 次
  reflect.New(rt)                                 1 次

读字段、取类型信息不分配;Call 与 DeepEqual 会分配。反射的成本不只是时间,也包括这些给 GC 增加的压力。

分步演示

reflect 的成本
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。基准测试每个变体 7 轮取中位数(为了跑完不超时,总共约 3 分钟);分配次数来自 testing.AllocsPerRun(1000 次);被编译器优化掉的两项(直接写 t.D、new(T))已在文中标注
    • 没有验证:缓存字段下标后的序列化库的真实性能(见 encoding/json 一篇)、reflect.Value.Call 在不同参数个数与变参下的成本、泛型作为反射替代方案的对比、reflect.TypeFor、MakeSlice/Append 等其它创建与修改操作、amd64 上的数字
    京ICP备2026067502号-1京公网安备11010502063487号