2.4 反射性能

Kesa...大约 5 分钟golang

1. 反射的用途

标准库 reflectopen in new window 为 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 修改字段的值

获取字段有两种方式:

  1. 通过名称获取,Value.Elem().FieldByName(field_name)
  2. 通过索引获取,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 语言自带的 jsonMarshalUnmarshal 方法,因为标准库中的 json 序列化和反序列化是利用反射实现的。(可选的替代方案有 easyjsonopen in new window,在大部分场景下,相比标准库,有 5 倍左右的性能提升。)

4.2 缓存

FieldByName 相比于 Field 有一个数量级的性能劣化。那在实际的应用中,就要避免直接调用 FieldByName

可以利用哈希表将 NameIndex 的映射缓存起来。避免每次反复查找,耗费大量的时间。

使用哈希表优化后再次测试之前的例子:


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

可以看出时间减少为原来的约六分之一。

Reference

  1. https://geektutu.com/post/hpg-reflect.htmlopen in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2