FieldByName 和 Field 十倍的性能差距让我对 FieldByName 的内部实现比较好奇,打开源代码一探究竟:
reflect/value.go
// FieldByName returns the struct field with the given name.
// It returns the zero Value if no field was found.
// It panics if v's Kind is not struct.
func (v Value) FieldByName(name string) Value {
v.mustBe(Struct)
if f, ok := v.typ.FieldByName(name); ok {
return v.FieldByIndex(f.Index)
}
return Value{}
}
reflect/type.go
func (t *rtype) FieldByName(name string) (StructField, bool) {
if t.Kind() != Struct {
panic("reflect: FieldByName of non-struct type")
}
tt := (*structType)(unsafe.Pointer(t))
return tt.FieldByName(name)
}
// FieldByName returns the struct field with the given name
// and a boolean to indicate if the field was found.
func (t *structType) FieldByName(name string) (f StructField, present bool) {
// Quick check for top-level name, or struct without embedded fields.
hasEmbeds := false
if name != "" {
for i := range t.fields {
tf := &t.fields[i]
if tf.name.name() == name {
return t.Field(i), true
}
if tf.embedded() {
hasEmbeds = true
}
}
}
if !hasEmbeds {
return
}
return t.FieldByNameFunc(func(s string) bool { return s == name })
}
而 (t *structType) FieldByName 中使用 for 循环,逐个字段查找,字段名匹配时返回。也就是说,在反射的内部,字段是按顺序存储的,因此按照下标访问查询效率为 O(1),而按照 Name 访问,则需要遍历所有字段,查询效率为 O(N)。结构体所包含的字段(包括方法)越多,那么两者之间的效率差距则越大。
4 如何提高性能
4.1 避免使用反射
4.2 缓存
在上面的例子中可以看到,FieldByName 相比于 Field 有一个数量级的性能劣化。那在实际的应用中,就要避免直接调用 FieldByName。我们可以利用字典将 Name 和 Index 的映射缓存起来。避免每次反复查找,耗费大量的时间。
我们利用缓存,优化下刚才的测试用例:
func BenchmarkReflect_FieldByNameCacheSet(b *testing.B) {
typ := reflect.TypeOf(Config{})
cache := make(map[string]int)
for i := 0; i < typ.NumField(); i++ {
cache[typ.Field(i).Name] = i
}
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.Field(cache["Name"]).SetString("name")
ins.Field(cache["IP"]).SetString("ip")
ins.Field(cache["URL"]).SetString("url")
ins.Field(cache["Timeout"]).SetString("timeout")
}
}
测试结果如下:
$ go test -bench="Set$" . -v
goos: darwin
goarch: amd64
pkg: example/hpg-reflect
BenchmarkSet-8 1000000000 0.303 ns/op
BenchmarkReflect_FieldSet-8 33429990 34.1 ns/op
BenchmarkReflect_FieldByNameSet-8 3612130 331 ns/op
BenchmarkReflect_FieldByNameCacheSet-8 14575906 78.2 ns/op
PASS
ok example/hpg-reflect 4.280s
消耗时间从原来的 10 倍,缩小到了 2 倍。
附 推荐与参考
在 这个项目中,也有好几处用到了反射。在 中,我们使用反射在服务端,利用接收到的二进制报文动态创建对象,例如利用反射实现函数的动态调用。在 中,我们使用反射,实现了结构体(struct)类型和数据库表名的映射,结构体字段和数据库字段的映射。同样利用反射动态创建对象的能力,将数据库中查询到的记录转换为 Go 语言中的对象。