Go Reflect 性能
source link: https://colobu.com/2019/01/29/go-reflect-performance/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Go reflect包 提供了运行时获取对象的类型和值的能力,它可以帮助我们实现代码的抽象和简化,实现动态的数据获取和方法调用, 提高开发效率和可读性, 也弥补Go在缺乏泛型的情况下对数据的统一处理能力。
通过reflect,我们可以实现获取对象类型、对象字段、对象方法的能力,获取struct的tag信息,动态创建对象,对象是否实现特定的接口,对象的转换、对象值的获取和设置、Select分支动态调用等功能, 看起来功能不错,但是大家也都知道一点: 使用reflect是有性能代价的!
测试
Java中的reflect的使用对性能也有影响, 但是和Java reflect不同, Java中不区分 Type
和 Value
类型的, 所以至少Java中我们可以预先讲相应的reflect对象缓存起来,减少反射对性能的影响, 但是Go没办法预先缓存reflect, 因为 Type
类型并不包含对象运行时的值,必须通过 ValueOf
和运行时实例对象才能获取 Value
对象。
对象的发射生成和获取都会增加额外的代码指令, 并且也会涉及 interface{}
装箱/拆箱操作,中间还可能增加临时对象的生成,所以性能下降是肯定的,但是具体能下降多少呢,还是得数据来说话。
当然,不同的reflect使用的姿势, 以及对象类型的不同,都会多多少少影响性能的测试数据,我们就以一个普通的struct类型为例:
package test import ( "reflect" "testing" ) type Student struct { Name string Age int Class string Score int } func BenchmarkReflect_New(b *testing.B) { var s *Student sv := reflect.TypeOf(Student{}) b.ResetTimer() for i :=0; i < b.N; i++ { sn := reflect.New(sv) s, _ = sn.Interface().(*Student) } _ = s } func BenchmarkDirect_New(b *testing.B) { var s *Student b.ResetTimer() for i :=0; i < b.N; i++ { s = new(Student) } _ = s } func BenchmarkReflect_Set(b *testing.B) { var s *Student sv := reflect.TypeOf(Student{}) b.ResetTimer() for i :=0; i < b.N; i++ { sn := reflect.New(sv) s = sn.Interface().(*Student) s.Name = "Jerry" s.Age =18 s.Class = "20005" s.Score =100 } } func BenchmarkReflect_SetFieldByName(b *testing.B) { sv := reflect.TypeOf(Student{}) b.ResetTimer() for i :=0; i < b.N; i++ { sn := reflect.New(sv).Elem() sn.FieldByName("Name").SetString("Jerry") sn.FieldByName("Age").SetInt(18) sn.FieldByName("Class").SetString("20005") sn.FieldByName("Score").SetInt(100) } } func BenchmarkReflect_SetFieldByIndex(b *testing.B) { sv := reflect.TypeOf(Student{}) b.ResetTimer() for i :=0; i < b.N; i++ { sn := reflect.New(sv).Elem() sn.Field(0).SetString("Jerry") sn.Field(1).SetInt(18) sn.Field(2).SetString("20005") sn.Field(3).SetInt(100) } } func BenchmarkDirect_Set(b *testing.B) { var s *Student b.ResetTimer() for i :=0; i < b.N; i++ { s = new(Student) s.Name = "Jerry" s.Age =18 s.Class = "20005" s.Score =100 } }
测试结果:
BenchmarkReflect_New-4 20000000 70.0 ns/op 48 B/op 1 allocs/op BenchmarkDirect_New-4 30000000 45.6 ns/op 48 B/op 1 allocs/op BenchmarkReflect_Set-4 20000000 73.6 ns/op 48 B/op 1 allocs/op BenchmarkReflect_SetFieldByName-4 3000000 492 ns/op 80 B/op 5 allocs/op BenchmarkReflect_SetFieldByIndex-4 20000000 111 ns/op 48 B/op 1 allocs/op BenchmarkDirect_Set-4 30000000 43.1 ns/op 48 B/op 1 allocs/op
测试结果
我们进行了两种功能的测试:
- 对象(struct)的创建
- 对象字段的赋值
对于对象的创建,通过反射生成对象需要 70
纳秒, 而直接new这个对象却只需要 45.6
纳秒, 性能差别还是很大的。
对于字段的赋值,一共四个测试用例:
-
Reflect_Set: 通过反射生成对象,并将这个对象转换成实际的对象,直接调用对象的字段进行赋值, 需要
73.6
纳秒 -
Reflect_SetFieldByName: 通过反射生成对象,通过
FieldByName
进行赋值, 需要492
纳秒 -
Reflect_SetFieldByIndex: 通过反射生成对象,通过
Field
进行赋值, 需要111
纳秒 -
Direct_Set: 直接调用对象的字段进行赋值, 只需要
43.1
纳秒
Reflect_Set
和 Direct_Set
性能的主要差别还是在于对象的生成,因为之后字段的赋值方法都是一样的,这也和对象创建的测试case的结果是一致的。
如果通过反射进行赋值,性能下降是很厉害的,耗时成倍的增长。比较有趣的是, FieldByName
方式赋值是 Field
方式赋值的好几倍, 原因在于 FieldByName
会有额外的循环进行字段的查找,虽然最终它还是调用 Field
进行赋值:
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{} } func (v Value) FieldByIndex(index []int) Value { if len(index) ==1 { return v.Field(index[0]) } v.mustBe(Struct) for i, x := range index { if i >0 { if v.Kind() == Ptr && v.typ.Elem().Kind() == Struct { if v.IsNil() { panic("reflect: indirection through nil pointer to embedded struct") } v = v.Elem() } } v = v.Field(x) } return v }
优化
从上面的测试结果看, 通过反射生成对象和字段赋值都会影响性能,但是通过反射的确确确实实能简化代码,为业务逻辑提供统一的代码, 比如标准库中json的编解码、rpc服务的注册和调用, 一些ORM框架比如gorm等,都是通过发射处理数据的,这是为了能处理通用的类型。
...... case reflect.String: v.SetString(string(s)) case reflect.Interface: if v.NumMethod() ==0 { v.Set(reflect.ValueOf(string(s))) } else { d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())}) } ......
for fieldIndex, field := range selectFields { if field.DBName == column { if field.Field.Kind() == reflect.Ptr { values[index] = field.Field.Addr().Interface() } else { reflectValue := reflect.New(reflect.PtrTo(field.Struct.Type)) reflectValue.Elem().Set(field.Field.Addr()) values[index] = reflectValue.Interface() resetFields[index] = field } selectedColumnsMap[column] = offset + fieldIndex if field.IsNormal { break } } }
在我们追求高性能的场景的时候,我们可能需要尽量避免发射的调用, 比如对json数据的unmarshal, easyjson就通过生成器的方式,避免使用发射。
func (v *Student) UnmarshalJSON(data []byte) error { r := jlexer.Lexer{Data: data} easyjson4a74e62dDecodeGitABCReflect(&r, v) return r.Error() } func (v *Student) UnmarshalEasyJSON(l *jlexer.Lexer) { easyjson4a74e62dDecodeGitABCReflect(l, v) } func easyjson4a74e62dDecodeGitABCReflect(in *jlexer.Lexer, out *Student) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { in.Consumed() } in.Skip() return } in.Delim('{') for !in.IsDelim('}') { key := in.UnsafeString() in.WantColon() if in.IsNull() { in.Skip() in.WantComma() continue } switch key { case "Name": out.Name = string(in.String()) case "Age": out.Age = int(in.Int()) case "Class": out.Class = string(in.String()) case "Score": out.Score = int(in.Int()) default: in.SkipRecursive() } in.WantComma() } in.Delim('}') if isTopLevel { in.Consumed() } }
其它的一些编解码库也提供了这种避免使用发射的方法来提高性能。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK