Go Reflect 提高反射性能

Go 语言/golang 高性能编程,Go 语言进阶教程,Go 语言高性能编程(high performance go)。本文介绍了反射的使用场景,并测试了反射的性能,以及某些场景下的替代方式。

high performance go - data structure

1 反射的用途

标准库 reflectarrow-up-right 为 Go 语言提供了运行时动态获取对象的类型和值以及动态创建对象的能力。反射可以帮助抽象和简化代码,提高开发效率。

Go 语言标准库以及很多开源软件中都使用了 Go 语言的反射能力,例如用于序列化和反序列化的 json、ORM 框架 gorm/xorm 等。

7days-golangarrow-up-right 这个项目中,也有好几处用到了反射。在 七天用Go从零实现RPC框架arrow-up-right 中,我们使用反射在服务端,利用接收到的二进制报文动态创建对象,例如利用反射实现函数的动态调用。在 7天用Go从零实现ORM框架GeeORMarrow-up-right 中,我们使用反射,实现了结构体(struct)类型和数据库表名的映射,结构体字段和数据库字段的映射。同样利用反射动态创建对象的能力,将数据库中查询到的记录转换为 Go 语言中的对象。

2 反射如何简化代码

接下来呢,我们利用反射实现一个简单的功能,来看看反射如何帮助我们简化代码的。

假设有一个配置类 Config,每个字段是一个配置项。为了简化实现,假设字段均为 string 类型:

配置默认从 json 文件中读取,如果环境变量中设置了某个配置项,则以环境变量中的配置为准。配置项和环境变量对应的规则非常简单:将 json 字段的字母转为大写,将 - 转为下划线,并添加 CONFIG_ 前缀。

最终的对应结果如下:

实现这个功能非常简单,使用 switch case 或者 if else 硬编码很快就搞定了。但是,如果使用硬编码,Config 结构发生改变,例如修改 json 对应的字段,删除或新增了一个配置项,这块逻辑也需要发生改变。而更大的问题在于:容易出错,不好测试!!!

这个时候,就有了 reflect 的用武之地了。

实现逻辑其实是非常简单的:

  • 在运行时,利用反射获取到 Config 的每个字段的 Tag 属性,拼接出对应的环境变量的名称。

  • 查看该环境变量是否存在,如果存在,则将环境变量的值赋值给该字段。

运行该程序,输出为:

可以看到,环境变量中设置的三个配置项已经生效。之后无论结构体 Config 内部的字段发生任何改变,这部分代码无需任何修改即可完美的适配,出错概率也极大地降低。

3 反射的性能

毫无疑问的是,反射会增加额外的代码指令,对性能肯定会产生影响的。具体影响有多大,我们可以使用 Benchmark 来测试一番。

3.1 创建对象

测试结果如下:

通过反射创建对象的耗时约为 new 的 1.5 倍,相差不是特别大。

3.2 修改字段的值

通过反射获取结构体的字段有两种方式,一种是 FieldByName,另一种是 Field(按照下标)。前面的例子中,我们使用的是 FieldByName

测试结果如下:

  • 三种场景下,对象已经提前创建好,测试的均为给字段赋值所消耗的时间。

  • 普通的赋值操作,每次耗时约为 0.3 ns,通过下标找到对应的字段再赋值,每次耗时约为 30 ns,通过名称找到对应字段再赋值,每次耗时约为 300 ns。

总结一下,对于一个普通的拥有 4 个字段的结构体 Config 来说,使用反射给每个字段赋值,相比直接赋值,性能劣化约 100 - 1000 倍。其中,FieldByName 的性能相比 Field 劣化 10 倍。

3.3 FieldByName 和 Field 性能差距

FieldByNameField 十倍的性能差距让我对 FieldByName 的内部实现比较好奇,打开源代码一探究竟:

  • reflect/value.go

  • reflect/type.go

整个调用链条是比较简单的:

(t *structType) FieldByName 中使用 for 循环,逐个字段查找,字段名匹配时返回。也就是说,在反射的内部,字段是按顺序存储的,因此按照下标访问查询效率为 O(1),而按照 Name 访问,则需要遍历所有字段,查询效率为 O(N)。结构体所包含的字段(包括方法)越多,那么两者之间的效率差距则越大。

4 如何提高性能

4.1 避免使用反射

使用反射赋值,效率非常低下,如果有替代方案,尽可能避免使用反射,特别是会被反复调用的热点代码。例如 RPC 协议中,需要对结构体进行序列化和反序列化,这个时候避免使用 Go 语言自带的 jsonMarshalUnmarshal 方法,因为标准库中的 json 序列化和反序列化是利用反射实现的。可选的替代方案有 easyjsonarrow-up-right,在大部分场景下,相比标准库,有 5 倍左右的性能提升。

4.2 缓存

在上面的例子中可以看到,FieldByName 相比于 Field 有一个数量级的性能劣化。那在实际的应用中,就要避免直接调用 FieldByName。我们可以利用字典将 NameIndex 的映射缓存起来。避免每次反复查找,耗费大量的时间。

我们利用缓存,优化下刚才的测试用例:

测试结果如下:

消耗时间从原来的 10 倍,缩小到了 2 倍。

附 推荐与参考

Last updated