“反射很慢”到底慢多少、慢在哪里?这篇逐项量:读写字段、调用函数与方法、类型判断、比较、创建,并数每一项的分配次数,看看哪些写法可以把反射的成本压下来(缓存字段下标与方法)。
直接读 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)。只是“看一眼类型”的反射用法并不贵。
手写比较(字段逐个 ==,切片用循环) 中位 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 增加的压力。