Golang 反射介绍和性能分析

2 篇文章 0 订阅
1 篇文章 0 订阅

 百度百科给的反射定义:“反射是一种计算机处理方式。有程序可以访问、检测和修改它本身状态或行为的这种能力。”我是觉着这个定义有点晦涩难懂,至少我在学Java的时候没太搞明白是什么意思。

struct TestStrcut type{
    IsField int `json:"is_field"`// 这个是IsField字段,类型是int,tag内容为is_field
}

为了让大家对反射先有个认识,由此我先提问一个小问题,我们在编写golang程序时,如果想获取到某个结构体的字段名或者类型名,可以获取到吗?

答:在程序中,貌似我们只能读取和修改实例的值,并不能读到他是什么类型(int、struct等类型)和字段名(上面代码中的IsField这个字段名)。如果我们真的有这个需求呢,想读到实例本身的元数据呢,我们需要借助于反射的特性,通过反射我们可以获取到实例本身的元数据信息(字段类型、字段名、字段tag等信息)。

 

类型元数据信息:元数据信息的意思指,类型本身的一些信息,字段名,tag名,类型名等信息。元数据信息存储在可执行文件的哪个位置,就不进行阐述了(目前我也不太清楚,哈哈,大家见谅啊),肯定不在堆栈上,也不在data和bss区域上。

 

看到这里,我们应该清楚反射最常用的一个功能,运行时获取实例的类型,这个特性一般用于什么场景呢?

答:我现在能想到的有两个例子:1.将数据序列化与反序列化(将结构体实例转化为json文本)2.数据库表和结构体相互映射。从这里可以看到,反射还是很有用的。大家也可以思考一下,反射应用的例子。

 

现在我对反射的特性作一个比较全面的描述,一共有以下几个作用:

  • 访问任意一个实例所属的类型以及该类型的元数据信息。
  • 构造任意一个结构的实例。意思就是指利用反射可以在运行程序时生成结构体类型。
  • 访问任意一个实例所具有的字段内容和方法,并且可以调用该实例的方法

反射性能分析

大家都或多或少听说过反射性能偏低,使用反射要比正常调用要低几倍到数十倍,不知道大家有没有思考过反射性能都低在哪些方面,我先做一个简单分析,通过反射在获取或者修改值内容时,多了几次内存引用,多绕了几次弯,肯定没有直接调用某个值来的迅速,这个是反射带来的固定性能损失,还有一方面的性能损失在于,结构体类型字段比较多时,要进行遍历匹配才能获取对应的内容。下面就根据反射具体示例来分析性能:

首先制定一个结构体,在后续写性能测试示例时会用得到,结构体如下:

// 测试用例
type TestReflectField struct {
	A string
	B int
	C bool
	D float64
}

// F 是一个无输入输出的方法
func (s *TestReflectField) F() {
	_ = fmt.Sprint(0)
}

// F1 是一个有简单输入值的方法
func (s *TestReflectField) F1(a int) {
	_ = fmt.Sprint(0)
}

// F2 是一个有复杂输入值的方法
func (s *TestReflectField) F2(a TestReflectField) {
	_ = fmt.Sprint(0)
}

// F3 是一个接口作为输入值的方法
func (s *TestReflectField) F3(a interface{}) {
	_ = fmt.Sprint(0)
}

// F4 是一个有多个输入值的方法
func (s *TestReflectField) F4(a, b, c, d, e, f int) {
	_ = fmt.Sprint(0)
}

// F5 是一个有返回值的方法
func (s *TestReflectField) F5() int {
	return 0
}

// TestInterface 定义一个接口
type TestInterface interface {
	F()
}

测试结构体初始化

// 测试结构体初始化的反射性能
func Benchmark_Reflect_New(b *testing.B) {
	var tf *TestReflectField
	t := reflect.TypeOf(TestReflectField{})
	for i := 0; i < b.N; i++ {
		tf = reflect.New(t).Interface().(*TestReflectField)
	}
	_ = tf
}

