让reflect快起来

0 评论
/ /
462 阅读
/
10194 字
28 2023-06

反射给人的印象就是慢,影响运行效率,其实不全对,如果用的合理还是很快的

一、基本的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,甚至超过了直接修改的速度