转载

Go Reflect 性能

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()
	}
}

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

原文  https://colobu.com/2019/01/29/go-reflect-performance/
正文到此结束
Loading...