82

Go Reflect 性能

 5 years ago
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中不区分 TypeValue 类型的, 所以至少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_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 + 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()
	}
}

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK