原文作者:smallnest
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类型为例:
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}
测试结果:
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_Set
和Direct_Set
性能的主要差别还是在于对象的生成,因为之后字段的赋值方法都是一样的,这也和对象创建的测试case的结果是一致的。
如果通过反射进行赋值,性能下降是很厉害的,耗时成倍的增长。比较有趣的是,FieldByName
方式赋值是Field
方式赋值的好几倍, 原因在于FieldByName
会有额外的循环进行字段的查找,虽然最终它还是调用Field
进行赋值:
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等,都是通过反射处理数据的,这是为了能处理通用的类型。
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 ......
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就通过生成器的方式,避免使用反射。
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}
其它的一些编解码库也提供了这种避免使用反射的方法来提高性能。
顺带测一下"装箱/拆箱"操作带来的性能影响
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}
测试结果:
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{}(以及反向操作)确实回带来一点点性能的影响,不过看起来影响倒不是很大。
版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢。
Golang语言社区
ID:Golangweb
www.bytedancing.com
游戏服务器架构丨分布式技术丨大数据丨游戏算法学习