反射给人的印象就是慢,影响运行效率,其实不全对,如果用的合理还是很快的
一、基本的reflect
package pkg
import (
"errors"
"reflect"
)
type People struct {
Name string
Age int
sex int
}
func simplePopulateStruct(people *People){
people.Name = "zhangSan"
}
//反射基本版
func populateStruct(people interface{})(err error){
val := reflect.ValueOf(people)
if val.Type().Kind() != reflect.Ptr{
return errors.New("pass in a pointer")
}
elm := val.Elem()
if elm.Type().Kind() != reflect.Struct{
return errors.New("type is not struct")
}
fval := elm.FieldByName("Name")
fval.SetString("zhangSan")
return
}
$ go test -bench=. -v --benchmem
goos: darwin
goarch: arm64
pkg: test/reflect-cache/pkg
BenchmarkPopulateStruct
BenchmarkPopulateStruct-10 11241620 89.62 ns/op 8 B/op 1 allocs/op
BenchmarkSimplePopulateStruct
BenchmarkSimplePopulateStruct-10 574125594 2.075 ns/op 0 B/op 0 allocs/op
PASS
ok test/reflect-cache/pkg 2.930s
我们可以看到差距还是不小的 89.62ns he 2.075ns 而且还没有堆内存分配,知道为什么有堆内存的分配吗?可以看这个 issue:https://github.com/golang/go/issues/2320
二、 reflect-cache改进版
我们能做得更好吗?好吧,通常我们运行的程序不会只做一件事然后停止。他们通常一遍又一遍地做着非常相似的事情。因此,我们可以设置一些东西以使重复的事情速度变快吗?
如果仔细查看我们正在执行的反射检查,我们会发现它们都取决于传入值的类型。如果我们将类型结果缓存起来,那么对于每种类型而言,我们只会进行一次检查。
我们再来考虑内存分配的问题。之前我们调用 Value.FieldByName 方法,实际是 Value.FieldByName 调用 Type.FieldByName,其调用 structType.FieldByName,最后调用 structType.Field 来引起内存分配的。我们可以在类型上调用 FieldByName 并缓存一些东西来获取 B 字段的值吗?实际上,如果我们缓存 Field.Index,就可以使用它来获取字段值而无需分配。
//反射-cache基本版
func populateStructCache(in interface{})(err error){
typ := reflect.TypeOf(in)
index , ok := cache[typ]
if !ok{
if typ.Kind() != reflect.Ptr{
return errors.New("pass in a pointer")
}
if typ.Elem().Kind() != reflect.Struct{
return errors.New("type is not struct")
}
f, ok1 := typ.Elem().FieldByName("Name")
if !ok1{
return errors.New("struct does not have field Name")
}
index = f.Index
cache[typ] = index
}
val := reflect.ValueOf(in)
elm := val.Elem()
fVal := elm.FieldByIndex(index)
fVal.SetString("zhangSan")
return
}
结果
$ go test -bench=. -v --benchmem
goos: darwin
goarch: arm64
pkg: test/reflect-cache/pkg
BenchmarkPopulateStruct
BenchmarkPopulateStruct-10 12861397 90.09 ns/op 8 B/op 1 allocs/op
BenchmarkPopulateStructCache
BenchmarkPopulateStructCache-10 33810992 34.68 ns/op 0 B/op 0 allocs/op
BenchmarkSimplePopulateStruct
BenchmarkSimplePopulateStruct-10 563873890 2.120 ns/op 0 B/op 0 allocs/op
PASS
ok test/reflect-cache/pkg 4.352s
我们看到 结果34.68ns 比90.09ns小很多 而且没有堆内存的分配
三、继续优化-利用字段偏移量
我们能做得更好吗?好吧,如果我们知道结构体字段 B 的偏移量并且知道它是 int 类型,就可以将其直接写入内存。我们可以从接口中恢复指向结构体的指针,因为空接口实际上是具有两个指针的结构的语法糖:第一个指向有关类型的信息,第二个指向值。
type eface struct {
_type *_type
data unsafe.Pointer
}
我们可以使用结构体中字段偏移量来直接寻址该值的字段 B。
新代码如下。
//反射-unsafe
func populateStructUnsafe(in interface{})(err error){
typ := reflect.TypeOf(in)
offset, ok := unsafeCache[typ]
if !ok {
if typ.Kind() != reflect.Ptr {
return fmt.Errorf("you must pass in a pointer")
}
if typ.Elem().Kind() != reflect.Struct {
return fmt.Errorf("you must pass in a pointer to a struct")
}
f, ok := typ.Elem().FieldByName("Name")
if !ok {
return fmt.Errorf("struct does not have field B")
}
if f.Type.Kind() != reflect.String {
return fmt.Errorf("field Name should be an string")
}
offset = f.Offset
unsafeCache[typ] = offset
}
structPtr := (*modelFace)(unsafe.Pointer(&in)).value
*(*string)(unsafe.Pointer(uintptr(structPtr) + offset)) = "zhangSan"
return
}
结果如下
go test -bench=. -v --benchmem
goos: darwin
goarch: arm64
pkg: test/reflect-cache/pkg
BenchmarkPopulateStruct
BenchmarkPopulateStruct-10 11368377 90.11 ns/op 8 B/op 1 allocs/op
BenchmarkPopulateStructCache
BenchmarkPopulateStructCache-10 34820835 33.68 ns/op 0 B/op 0 allocs/op
BenchmarkPopulateStructUnsafe
BenchmarkPopulateStructUnsafe-10 64028455 18.66 ns/op 0 B/op 0 allocs/op
BenchmarkSimplePopulateStruct
BenchmarkSimplePopulateStruct-10 581613267 2.060 ns/op 0 B/op 0 allocs/op
PASS
ok test/reflect-cache/pkg 5.835s
相比于类型的缓存,这时offset更加高效,达到了18.66ns 比33.68ns 提高不少
四、更改key的类型
还能让它走得更快吗?如果我们对 CPU 进行采样,将会看到大部分时间都用于访问 map,它还会显示 map 访问在调用 runtime.interhash 和 runtime.interequal。这些是用于 hash 接口并检查它们是否相等的函数。也许使用更简单的 key 会加快速度?我们可以使用来自接口的类型信息的地址,而不是 reflect.Type 本身。
//反射-uintptr
func populateStructUintPtr(in interface{})(err error){
inf := (*modelFace)(unsafe.Pointer(&in))
offset, ok := uintPtrCache[uintptr(inf.typ)]
if !ok {
typ := reflect.TypeOf(in)
if typ.Kind() != reflect.Ptr {
return fmt.Errorf("you must pass in a pointer")
}
if typ.Elem().Kind() != reflect.Struct {
return fmt.Errorf("you must pass in a pointer to a struct")
}
f, ok := typ.Elem().FieldByName("Name")
if !ok {
return fmt.Errorf("struct does not have field B")
}
if f.Type.Kind() != reflect.String {
return fmt.Errorf("field B should be an String")
}
offset = f.Offset
uintPtrCache[uintptr(inf.typ)] = offset
}
*(*string)(unsafe.Pointer(uintptr(inf.value) + offset)) = "zhangSan"
return
}
结果
func BenchmarkPopulateStructUintPtr(b *testing.B){
for i := 0; i < b.N; i++ {
var m People
if err := populateStructUintPtr(&m);err != nil{
b.Fatal(err)
}
if m.Name != "zhangSan"{
b.Fatal("name is err")
}
}
}
$ go test -bench=. -v --benchmem
goos: darwin
goarch: arm64
pkg: test/reflect-cache/pkg
BenchmarkPopulateStruct
BenchmarkPopulateStruct-10 9324838 107.8 ns/op 40 B/op 2 allocs/op
BenchmarkPopulateStructCache
BenchmarkPopulateStructCache-10 21790118 53.78 ns/op 32 B/op 1 allocs/op
BenchmarkPopulateStructUnsafe
BenchmarkPopulateStructUnsafe-10 31375760 37.73 ns/op 32 B/op 1 allocs/op
BenchmarkPopulateStructUintPtr
BenchmarkPopulateStructUintPtr-10 52600530 23.11 ns/op 32 B/op 1 allocs/op
BenchmarkSimplePopulateStruct
BenchmarkSimplePopulateStruct-10 578709519 2.065 ns/op 0 B/op 0 allocs/op
PASS
ok test/re
可以看到 已经到了23.11ns
五、引入描述符
还能更快吗?通常如果我们要将数据 unmarshaling 到结构体中,它总是相同的结构。因此,我们可以将功能一分为二,其中一个函数用于检查结构是否符合要求并返回一个描述符,另外一个函数则可以在之后的填充调用中使用该描述符。
以下是我们的新代码版本。调用者应该在初始化时调用describeType函数以获得一个typeDescriptor,之后调用populateStructUnsafe3函数时会用到它。在这个非常简单的例子中,typeDescriptor只是结构体中B字段的偏移量。
type typeDescriptor uintptr
//描述符
func populateStructUintDescriptor(in interface{}, ti typeDescriptor)(err error){
structPtr := (*modelFace)(unsafe.Pointer(&in)).value
*(*string)(unsafe.Pointer(uintptr(structPtr) + uintptr(ti))) = "zhangSan"
return nil
}
func describeType(in interface{}) (typeDescriptor, error) {
typ := reflect.TypeOf(in)
if typ.Kind() != reflect.Ptr {
return 0, fmt.Errorf("you must pass in a pointer")
}
if typ.Elem().Kind() != reflect.Struct {
return 0, fmt.Errorf("you must pass in a pointer to a struct")
}
f, ok := typ.Elem().FieldByName("Name")
if !ok {
return 0, fmt.Errorf("struct does not have field B")
}
if f.Type.Kind() != reflect.String {
return 0, fmt.Errorf("field B should be an int")
}
return typeDescriptor(f.Offset), nil
}
func BenchmarkPopulateStructDescriptor(b *testing.B){
descriptor, err := describeType((*People)(nil))
if err != nil {
b.Fatal(err)
}
for i := 0; i < b.N; i++ {
var m People
if err := populateStructUintDescriptor(&m,descriptor);err != nil{
b.Fatal(err)
}
if m.Name != "zhangSan"{
b.Fatal("name is err")
}
}
}
$ go test -bench=. -v --benchmem
goos: darwin
goarch: arm64
pkg: test/reflect-cache/pkg
BenchmarkPopulateStruct
BenchmarkPopulateStruct-10 10680764 109.8 ns/op 40 B/op 2 allocs/op
BenchmarkPopulateStructCache
BenchmarkPopulateStructCache-10 22108149 54.34 ns/op 32 B/op 1 allocs/op
BenchmarkPopulateStructUnsafe
BenchmarkPopulateStructUnsafe-10 30279804 37.89 ns/op 32 B/op 1 allocs/op
BenchmarkPopulateStructUintPtr
BenchmarkPopulateStructUintPtr-10 50561442 23.03 ns/op 32 B/op 1 allocs/op
BenchmarkPopulateStructDescriptor
BenchmarkPopulateStructDescriptor-10 840861920 1.524 ns/op 0 B/op 0 allocs/op
BenchmarkSimplePopulateStruct
BenchmarkSimplePopulateStruct-10 583841812 2.056 ns/op 0 B/op 0 allocs/op
PASS
ok test/reflect-cache/pkg 8.230s
可以看到达到了惊人的1.524ns,甚至超过了直接修改的速度