// 测试结构体初始化的性能
func Benchmark_New(b *testing.B) {
	var tf *TestReflectField
	for i := 0; i < b.N; i++ {
		tf = new(TestReflectField)
	}
	_ = tf
}

测试结果如下:

性能分析:

使用反射初始化结构体创建实例,比使用new创建实例性能要低两倍不到,性能损耗不算大,在可以接受的范围。

测试结构体字段赋值

测试正常修改字段内容和通过反射设置字段内容的性能对比,测试代码如下:

// 测试反射设置结构体字段的性能
func Benchmark_Reflect_Field(b *testing.B) {
	var tf = new(TestReflectField)

	temp := reflect.ValueOf(tf).Elem()
	for i := 0; i < b.N; i++ {
		temp.Field(1).SetInt(int64(1995))
	}
	_ = tf
}

// 测试反射设置结构体字段的性能
func Benchmark_Reflect_FieldByName(b *testing.B) {
	var tf = new(TestReflectField)
	temp := reflect.ValueOf(tf).Elem()
	for i := 0; i < b.N; i++ {
		temp.FieldByName("B").SetInt(int64(1995))
	}
	_ = tf
}

// 测试结构体字段设置的性能
func Benchmark_Field(b *testing.B) {
	var tf = new(TestReflectField)
	for i := 0; i < b.N; i++ {
		tf.B = i
	}
	_ = tf
}

测试结果如下:

性能分析:

直接对实例变量进行赋值每次0.28ns,性能是通过反射操作实例变量的23倍左右。使用反射的两个测试案例都很慢,使用FieldByName("B")方法性能比使用Field(2)方法性能要低十倍左右,看代码的话我们会发现,FieldByName是通过遍历匹配所有的变量,来查询所需要的内容,所以性能要低上很多。建议:减少使用FieldByName方法。在需要使用反射进行成员变量访问的时候,尽可能的使用成员的序号。如果只知道成员变量的名称的时候,看具体代码的使用场景,如果可以在启动阶段或在频繁访问前,通过 Type.NumField 、Type.Field 和 StructField.Name 得到成员的序号,注意这里需要的是使用的是 reflect.Type 而不是 reflect.Value,通过 reflect.Value 是得不到字段名称的。

测试实例变量读取性能

测试通过实例变量直接读取数据和通过反射读取数据的性能,示例代码如下:

// 测试反射读取结构体字段值的性能
func Benchmark_Reflect_GetField(b *testing.B) {
	var tf = new(TestReflectField)
	var r int64
	temp := reflect.ValueOf(tf).Elem()
	for i := 0; i < b.N; i++ {
		r = temp.Field(1).Int()
	}
	_ = tf
	_ = r
}

// 测试反射读取结构体字段值的性能
func Benchmark_Reflect_GetFieldByName(b *testing.B) {
	var tf = new(TestReflectField)
	temp := reflect.ValueOf(tf).Elem()
	var r int64
	for i := 0; i < b.N; i++ {
		r = temp.FieldByName("B").Int()
	}
	_ = tf
	_ = r
}

// 测试结构体字段读取数据的性能
func Benchmark_GetField(b *testing.B) {
	var tf = new(TestReflectField)
	tf.B = 1995
	var r int
	for i := 0; i < b.N; i++ {
		r = tf.B
	}
	_ = tf
	_ = r
}

性能分析:

使用反射读取数据比普通读取数据满了16倍左右,这都是源于多了几次内存调用导致的性能下降。通过FieldByName("B")方法获取数据比通过序号Field(2)方法性能低了十几倍,原因上面已经阐述过了。建议如上,如果只知道字段名字的前提下,先提前通过 Type.NumField 、Type.Field 和 StructField.Name 得到成员的序号,然后调用Field()获取数据。

测试方法调用的性能

测试各种情形下的方法调用,无参数,有参数,有复杂参数,有返回值方法等情况,测试案例如下:


