Golang探索reflect提速的方法

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值