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

Go Reflect 性能

鸟窝 2019-08-10 22:15:47 浏览 13,663 次

   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. Xvfb+YSlow+ShowSlow搭建前端性能测试框架 (阅读 55,342)
  2. 30分钟3300%性能提升――python+memcached网页优化小记 (阅读 13,581)
  3. 长连接(KeepAlive)在 http 连接中的性能影响 (阅读 8,581)
  4. 服务器性能测试工具推荐 (阅读 7,901)
  5. SQL vs NoSQL:数据库并发写入性能比拼 (阅读 7,881)
  6. WEB性能测试工具推荐 (阅读 6,942)
  7. 分析进程内存分配情况,解决程序性能问题 (阅读 6,681)
  8. 由12306.cn谈谈网站性能技术 (阅读 6,203)
  9. 从Go看,语言设计(一) (阅读 6,042)
  10. [调优] Squid 不同版本的性能对比 (阅读 5,462)