前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go Reflect 性能

Go Reflect 性能

作者头像
李海彬
发布2019-03-07 10:57:48
9460
发布2019-03-07 10:57:48
举报
文章被收录于专栏:Golang语言社区Golang语言社区

原文作者:smallnest

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

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

测试

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

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

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

代码语言:javascript
复制
 1package test
 2import (
 3    "reflect"
 4    "testing"
 5)
 6type Student struct {
 7    Name  string
 8    Age   int
 9    Class string
10    Score int
11}
12func BenchmarkReflect_New(b *testing.B) {
13    var s *Student
14    sv := reflect.TypeOf(Student{})
15    b.ResetTimer()
16    for i := 0; i < b.N; i++ {
17        sn := reflect.New(sv)
18        s, _ = sn.Interface().(*Student)
19    }
20    _ = s
21}
22func BenchmarkDirect_New(b *testing.B) {
23    var s *Student
24    b.ResetTimer()
25    for i := 0; i < b.N; i++ {
26        s = new(Student)
27    }
28    _ = s
29}
30func BenchmarkReflect_Set(b *testing.B) {
31    var s *Student
32    sv := reflect.TypeOf(Student{})
33    b.ResetTimer()
34    for i := 0; i < b.N; i++ {
35        sn := reflect.New(sv)
36        s = sn.Interface().(*Student)
37        s.Name = "Jerry"
38        s.Age = 18
39        s.Class = "20005"
40        s.Score = 100
41    }
42}
43func BenchmarkReflect_SetFieldByName(b *testing.B) {
44    sv := reflect.TypeOf(Student{})
45    b.ResetTimer()
46    for i := 0; i < b.N; i++ {
47        sn := reflect.New(sv).Elem()
48        sn.FieldByName("Name").SetString("Jerry")
49        sn.FieldByName("Age").SetInt(18)
50        sn.FieldByName("Class").SetString("20005")
51        sn.FieldByName("Score").SetInt(100)
52    }
53}
54func BenchmarkReflect_SetFieldByIndex(b *testing.B) {
55    sv := reflect.TypeOf(Student{})
56    b.ResetTimer()
57    for i := 0; i < b.N; i++ {
58        sn := reflect.New(sv).Elem() 
59        sn.Field(0).SetString("Jerry")
60        sn.Field(1).SetInt(18)
61        sn.Field(2).SetString("20005")
62        sn.Field(3).SetInt(100)
63    }
64}
65func BenchmarkDirect_Set(b *testing.B) {
66    var s *Student
67    b.ResetTimer()
68    for i := 0; i < b.N; i++ {
69        s = new(Student)
70        s.Name = "Jerry"
71        s.Age = 18
72        s.Class = "20005"
73        s.Score = 100
74    }
75}

测试结果:

代码语言:javascript
复制
1BenchmarkReflect_New-4                   20000000            70.0 ns/op        48 B/op          1 allocs/op
2BenchmarkDirect_New-4                    30000000            45.6 ns/op        48 B/op          1 allocs/op
3BenchmarkReflect_Set-4                   20000000            73.6 ns/op        48 B/op          1 allocs/op
4BenchmarkReflect_SetFieldByName-4         3000000           492 ns/op          80 B/op          5 allocs/op
5BenchmarkReflect_SetFieldByIndex-4       20000000           111 ns/op          48 B/op          1 allocs/op
6BenchmarkDirect_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进行赋值:

代码语言:javascript
复制
 1func (v Value) FieldByName(name string) Value {
 2    v.mustBe(Struct)
 3    if f, ok := v.typ.FieldByName(name); ok {
 4        return v.FieldByIndex(f.Index)
 5    }
 6    return Value{}
 7}
 8func (v Value) FieldByIndex(index []int) Value {
 9    if len(index) == 1 {
10        return v.Field(index[0])
11    }
12    v.mustBe(Struct)
13    for i, x := range index {
14        if i > 0 {
15            if v.Kind() == Ptr && v.typ.Elem().Kind() == Struct {
16                if v.IsNil() {
17                    panic("reflect: indirection through nil pointer to embedded struct")
18                }
19                v = v.Elem()
20            }
21        }
22        v = v.Field(x)
23    }
24    return v
25}

优化

