IT技术博客大学习 共学习 共进步

Go Reflect 性能

鸟窝 2019-08-10 22:15:47 累计浏览 13,825 次
本机暂存

   Go reflect包提供了运行时获取对象的类型和值的能力,它可以帮助我们实现代码的抽象和简化,实现动态的数据获取和方法调用, 提高开发效率和可读性, 也弥补Go在缺乏泛型的情况下对数据的统一处理能力。

   通过reflect,我们可以实现获取对象类型、对象字段、对象方法的能力,获取struct的tag信息,动态创建对象,对象是否实现特定的接口,对象的转换、对象值的获取和设置、Select分支动态调用等功能, 看起来功能不错,但是大家也都知道一点: 使用reflect是有性能代价的!

测试

   Java中的reflect的使用对性能也有影响, 但是和Java reflect不同, Java中不区分TypeValue类型的, 所以至少Java中我们可以预先讲相应的reflect对象缓存起来,减少反射对性能的影响, 但是Go没办法预先缓存reflect, 因为Type类型并不包含对象运行时的值,必须通过ValueOf和运行时实例对象才能获取Value对象。

   对象的反射生成和获取都会增加额外的代码指令, 并且也会涉及interface{}装箱/拆箱操作,中间还可能增加临时对象的生成,所以性能下降是肯定的,但是具体能下降多少呢,还是得数据来说话。

   当然,不同的reflect使用的姿势, 以及对象类型的不同,都会多多少少影响性能的测试数据,我们就以一个普通的struct类型为例:

package testimport ("reflect""testing")type Student struct {Name  stringAge   intClass stringScore int}func BenchmarkReflect_New(b *testing.B) {var s *Studentsv := 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 *Studentb.ResetTimer()for i := 0; i < b.N; i++ {s = new(Student)}_ = s}func BenchmarkReflect_Set(b *testing.B) {var s *Studentsv := 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 = 18s.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 *Studentb.ResetTimer()for i := 0; i < b.N; i++ {s = new(Student)s.Name = "Jerry"s.Age = 18s.Class = "20005"s.Score = 100}}

   测试结果:

BenchmarkReflect_New-4               20000000    70.0 ns/op      48 B/op       1 allocs/opBenchmarkDirect_New-4                30000000    45.6 ns/op      48 B/op       1 allocs/opBenchmarkReflect_Set-4               20000000    73.6 ns/op      48 B/op       1 allocs/opBenchmarkReflect_SetFieldByName-4     3000000   492 ns/op      80 B/op       5 allocs/opBenchmarkReflect_SetFieldByIndex-4   20000000   111 ns/op      48 B/op       1 allocs/opBenchmarkDirect_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_SetDirect_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等,都是通过反射处理数据的,这是为了能处理通用的类型。

https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L946
  ......      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())})          }  ......
https://github.com/jinzhu/gorm/blob/master/scope.go#L495
     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 + fieldIndexif 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()}}

   其它的一些编解码库也提供了这种避免使用反射的方法来提高性能。

顺带测一下"装箱/拆箱"操作带来的性能影响

func DirectInvoke(s *Student) {s.Name = "Jerry"s.Age = 18s.Class = "20005"s.Score = 100}func InterfaceInvoke(i interface{}) {s := i.(*Student)s.Name = "Jerry"s.Age = 18s.Class = "20005"s.Score = 100}func BenchmarkDirectInvoke(b *testing.B) {s := new(Student)for i := 0; i < b.N; i++ {DirectInvoke(s)}_ = s}func BenchmarkInterfaceInvoke(b *testing.B) {s := new(Student)for i := 0; i < b.N; i++ {InterfaceInvoke(s)}_ = s}

   测试结果:

BenchmarkDirectInvoke-4              300000000 5.60 ns/op       0 B/op       0 allocs/opBenchmarkInterfaceInvoke-4           200000000 6.64 ns/op       0 B/op       0 allocs/op

   可以看到将具体对象转换成 interface{}(以及反向操作)确实回带来一点点性能的影响,不过看起来影响倒不是很大。

建议继续学习

  1. 记录一个软中断问题 (累计阅读 16,882)
  2. 面向“接口”编程和面向“实现”编程 (累计阅读 13,846)
  3. 一种基于长连接的社交游戏服务器程序构架 (累计阅读 7,424)
  4. 关于Apache调优点滴 (累计阅读 6,563)
  5. 从Go看,语言设计(一) (累计阅读 6,101)
  6. MYSQL分页limit速度太慢优化方法 (累计阅读 5,786)
  7. 关于哈希map奇慢无比的原因定位 (累计阅读 4,846)
  8. 一线DBA总结:MySQL搭配XFS文件系统优势最大 (累计阅读 4,842)
  9. go-kit 入门(一) (累计阅读 4,661)
  10. 分布式存储Seaweedfs源码分析 (累计阅读 4,647)