go vs java基准测试_Go 原生VS反射基准测试性能的恐怖表现

一些比较流行的框架设计思想都是基于反射,比如反转控制(IOC)和依赖注入(DI),但是你了解其中的性能表现吗?

一般来说文件 I/O 的延迟远远大于书写反射代码造成的时延。然而,更快的响应速度和更低的CPU使用率仍然是网络服务器的优化目标。所以反射不仅带来了灵活性,也带来了性能低下的束缚。

要善用反思 反射 这把双刃剑,就需要详细了解反射的性能表现。以下基准测试在结构体赋值、函数调用等方面比较了原生调用和反射调用之间的性能差异。

一、结构体赋值比较

由于反射在结构中应用较多,因此结构体的访问性能成为人们关注的关键点。下面的示例使用一个实例化结构,访问其成员变量,然后使用 Go 语言的基准测试快速测试结果。

1.1 原生结构体的访问和赋值过程:

// 声明一个结构体, 拥有一个字段

type data struct {

Age int8

}

func BenchmarkNativeValue(b *testing.B) {

// 实例化结构体

v := data{Age: 22}

// 停止基准测试的计时器

b.StopTimer()

// 重置基准测试计时器数据

b.ResetTimer()

// 重新启动基准测试计时器

b.StartTimer()

// 根据基准测试数据进行循环测试

for i := 0; i < b.N; i++ {

// 结构体成员赋值测试

v.Age = 30

}

}

由于测试必须集中在赋值性能测试上,因此需要减少其他代码的干扰,所以在赋值完成后,将基准测试的计时器复位,重新开始计时,使用循环中基准测试提供的测试数量。

1.2 反射访问结构体成员并赋值的过程:

func BenchmarkReflectValue(b *testing.B) {

v := data{Age: 22}

// 取出结构体指针的反射值对象并取其元素

vv := reflect.ValueOf(&v).Elem()

// 根据名字取结构体成员

f := vv.FieldByName("Age")

b.StopTimer()

b.ResetTimer()

b.StartTimer()

for i := 0; i < b.N; i++ {

// 反射测试设置成员值性能

f.SetInt(30)

}

}

上面的代码中用到的反射值对象 SetInt() 函数方法,其中 Go 源码如下:

func (v Value) SetInt(x int64) {

v.mustBeAssignable()

switch k := v.kind(); k {

default:

panic(&ValueError{"reflect.Value.SetInt", v.kind()})

case Int:

*(*int)(v.ptr) = int(x)

case Int8:

*(*int8)(v.ptr) = int8(x)

case Int16:

*(*int16)(v.ptr) = int16(x)

case Int32:

*(*int32)(v.ptr) = int32(x)

case Int64:

*(*int64)(v.ptr) = x

}

}

因此使用 SetInt() 函数赋值是利用指针转换并赋值,其中并不会遍历结构体和内存操作这些耗时的算法在里面。

二、结构体成员搜索并赋值对比

func BenchmarkReflectGetFieldAndValue(b *testing.B) {

v := data{Hp: 2}

vv := reflect.ValueOf(&v).Elem()

b.StopTimer()

b.ResetTimer()

b.StartTimer()

for i := 0; i < b.N; i++ {

// 测试结构体成员的查找和设置成员的性能

vv.FieldByName("Hp").SetInt(3)

}

}

上面的代码将反射值对象的 FieldByName() 函数与 SetInt() 函数放在循环里进行检测,主要对比测试FieldByName() 函数对性能的影响。FieldByName() 函数源码如下:

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{}

}

底层代码介绍如下:

v.typ.FieldByName(name) 是通过名字查询类型对象,这里会进行一次遍历查找;找到类型对象后,return v.FieldByIndex(f.Index) 还需要在值中再次遍历一次查找对应的值。

通过查看源码可以看出,如果结构体字段数量和相对位置的不确定,那么 FieldByName() 函数就是效率比较低的查询方法。

三、调用函数方式性能测试

