2.4 反射性能
1. 反射的用途
标准库 reflect 为 Go 语言提供了运行时动态获取对象的类型和值以及动态创建对象的能力。反射可以帮助抽象和简化代码,提高开发效率。
2. 反射如何简化代码
假设有一个配置类 Config,每个字段是一个配置项。
type Config struct {
Name string `json:"server-name"`
IP string `json:"server-ip"`
URL string `json:"server-url"`
Timeout string `json:"timeout"`
}
参数默认从json
中读取,若环境变量中有配置则优先读取,环境变量全部为大写字母和下划线,并以CONFIG_
开头,如CONFIG_TIMEOUT
。
可以使用switch
或者if-else
结构来进行处理,但是当字段数量较多时,若需要修改某个字段,将需要进行多处修改,使得出错的概率上升。
此时使用反射可以编写通用代码,减少重复代码:
func readConfig() *Config {
// read from xxx.json,省略
config := Config{}
typ := reflect.TypeOf(config)
value := reflect.Indirect(reflect.ValueOf(&config))
for i := 0; i < typ.NumField(); i++ {
f := typ.Field(i)
if v, ok := f.Tag.Lookup("json"); ok {
key := fmt.Sprintf("CONFIG_%s", strings.ReplaceAll(strings.ToUpper(v), "-", "_"))
if env, exist := os.LookupEnv(key); exist {
value.FieldByName(f.Name).Set(reflect.ValueOf(env))
}
}
}
return &config
}
func main() {
os.Setenv("CONFIG_SERVER_NAME", "global_server")
os.Setenv("CONFIG_SERVER_IP", "10.0.0.1")
os.Setenv("CONFIG_SERVER_URL", "geektutu.com")
c := readConfig()
fmt.Printf("%+v", c)
}
- 在运行时,利用反射获取到
Config
的每个字段的Tag
属性,拼接出对应的环境变量的名称。 - 查看该环境变量是否存在,如果存在,则将环境变量的值赋值给该字段。
3. 反射的性能
使用反射会增加代码指令,对性能是一定会有影响的,可以通过测试检查影响有多大。
3.1 创建对象
func BenchmarkGetObjUsingNew(b *testing.B) {
var res *Config
for i := 0; i < b.N; i++ {
res = new(Config)
}
_ = res
}
func BenchmarkGetObjUsingReflect(b *testing.B) {
var res *Config
t := reflect.TypeOf(Config{})
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, _ = reflect.New(t).Interface().(*Config)
}
_ = res
}
BenchmarkGetObjUsingNew-12 39839181 38.96 ns/op 64 B/op 1 allocs/op
BenchmarkGetObjUsingReflect-12 21852828 51.67 ns/op 64 B/op 1 allocs/op
可以看出通过反射创建对象的时间约为new
的1.5倍。
3.2 修改字段的值
获取字段有两种方式:
- 通过名称获取,
Value.Elem().FieldByName(field_name)
- 通过索引获取,
Value.Elem().Field(field_index)
func BenchmarkSet(b *testing.B) {
cfg := &Config{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
cfg.Name = "Name"
cfg.IP = "IP"
cfg.URL = "URL"
cfg.Timeout = "Timeout"
}
}
func BenchmarkSetUsingReflectByFieldName(b *testing.B) {
typ := reflect.TypeOf(Config{})
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.FieldByName("Name").SetString("Name")
ins.FieldByName("IP").SetString("IP")
ins.FieldByName("URL").SetString("URL")
ins.FieldByName("Timeout").SetString("Timeout")
}
}
func BenchmarkSetUsingReflectByFieldIdx(b *testing.B) {
typ := reflect.TypeOf(Config{})
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.Field(0).SetString("Name")
ins.Field(1).SetString("IP")
ins.Field(2).SetString("URL")
ins.Field(3).SetString("Timeout")
}
}
BenchmarkSet-12 1000000000 0.2504 ns/op 0 B/op 0 allocs/op
BenchmarkSetUsingReflectByFieldName-12 4147510 291.7 ns/op 32 B/op 4 allocs/op
BenchmarkSetUsingReflectByFieldIdx-12 79601461 17.37 ns/op 0 B/op 0 allocs/op
可以看出使用反射给每个字段赋值,相比直接赋值,性能劣化约 100 - 1000 倍。其中,FieldByName
的性能相比 Field
劣化 10 倍。
3.3 FieldByName 和 Field
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 })
}
调用链如下:
(v Value) FieldByName -> (t *rtype) FieldByName -> (t *structType) FieldByName
(t *structType) FieldByName
中使用 for 循环,逐个字段查找,字段名匹配时返回。
在反射的内部,字段是按顺序存储的,因此按照下标访问查询效率为 O(1)
按照 Name
访问,则需要遍历所有字段,查询效率为 O(N)。结构体所包含的字段(包括方法)越多,那么两者之间的效率差距则越大。
4. 如何提高性能
4.1 避免使用反射
使用反射赋值,效率非常低下,如果有替代方案,尽可能避免使用反射,特别是会被反复调用的热点代码。
例如 RPC 协议中,需要对结构体进行序列化和反序列化,这个时候避免使用 Go 语言自带的 json
的 Marshal
和 Unmarshal
方法,因为标准库中的 json 序列化和反序列化是利用反射实现的。(可选的替代方案有 easyjson,在大部分场景下,相比标准库,有 5 倍左右的性能提升。)
4.2 缓存
FieldByName
相比于 Field
有一个数量级的性能劣化。那在实际的应用中,就要避免直接调用 FieldByName
。
可以利用哈希表将 Name
和 Index
的映射缓存起来。避免每次反复查找,耗费大量的时间。
使用哈希表优化后再次测试之前的例子:
func BenchmarkSetUsingReflectByFieldName(b *testing.B) {
typ := reflect.TypeOf(Config{})
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.FieldByName("Name").SetString("Name")
ins.FieldByName("IP").SetString("IP")
ins.FieldByName("URL").SetString("URL")
ins.FieldByName("Timeout").SetString("Timeout")
}
}
func BenchmarkSetUsingReflectByFieldNameWithCache(b *testing.B) {
typ := reflect.TypeOf(Config{})
fCache := make(map[string]int, typ.NumField())
for i := 0; i < typ.NumField(); i++ {
fCache[typ.Field(i).Name] = i
}
ins := reflect.New(typ).Elem()
b.ResetTimer()
for i := 0; i < b.N; i++ {
ins.FieldByName("Name").SetString("Name")
ins.FieldByName("IP").SetString("IP")
ins.FieldByName("URL").SetString("URL")
ins.FieldByName("Timeout").SetString("Timeout")
}
}
BenchmarkSetUsingReflectByFieldName-12 4135982 290.2 ns/op 32 B/op 4 allocs/op
BenchmarkSetUsingReflectByFieldNameWithCache-12 22643047 50.44 ns/op 0 B/op 0 allocs/op
可以看出时间减少为原来的约六分之一。