Golang的reflect反射提速
基础知识
本文主要是介绍golang编程中,提高reflect效率的一些方式。关于反射的基础内容,在此不过多介绍。
如果想要了解相关知识,推荐看下列介绍reflect基础与应用的文章:
手写一个工具的过程讲清楚Go反射的使用方法和应用场景
深入介绍reflect原理:
Go 语言反射的实现原理
背景引入
在典型的需要反射的场景如反序列化,传递的参数对象类型并不固定。但是,反射的缺点就是效率比较低。
如我们有一个对象User,现在需要更改User的Age属性。
type User struct {
Name string
Age int
G Geo
}
type Geo struct {
X int
Y int
S string
}
// 直接版本
func SetStdAge(u any, age int) error {
if user, ok := u.(*User); ok {
user.Age = age
}
return nil
}
// 反射基本版
func SetAge(u any, a int) error {
typ := reflect.TypeOf(u)
val := reflect.ValueOf(u)
for typ.Kind() == reflect.Pointer {
typ = typ.Elem()
val = val.Elem()
}
if typ.Kind() != reflect.Struct {
return ErrType
}
fd := val.FieldByName("Age")
if fd.CanSet() {
fd.SetInt(int64(a))
}
return nil
}
两种age属性修改的方法,性能测试对比如下:
BenchmarkSimple/std_setValue-8 659803609 1.813 ns/op 0 B/op 0 allocs/op
BenchmarkSimple/base_simple-8 14242274 83.28 ns/op 8 B/op 1 allocs/op
可以看到,使用了反射会降低,所以,本文介绍针对加快reflect反射效率的探索。
reflect加速
使用cache
在基本版的反射中,每一个对象都需要调用FieldByName查找对应的属性信息,如果将类型结果缓存起来,无疑可以加快反射的效率。
var cacheV1_1 = map[reflect.Type][]int{}
func optimizeV1_1(u any, age int) error {
typ := reflect.TypeOf(u)
val := reflect.ValueOf(u)
for typ.Kind() == reflect.Pointer {
typ = typ.Elem()
val = val.Elem()
}
if typ.Kind() != reflect.Struct {
return ErrType
}
var index []int
var ok bool
if index, ok = cacheV1_1[typ]; !ok {
fd, exit := typ.FieldByName("Age")
if !exit {
return ErrNoField("Age")
}
index = fd.Index
cacheV1_1[typ] = index
}
fd := val.FieldByIndex(index)
if fd.CanSet() {
fd.SetInt(int64(age))
}
return nil
}
BenchmarkSimple/op_cache_v1-8 38810584 31.27 ns/op 0 B/op 0 allocs/op
使用cache与unsafe
reflect是基于unsafe包,目的为了提供安全访问的方法给开发者。在上面优化的基础上,再使用unsafe字段偏移量来设置对应的值,看看是否可以获得更高的效率。
- 可以通过reflect.Type.Offset获得属性相对结构体的偏移量
- any空接口在的接口为eface(存放了对象的类型与值的信息),可以通过自定义类型转化出any的结构,随后可以获得结构体的初始偏移量。
/* 在cache缓存的基础上,还可以直接使用unsafe的字段偏移量来完成值的修改 —— 优化的是setInt动作的冗余保证*/
var cacheV2 = map[reflect.Type]uintptr{}
// 空接口实际上是具有两个指针的结构的语法糖:第一个指向有关类型的信息,第二个指向值
// 可以使用结构体中字段偏移量来直接寻址该值的字段
type intfaceMark struct {
typ unsafe.Pointer
value unsafe.Pointer
}
func optimizeV2(u any, age int) error {
typ := reflect.TypeOf(u)
val := reflect.ValueOf(u)
for typ.Kind() == reflect.Pointer {
typ = typ.Elem()
val = val.Elem()
}
if typ.Kind() != reflect.Struct {
return ErrType
}
ptr, ok := cacheV2[typ]
if !ok {
structField, exit := typ.FieldByName("Age")
if !exit {
return ErrNoField("Age")
}
ptr = structField.Offset
cacheV2[typ] = ptr
}
structPtr := (*intfaceMark)(unsafe.Pointer(&u)).value
*(*int)(unsafe.Pointer(uintptr(structPtr) + ptr)) = age
return nil
}
BenchmarkSimple/op_unsafe_v2-8 49728358 24.80 ns/op 0 B/op 0 allocs/op
优化cache
纵观代码,可以压榨的内容:去掉了重复执行的步骤,去掉了相对而言冗余的封装。进一步的优化方向,可以是以map为基础的cache。
如果我们对 CPU 进行采样,将会看到大部分时间都用于访问 map,它还会显示 map 访问在调用 runtime.interhash 和 runtime.interequal。这些是用于 hash 接口并检查它们是否相等的函数。也许使用更简单的 key 会加快速度。
通过对上一版本的优化,其实我们也可以获得到结构体对应的类型信息。那么,也可以将cache的Key换成uintptr,这个类型的key可以提高map的索引速度。
/* 针对cache的进一步优化,提高map的索引速度——使用更加简单的key */
var cacheUnsafeV3 = map[uintptr]uintptr{}
func optimizeV3(u any, age int) error {
infMark := (*intfaceMark)(unsafe.Pointer(&u))
offset, ok := cacheUnsafeV3[uintptr(infMark.typ)]
if !ok {
typ := reflect.TypeOf(u)
val := reflect.ValueOf(u)
for typ.Kind() == reflect.Pointer {
typ = typ.Elem()
val = val.Elem()
}
if typ.Kind() != reflect.Struct {
return ErrType
}
fd, exit := typ.FieldByName("Age")
if !exit {
return ErrNoField("Age")
}
offset = fd.Offset
cacheUnsafeV3[uintptr(infMark.typ)] = offset
}
structPtr := infMark.value
*(*int)(unsafe.Pointer(uintptr(structPtr) + offset)) = age
return nil
}
BenchmarkSimple/op_mapKey_v3-8 224674586 5.415 ns/op 0 B/op 0 allocs/op
可以看到,到了这里,reflect其实也可以变得很高效。至于如果还要拼命压榨,丧心病狂下,还可以把cache也去掉——直接省下了索引map的时间。但是,这样就失去了通用性。
可以把上述代码动作抽象为两步:
- 获取结构,并返回一个“描述符”
- 直接通过描述符得到偏移量,修改对应的属性
这样的效率会更高,但是,并不通用。
所以,从基础版的反射到最后结果,步步提升了reflect的效率。
BenchmarkSimple/std_setValue-8 659803609 1.813 ns/op 0 B/op 0 allocs/op
BenchmarkSimple/base_simple-8 14242274 83.28 ns/op 8 B/op 1 allocs/op
BenchmarkSimple/op_cache_v1-8 38810584 31.27 ns/op 0 B/op 0 allocs/op
BenchmarkSimple/op_unsafe_v2-8 49728358 24.80 ns/op 0 B/op 0 allocs/op
BenchmarkSimple/op_mapKey_v3-8 224674586 5.415 ns/op 0 B/op 0 allocs/op
上述代码与测试放在这里:
https://gitee.com/zeng-jinghao/go101stu/tree/master/base/fast_reflect
参考
最核心:https://philpearl.github.io/post/aintnecessarilyslow/
https://mojotv.cn/go/go-unit-test-interface