通过反射方式调用函数,其中可能导致的性能问题也要引起足够重视。

3.1 原生调用函数:

// 一个普通函数

func foo(v int) {

}

func BenchmarkNativeFunc(b *testing.B) {

for i := 0; i < b.N; i++ {

// 原生方式

foo(0)

}

}

3.2 反射调用函数:

func BenchmarkReflectFunc(b *testing.B) {

// 取函数的反射值对象

v := reflect.ValueOf(foo)

b.StopTimer()

b.ResetTimer()

b.StartTimer()

for i := 0; i < b.N; i++ {

// 反射调用函数

v.Call([]reflect.Value{reflect.ValueOf(2)})

}

}

下面对反射的相关方法分析一下:首先我们根据函数名取出反射值的对象,紧接着使用 reflect.ValueOf(2) 用 2 构造为反射值的对象,因为反射函数调用的参数必须都是反射值对象。再使用 []reflect.Value 构造多个参数列表传给反射值的对象并调用 Call() 函数。

通过反射调用函数的参数构造过程很非常复杂,构建很多对象会造成很大的内存回收负担。Call() 方法内部就更为复杂,需要将参数列表的每个值从 reflect.Value 类型转换为内存。调用完毕后,还要将函数返回值重新转换为 reflect.Value 类型返回。因此,反射调用函数的性能堪忧。

通过反射调用函数的参数构造过程很是复杂,而且构造很多对象会造成很大的内存回收问题。Call() 方法内部实现就更加复杂,它需要转换反射参数列表的每个值转换为内存值的类型。在调用完成后,应该将函数的返回值重新转换为 reflect.Value。所以反射调用函数的性能非常低效。

四、基准测试结果

通过执行 go test -v -bench=. 命令查看测试结果:

% go test -v -bench=.

goos: darwin

goarch: amd64

BenchmarkNativeValue

BenchmarkNativeValue-4 1000000000 0.326 ns/op

BenchmarkReflectValue

BenchmarkReflectValue-4 328987927 3.59 ns/op

BenchmarkReflectGetFieldAndValue

BenchmarkReflectGetFieldAndValue-4 13575862 80.4 ns/op

BenchmarkNativeFunc

BenchmarkNativeFunc-4 1000000000 0.325 ns/op

BenchmarkReflectFunc

BenchmarkReflectFunc-4 7053134 168 ns/op

PASS

ok test 5.226s

根据执行结果分析:

BenchmarkNativeValue 是原生的结构体成员变量的赋值,根据参考基准,每一次操作耗时 0.326 ns(纳秒)。

BenchmarkReflectValue 是通过反射的方式赋值,每一次操作耗时为 3.59 ns,性能比原生赋值低了 11 倍。

BenchmarkReflectGetFieldAndValue 是通过反射查找结构体成员且通过反射赋值,根据参考基准,每一次操作耗时 80.4 ns,减去通过反射结构体成员赋值的 80.4 - 3.59 = 76.81 ns,性能大概比原生低了 235 倍。这个测试结果与我们通过代码的分析结果类似。因为 SetInt 没有遍历操作性能可以接受,但是 FieldByName() 两次遍历导致性能非常低效。

BenchmarkNativeFunc 是原生函数的调用测试,性能与原生访问结构体成员接近,每一次操作耗时 0.325 ns。

BenchmarkReflectFunc 是通过反射函数调用的,性能就差了很多了,每一次操作耗时 168 ns,操作耗时比原生多消耗 516 倍。

五、总结

经过对代码的分析以及基准测试结果的数值对比,我们可以最终得出一些结论:

5.1 如果可以使用原生书写代码,应该尽可能减少反射代码的书写;

5.2 可以通过缓存反射值对象的方式,减少频繁获取反射值对象的性能影响;

5.3 尽可能避免通过反射的方式调用函数,如果必须使用反射调用函数也需提前缓存反射函数参数列表,并并尽可能少使用返回值。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值