从上面的测试结果看, 通过反射生成对象和字段赋值都会影响性能,但是通过反射的确确确实实能简化代码,为业务逻辑提供统一的代码, 比如标准库中json的编解码、rpc服务的注册和调用, 一些ORM框架比如gorm等,都是通过反射处理数据的,这是为了能处理通用的类型。

https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L946

代码语言:javascript
复制
 1  ......
 2      case reflect.String:
 3    v.SetString(string(s))
 4case reflect.Interface:
 5    if v.NumMethod() == 0 {
 6        v.Set(reflect.ValueOf(string(s)))
 7    } else {
 8        d.saveError(&UnmarshalTypeError{Value: "string", Type: v.Type(), Offset: int64(d.readIndex())})
 9          }
10  ......

https://github.com/jinzhu/gorm/blob/master/scope.go#L495

代码语言:javascript
复制
 1     for fieldIndex, field := range selectFields {
 2if field.DBName == column {
 3    if field.Field.Kind() == reflect.Ptr {
 4        values[index] = field.Field.Addr().Interface()
 5    } else {
 6        reflectValue := reflect.New(reflect.PtrTo(field.Struct.Type))
 7        reflectValue.Elem().Set(field.Field.Addr())
 8        values[index] = reflectValue.Interface()
 9        resetFields[index] = field
10    }
11    selectedColumnsMap[column] = offset + fieldIndex
12    if field.IsNormal {
13        break
14    }
15}
16     }

在我们追求高性能的场景的时候,我们可能需要尽量避免反射的调用, 比如对json数据的unmarshal, easyjson就通过生成器的方式,避免使用反射。

代码语言:javascript
复制
 1func (v *Student) UnmarshalJSON(data []byte) error {
 2    r := jlexer.Lexer{Data: data}
 3    easyjson4a74e62dDecodeGitABCReflect(&r, v)
 4    return r.Error()
 5}
 6func (v *Student) UnmarshalEasyJSON(l *jlexer.Lexer) {
 7    easyjson4a74e62dDecodeGitABCReflect(l, v)
 8}
 9func easyjson4a74e62dDecodeGitABCReflect(in *jlexer.Lexer, out *Student) {
10    isTopLevel := in.IsStart()
11    if in.IsNull() {
12        if isTopLevel {
13            in.Consumed()
14        }
15        in.Skip()
16        return
17    }
18    in.Delim('{')
19    for !in.IsDelim('}') {
20        key := in.UnsafeString()
21        in.WantColon()
22        if in.IsNull() {
23            in.Skip()
24            in.WantComma()
25            continue
26        }
27        switch key {
28        case "Name":
29            out.Name = string(in.String())
30        case "Age":
31            out.Age = int(in.Int())
32        case "Class":
33            out.Class = string(in.String())
34        case "Score":
35            out.Score = int(in.Int())
36        default:
37            in.SkipRecursive()
38        }
39        in.WantComma()
40    }
41    in.Delim('}')
42    if isTopLevel {
43        in.Consumed()
44    }
45}

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

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

代码语言:javascript
复制
 1func DirectInvoke(s *Student) {
 2    s.Name = "Jerry"
 3    s.Age = 18
 4    s.Class = "20005"
 5    s.Score = 100
 6}
 7func InterfaceInvoke(i interface{}) {
 8    s := i.(*Student)
 9    s.Name = "Jerry"
10    s.Age = 18
11    s.Class = "20005"
12    s.Score = 100
13}
14func BenchmarkDirectInvoke(b *testing.B) {
15    s := new(Student)
16    for i := 0; i < b.N; i++ {
17        DirectInvoke(s)
18    }
19    _ = s
20}
21func BenchmarkInterfaceInvoke(b *testing.B) {
22    s := new(Student)
23    for i := 0; i < b.N; i++ {
24        InterfaceInvoke(s)
25    }
26    _ = s
27}

测试结果:

代码语言:javascript
复制
1BenchmarkDirectInvoke-4                  300000000            5.60 ns/op        0 B/op          0 allocs/op
2BenchmarkInterfaceInvoke-4               200000000            6.64 ns/op        0 B/op          0 allocs/op

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


版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-02-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 测试
  • 测试结果
  • 优化
  • 顺带测一下"装箱/拆箱"操作带来的性能影响
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档