// 测试通过结构体访问方法性能
func BenchmarkMethod(b *testing.B) {
	t := &TestReflectField{}
	for i := 0; i < b.N; i++ {
		t.F()
	}
}

// 测试通过反射访问无参数方法性能
func BenchmarkReflectMethod(b *testing.B) {
	v := reflect.ValueOf(&TestReflectField{})
	for i := 0; i < b.N; i++ {
		v.Method(0).Call(nil)
	}
}

// 测试通过反射访问无参数方法性能
func BenchmarkReflectMethodByName(b *testing.B) {
	v := reflect.ValueOf(&TestReflectField{})
	for i := 0; i < b.N; i++ {
		v.MethodByName("F").Call(nil)
	}
}

// 测试通过反射访问有参数方法性能
func BenchmarkReflectMethod_WithArgs(b *testing.B) {
	v := reflect.ValueOf(&TestReflectField{})
	for i := 0; i < b.N; i++ {
		v.Method(1).Call([]reflect.Value{reflect.ValueOf(i)})
	}
}

// 测试通过反射访问结构体参数方法性能
func BenchmarkReflectMethod_WithArgs_Mul(b *testing.B) {
	v := reflect.ValueOf(&TestReflectField{})
	for i := 0; i < b.N; i++ {
		v.Method(2).Call([]reflect.Value{reflect.ValueOf(TestReflectField{})})
	}
}

// 测试通过反射访问接口参数方法性能
func BenchmarkReflectMethod_WithArgs_Interface(b *testing.B) {
	v := reflect.ValueOf(&TestReflectField{})
	for i := 0; i < b.N; i++ {
		var tf TestInterface = &TestReflectField{}
		v.Method(3).Call([]reflect.Value{reflect.ValueOf(tf)})
	}
}

// 测试访问多参数方法性能
func BenchmarkMethod_WithManyArgs(b *testing.B) {
	s := &TestReflectField{}
	for i := 0; i < b.N; i++ {
		s.F4(i, i, i, i, i, i)
	}
}

// 测试通过反射访问多参数方法性能
func BenchmarkReflectMethod_WithManyArgs(b *testing.B) {
	v := reflect.ValueOf(&TestReflectField{})
	va := make([]reflect.Value, 0)
	for i := 1; i <= 6; i++ {
		va = append(va, reflect.ValueOf(i))
	}

	for i := 0; i < b.N; i++ {
		v.Method(4).Call(va)
	}
}

// 测试访问有返回值的方法性能
func BenchmarkMethod_WithResp(b *testing.B) {
	s := &TestReflectField{}
	for i := 0; i < b.N; i++ {
		_ = s.F5()
	}
}

// 测试通过反射访问有返回值的方法性能
func BenchmarkReflectMethod_WithResp(b *testing.B) {
	v := reflect.ValueOf(&TestReflectField{})
	for i := 0; i < b.N; i++ {
		_ = v.Method(5).Call(nil)[0].Int()
	}
}

测试结果如下:

性能分析:

经过前几次性能测试,不用看测试结果我们也大概能猜出结果是什么样子,通过反射调用方法肯定比普通调用方法慢很多。不考虑方法执行内容的前提下,反射调用方法比直接调用慢5~10倍,如果考虑到方法本身就有复杂的执行过程,这个性能差距会被缩小。这几个不同情况的反射也有差别,性能从高到低如下:无参数方法>有参数方法≈接口调用有参数方法>复杂参数方法>多参数方法。

总结

使用反射必定会导致性能下降,但是反射是一个强有力的工具,可以解决我们平时的很多问题,比如数据库映射、数据序列化、代码生成场景。在使用反射的时候,我们需要避免一些性能过低的操作,例如使用FieldByName()和MethodByName()方法,如果必须使用这些方法的时候,我们可以预先通过字段名或者方法名获取到对应的字段序号,然后使用性能较高的反射操作,以此提升使用反射的性能。

 

参考

1.golang 1.12 工具库

2.公司内部技术博客-Golang 反射性能和优化

 

源码:见下方资源,如果可以显示的话

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值