一. 反射的性能分析
标准库 reflect 为 Go 语言提供了运行时动态获取对象的类型和值以及动态创建对象的能力 反射会增加额外的代码指令,对性能肯定会产生影响的
创建对象
通过反射创建对象的耗时约为 new 的 1.5 倍,相差不是特别大
func BenchmarkNew ( b * testing. B) {
var config * Config
for i := 0 ; i < b. N; i++ {
config = new ( Config)
}
_ = config
}
func BenchmarkReflectNew ( b * testing. B) {
var config * Config
typ := reflect. TypeOf ( Config{ } )
b. ResetTimer ( )
for i := 0 ; i < b. N; i++ {
config, _ = reflect. New ( typ) . Interface ( ) . ( * Config)
}
_ = config
}
修改字段的值
通过反射获取结构体的字段有两种方式,一种是 FieldByName,另一种是 Field(按照下标) 对象已经提前创建好,测试的均为给字段赋值所消耗的时间。
func BenchmarkSet ( b * testing. B) {
config := new ( Config)
b. ResetTimer ( )
for i := 0 ; i < b. N; i++ {
config. Name = "name"
config. IP = "ip"
config. URL = "url"
config. Timeout = "timeout"
}
}
func BenchmarkReflect_FieldSet ( 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" )
}
}
func BenchmarkReflect_FieldByNameSet ( 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" )
}
}
普通的赋值操作,每次耗时约为 0.3 ns,通过下标找到对应的字段再赋值,每次耗时约为 30 ns,通过名称找到对应字段再赋值,每次耗时约为 300 ns。 总结一下,对于一个普通的拥有 4 个字段的结构体 Config 来说,使用反射给每个字段赋值,相比直接赋值,性能劣化约 100 - 1000 倍。其中,FieldByName 的性能相比 Field 劣化 10 倍
FieldByName 和 Field 性能差距
FieldByName 中使用 for 循环,逐个字段查找,字段名匹配时返回。也就是说,在反射的内部,字段是按顺序存储的,因此按照下标访问查询效率为 O(1),而按照 Name 访问,则需要遍历所有字段,查询效率为 O(N)。结构体所包含的字段(包括方法)越多,那么两者之间的效率差距则越大
二. 如何提高性能
避免使用反射: 使用反射赋值,效率非常低下,如果有替代方案,尽可能避免使用反射,特别是会被反复调用的热点代码。例如 Go 语言自带的json 标准库Marshal 和 Unmarshal 序列化和反序列化是利用反射实现的。可选的替代方案有 easyjson,在大部分场景下,相比标准库,有 5 倍左右的性能提升 缓存: 上面的例子中可以看到,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" )
}
}