Golang的反射最为人诟病的就是它极差的性能,接下来我们尝试优化它的性能。
如果我们使用正常的流程来创建一个对象,将会是如下的代码片段:

  1. type People struct {
  2. Age int
  3. Name string
  4. }
  5. func New() *People {
  6. return &People{
  7. Age: 18,
  8. Name: "shiina",
  9. }
  10. }

以上的代码非常好读,但是如果我们要开发一款框架,接收的类型非常有可能是动态的、不确定的,那么就会使用到反射(Reflect)功能,使用反射来创建一个如上的Person对象大概是如下的代码片段:

  1. func NewUseReflect() interface{} {
  2. var p People
  3. t := reflect.TypeOf(p)
  4. v := reflect.New(t)
  5. v.Elem().Field(0).Set(reflect.ValueOf(18))
  6. v.Elem().Field(1).Set(reflect.ValueOf("shiina"))
  7. return v.Interface()
  8. }

如上是一段普通的反射代码,既然大家都说Go的反射性能极差,那么我们就来自己看一下它的性能和上一个我们正常创建Person对象比性能差了多少。

简单的性能测试

让我们先用Go自带的go bench来分析一下它的性能

  1. func BenchmarkNew(b *testing.B) {
  2. b.ReportAllocs()
  3. b.ResetTimer()
  4. for i := 0; i < b.N; i++ {
  5. New()
  6. }
  7. }
  8. func BenchmarkNewUseReflect(b *testing.B) {
  9. b.ReportAllocs()
  10. b.ResetTimer()
  11. for i := 0; i < b.N; i++ {
  12. NewUseReflect()
  13. }
  14. }

我们得到的测试结果如下:

  1. BenchmarkNew
  2. BenchmarkNew-16 1000000000 1.55 ns/op 0 B/op 0 allocs/op
  3. BenchmarkNewUseReflect
  4. BenchmarkNewUseReflect-16 4787185 248 ns/op 64 B/op 2 allocs/op

我们能够发现使用反射的耗时是不使用的160倍左右

性能损耗的猜测

那么反射创建对象,主要的性能损耗在哪里呢?我们先进行一个实验:
并且当我们增加更多的结构体成员变量,比如增加两个string类型的成员变量,进行一次性能测试,然后再去掉所有的成员变量,进行一次性能测试。

  • 四个成员变量: ```go type People struct { Age int Name string Test1 string Test2 string } func New() interface{} { return &People{
    1. Age: 18,
    2. Name: "shiina",
    Test1: “test1”, Test2: “test2”, } } func NewUseReflect() interface{} { var p People t := reflect.TypeOf(p) v := reflect.New(t) v.Elem().Field(0).Set(reflect.ValueOf(18)) v.Elem().Field(1).Set(reflect.ValueOf(“shiina”)) v.Elem().Field(2).Set(reflect.ValueOf(“test1”)) v.Elem().Field(3).Set(reflect.ValueOf(“test2”)) return v.Interface() } —————————————————————————————————————————— BenchmarkNew BenchmarkNew-16 1000000000 1.12 ns/op 0 B/op 0 allocs/op BenchmarkNewUseReflect BenchmarkNewUseReflect-16 3334735 366 ns/op 128 B/op 2 allocs/op
  1. - 无成员变量:
  2. ```go
  3. type People struct{}
  4. func New() interface{} {
  5. return &People{}
  6. }
  7. func NewUseReflect() interface{} {
  8. var p People
  9. t := reflect.TypeOf(p)
  10. v := reflect.New(t)
  11. return v.Interface()
  12. }
  13. ——————————————————————————————————————————
  14. BenchmarkNew
  15. BenchmarkNew-16 1000000000 1.32 ns/op 0 B/op 0 allocs/op
  16. BenchmarkNewUseReflect
  17. BenchmarkNewUseReflect-16 17362648 62.3 ns/op 0 B/op 0 allocs/op

我们猜测,反射性能的损耗具体分为两个部分,一个部分是reflect.New(),另一个部分是value.Field().Set()
这时候我们可以使用Go原生自带的性能分析工具pprof来分析一下它们的主要耗时,来验证我们的猜测。
我们对四个成员变量测试用例使用pprof

  1. # 生成测试数据
  2. kieranhu@KIERANHU-MC0 ~/Downloads> go test -bench=. -benchmem -memprofile memprofile.out -cpuprofile profile.out
  3. # 分析测试数据
  4. kieranhu@KIERANHU-MC0 ~/Downloads> go tool pprof ./profile.out
  5. Type: cpu
  6. Time: Apr 24, 2020 at 7:38pm (CST)
  7. Duration: 2.02s, Total samples = 1.92s (94.91%)
  8. Entering interactive mode (type "help" for commands, "o" for options)
  9. (pprof) list NewUseReflect

我们使用pprof得到了该函数的主要耗时,可以发现与我们的猜测无误,耗时主要分为三个部分:reflect.TypeOf(),reflect.New(),value.Field().Set(),其中我们可以把reflect.TypeOf()放到函数外,在初始化的时候生成,接下来我们主要关注value.Fidle().Set()

  1. ROUTINE ======================== begonia.NewUseReflect in /Users/kieranhu/go/src/begonia/reflect_test.go
  2. 60ms 2.17s (flat, cum) 64.97% of Total
  3. . . 29:
  4. 10ms 10ms 30:func NewUseReflect() interface{} {
  5. . . 31: var p People
  6. 10ms 580ms 32: t := reflect.TypeOf(p)
  7. . 440ms 33: v := reflect.New(t)
  8. 10ms 220ms 34: v.Elem().Field(0).Set(reflect.ValueOf(18))
  9. 10ms 250ms 35: v.Elem().Field(1).Set(reflect.ValueOf("shiina"))
  10. . 280ms 36: v.Elem().Field(2).Set(reflect.ValueOf("test1"))
  11. 10ms 220ms 37: v.Elem().Field(3).Set(reflect.ValueOf("test2"))
  12. 10ms 170ms 38: return v.Interface()
  13. . . 39:}
  14. . . 40:
  15. 复制代码

干掉 value.Field().Set()

我们先从怎么不用xxx=xxx进行赋值说起。

unsafe

Go中有一个包叫unsafe,顾名思义,它不安全,因为它可以直接操作内存。我们可以使用unsafe,来对一个字符串进行赋值,具体的步骤大概如下:

  • 获得该字符串的地址
  • 对该地址赋值

我们通过四行就可以完成上面的操作:

  1. str := ""
  2. // 获得该字符串的地址
  3. p := uintptr(unsafe.Pointer(&str))
  4. // 在该地址上赋值
  5. *(*string)(unsafe.Pointer(p))="test"
  6. fmt.Println(str)
  7. -----------------
  8. test

当我们能够使用unsafe来操作内存时,就可以进一步尝试操作结构体了。

操作结构体

我们通过上述代码,得到一个结论:

  • 只要我们知道内存地址,就可以操作任意变量。

接下来我们可以尝试去操作结构体了。
Go的结构体有以下的两个特点:

  • 结构体的成员变量是顺序存储的
  • 结构体第一个成员变量的地址就是该结构体的地址。

根据以上两点,以及刚刚我们得到的结论,我们可能够得到以下的方法,来干掉value.Field().Set()

  • 获得结构体地址
  • 获得结构体内成员变量的偏移量
  • 得到结构体成员变量地址
  • 修改变量值

我们逐个来获得获得。
Gointerface类型是以这样的形式保存的:

  1. // emptyInterface is the header for an interface{} value.
  2. type emptyInterface struct {
  3. typ *rtype
  4. word unsafe.Pointer
  5. }

这个结构体的定义可以在reflect/Value.go找到。
在这个结构体中typ是该interface的具体类型,word指针保存了指向结构体的地址。
现在我们了解了interface的存储类型后,我们只需要将一个空接口interface{}转换为emptyInterface类型,然后得到其中的word,就可以拿到结构体的地址了,即解决了第一步。

结构体类型强转

先用下面这段代码示例,来解决一下不同结构体之间的转换:

  1. type Test1 struct {
  2. Test1 string
  3. }
  4. type Test2 struct {
  5. test2 string
  6. }
  7. func TestStruct(t *testing.T) {
  8. t1 := Test1{
  9. Test1: "hello",
  10. }
  11. t2 := *(*Test2)(unsafe.Pointer(&t1))
  12. fmt.Println(t2)
  13. }
  14. ----------------
  15. {hello}

然后我们更换两个结构体中的成员变量类型,再尝试一下:

  1. type Test1 struct {
  2. a int32
  3. b []byte
  4. }
  5. type Test2 struct {
  6. b int16
  7. a string
  8. }
  9. func TestStruct(t *testing.T) {
  10. t1 := Test1{
  11. a:1,
  12. b:[]byte("asdasd"),
  13. }
  14. t2 := *(*Test2)(unsafe.Pointer(&t1))
  15. fmt.Println(t2)
  16. }
  17. ----------------
  18. {1 asdasd}

我们可以发现,后面这次尝试两个结构体的类型完全不同,但是其中int32和int16的存储方式相同,[]byte和string的存储方式相同,我们可以得出一个简单的结论:

  • 不论类型签名是否相同,只要底层存储方式相同,我们就可以强制转换,并且可以突破私有成员变量限制。

通过上面我们得到的结论,可以将reflect/value.go里面的emptyInterface类型复制出来。然后我们对interface强转并取到word,就可以拿到结构体的地址了。

  1. type emptyInterface struct {
  2. typ *struct{}
  3. word unsafe.Pointer
  4. }
  5. func TestStruct(t *testing.T) {
  6. var in interface{}
  7. in = People{
  8. Age: 18,
  9. Name: "shiina",
  10. Test1: "test1",
  11. Test2: "test2",
  12. }
  13. t2 := uintptr(((*emptyInterface)(unsafe.Pointer(&in))).word)
  14. *(*int)(unsafe.Pointer(t2))=111
  15. fmt.Println(in)
  16. }
  17. ---------------
  18. {111 shiina test1 test2}

我们获取了结构体地址后,根据结构体地址,修改了结构体内第一个成员变量的值,接下来我们开始进行第二步:得到结构体成员变量的偏移量

我们可以通过反射,来轻松的获得每一个成员变量的偏移量,进而根据结构体的地址,获得每一个成员变量的地址。
当我们获得了每一个成员变量的地址后,就可以很轻易的修改它了。

  1. var in interface{}
  2. in = People{
  3. Age: 18,
  4. Name: "shiina",
  5. Test1: "test1",
  6. Test2: "test2",
  7. }
  8. typeP := reflect.TypeOf(in)
  9. offset1 := typeP.Field(1).Offset
  10. offset2 := typeP.Field(2).Offset
  11. offset3 := typeP.Field(3).Offset
  12. t2 := uintptr(((*emptyInterface)(unsafe.Pointer(&in))).word)
  13. *(*int)(unsafe.Pointer(t2)) = 111
  14. *(*string)(unsafe.Pointer(t2 + offset1)) = "hello"
  15. *(*string)(unsafe.Pointer(t2 + offset2)) = "hello1"
  16. *(*string)(unsafe.Pointer(t2 + offset3)) = "hello2"
  17. fmt.Println(in)
  18. ---------------------
  19. {111 hello hello1 hello2}

我们刚刚成功的利用地址修改了结构体的成员变量,没有使用到value.Field().Set()。接下来我们利用刚刚的技巧,修改反射函数,并再次进行性能测试。
我们保留以前的反射函数做对比,新建一个NewQuickReflect()来使用这种技巧创建对象:

  1. var (
  2. offset1 uintptr
  3. offset2 uintptr
  4. offset3 uintptr
  5. p People
  6. t = reflect.TypeOf(p)
  7. )
  8. func init() {
  9. offset1 = t.Field(1).Offset
  10. offset2 = t.Field(2).Offset
  11. offset3 = t.Field(3).Offset
  12. }
  13. type People struct {
  14. Age int
  15. Name string
  16. Test1 string
  17. Test2 string
  18. }
  19. type emptyInterface struct {
  20. typ *struct{}
  21. word unsafe.Pointer
  22. }
  23. func New() *People {
  24. return &People{
  25. Age: 18,
  26. Name: "shiina",
  27. Test1: "test1",
  28. Test2: "test2",
  29. }
  30. }
  31. func NewUseReflect() interface{} {
  32. v := reflect.New(t)
  33. v.Elem().Field(0).Set(reflect.ValueOf(18))
  34. v.Elem().Field(1).Set(reflect.ValueOf("shiina"))
  35. v.Elem().Field(2).Set(reflect.ValueOf("test1"))
  36. v.Elem().Field(3).Set(reflect.ValueOf("test2"))
  37. return v.Interface()
  38. }
  39. func NewQuickReflect() interface{} {
  40. v := reflect.New(t)
  41. p := v.Interface()
  42. ptr0 := uintptr((*emptyInterface)(unsafe.Pointer(&p)).word)
  43. ptr1 := ptr0 + offset1
  44. ptr2 := ptr0 + offset2
  45. ptr3 := ptr0 + offset3
  46. *((*int)(unsafe.Pointer(ptr0))) = 18
  47. *((*string)(unsafe.Pointer(ptr1))) = "shiina"
  48. *((*string)(unsafe.Pointer(ptr2))) = "test1"
  49. *((*string)(unsafe.Pointer(ptr3))) = "test2"
  50. return p
  51. }
  52. func BenchmarkNew(b *testing.B) {
  53. b.ReportAllocs()
  54. b.ResetTimer()
  55. for i := 0; i < b.N; i++ {
  56. New()
  57. }
  58. }
  59. func BenchmarkNewUseReflect(b *testing.B) {
  60. b.ReportAllocs()
  61. b.ResetTimer()
  62. for i := 0; i < b.N; i++ {
  63. NewUseReflect()
  64. }
  65. }
  66. func BenchmarkNewQuickReflect(b *testing.B) {
  67. b.ReportAllocs()
  68. b.ResetTimer()
  69. for i := 0; i < b.N; i++ {
  70. NewQuickReflect()
  71. }
  72. }

运行后我们的测试结果:

  1. BenchmarkNew
  2. BenchmarkNew-16 1000000000 1.34 ns/op 0 B/op 0 allocs/op
  3. BenchmarkNewUseReflect
  4. BenchmarkNewUseReflect-16 3715539 276 ns/op 64 B/op 1 allocs/op
  5. BenchmarkNewQuickReflect
  6. BenchmarkNewQuickReflect-16 12772573 94.7 ns/op 64 B/op 1 allocs/op
  7. 复制代码

可以看出我们的性能从原生205倍提升到了70倍,并且这个优化的程度将会随着结构体成员变量越多而越明显。
我们对新写的NewQuickReflect函数使用pprof分析一下,继续观察有没有可以优化的点。

  1. ROUTINE ======================== begonia.NewQuickReflect in /Users/kieranhu/go/src/begonia/reflect_test.go
  2. 120ms 1.07s (flat, cum) 28.53% of Total
  3. . . 57:
  4. . . 58:func NewQuickReflect() interface{} {
  5. 40ms 800ms 59: v := reflect.New(t)
  6. . . 60:
  7. . 180ms 61: p := v.Interface()
  8. . . 62: ptr0 := uintptr((*emptyInterface)(unsafe.Pointer(&p)).word)
  9. 40ms 40ms 63: ptr1 := ptr0 + offset1
  10. 10ms 10ms 64: ptr2 := ptr0 + offset2
  11. . . 65: ptr3 := ptr0 + offset3
  12. 10ms 10ms 66: *((*int)(unsafe.Pointer(ptr0))) = 18
  13. . 10ms 67: *((*string)(unsafe.Pointer(ptr1))) = "shiina"
  14. . . 68: *((*string)(unsafe.Pointer(ptr2))) = "test1"
  15. . . 69: *((*string)(unsafe.Pointer(ptr3))) = "test2"
  16. 20ms 20ms 70: return p
  17. . . 71:}
  18. . . 72:

我们能够发现最多的损耗花在了reflect.New()上,我们着手尝试对它进行优化。

干掉 reflect.New()

池化

对于改善创建对象耗时来说,最简单的优化方式便是池化,我们利用sync.pool创建一个对象池,并且模拟对象池中资源充足的情况下的性能:

  1. var (
  2. /**
  3. ...........
  4. **/
  5. pool sync.Pool
  6. )
  7. func init() {
  8. /**
  9. ............
  10. **/
  11. pool.New = func() interface{} {
  12. return reflect.New(t)
  13. }
  14. for i := 0; i < 100; i++ {
  15. pool.Put(reflect.New(t).Elem())
  16. }
  17. }
  18. /**
  19. ............
  20. **/
  21. func NewQuickReflectWithPool() interface{} {
  22. p := pool.Get()
  23. ptr0 := uintptr((*emptyInterface)(unsafe.Pointer(&p)).word)
  24. ptr1 := ptr0 + offset1
  25. ptr2 := ptr0 + offset2
  26. ptr3 := ptr0 + offset3
  27. *((*int)(unsafe.Pointer(ptr0))) = 18
  28. *((*string)(unsafe.Pointer(ptr1))) = "shiina"
  29. *((*string)(unsafe.Pointer(ptr2))) = "test1"
  30. *((*string)(unsafe.Pointer(ptr3))) = "test2"
  31. return p
  32. }
  33. func BenchmarkQuickReflectWithPool(b *testing.B) {
  34. b.ReportAllocs()
  35. b.ResetTimer()
  36. for i := 0; i < b.N; i++ {
  37. obj := NewQuickReflectWithPool()
  38. pool.Put(obj)
  39. }
  40. }

在上述这个用例中,我们一拿到这个对象几乎就立即放回了对象池,模拟的是对象池资源充足情况下的性能:

  1. BenchmarkNew
  2. BenchmarkNew-16 1000000000 1.26 ns/op 0 B/op 0 allocs/op
  3. BenchmarkNewUseReflect
  4. BenchmarkNewUseReflect-16 5515128 226 ns/op 64 B/op 1 allocs/op
  5. BenchmarkNewQuickReflect
  6. BenchmarkNewQuickReflect-16 21561645 91.4 ns/op 64 B/op 1 allocs/op
  7. BenchmarkQuickReflectWithPool
  8. BenchmarkQuickReflectWithPool-16 40770750 55.6 ns/op 0 B/op 0 allocs/op
  9. 复制代码

我们可以发现在对象池对象充足的情况下,没有了malloc带来的耗时,我们的性能从原生72倍提升到原生的44倍
但是当对象池不充足情况下,就没有这么可喜的效率了。

另一个思路

我们能够发现现在主要的耗时都在利用反射的创建对象上,这个时候我脑海里有一个思路:
在我们需要的是值类型(例如Person{}),而不是指针的时候(例如&Person)时,我们是不是可以利用Go的这个特性:

  • 值类型传递值而不是指针的时候会进行拷贝

来在使用反射的前提下,利用值传递特性获得一个原生级别对象拷贝?
如果不使用反射,已知类型的情况下会是如下的代码:

  1. func TestStruct(t *testing.T) {
  2. p1 := People{}
  3. var p2 interface{}
  4. p2 = p1
  5. ptr0 := uintptr((*emptyInterface)(unsafe.Pointer(&p2)).word)
  6. ptr1 := ptr0 + offset1
  7. ptr2 := ptr0 + offset2
  8. ptr3 := ptr0 + offset3
  9. *((*int)(unsafe.Pointer(ptr0))) = 18
  10. *((*string)(unsafe.Pointer(ptr1))) = "shiina"
  11. *((*string)(unsafe.Pointer(ptr2))) = "test1"
  12. *((*string)(unsafe.Pointer(ptr3))) = "test2"
  13. fmt.Println(p1)
  14. fmt.Println(p2)
  15. }
  16. ------------------------
  17. {0 }
  18. {18 shiina test1 test2}

我们可以看到,我们使用这样一个值传递的特性,得到了一份p1的拷贝
很可惜的是,当我们不能直接指定类型的时候,想象中这样场景一直实现不了,会直接修改原变量的值,最终我找到了这样的调用方法:

  1. func TestNew(t *testing.T) {
  2. elemValue := reflect.New(reflect.TypeOf(People{})).Elem()
  3. p := elemValue.Interface()
  4. ptr0 := uintptr((*emptyInterface)(unsafe.Pointer(&p)).word)
  5. ptr1 := ptr0 + offset1
  6. ptr2 := ptr0 + offset2
  7. ptr3 := ptr0 + offset3
  8. *((*int)(unsafe.Pointer(ptr0))) = 18
  9. *((*string)(unsafe.Pointer(ptr1))) = "shiina"
  10. *((*string)(unsafe.Pointer(ptr2))) = "test1"
  11. *((*string)(unsafe.Pointer(ptr3))) = "test2"
  12. fmt.Println(p)
  13. fmt.Println(elemValue)
  14. }
  15. -------------------
  16. {18 shiina test1 test2}
  17. {0 }

每次elemValue.Interface()时都会拷贝一个新的对象,这是我们期待的结果,接下来我们将它和之前的池化等一起进行性能测试

  1. BenchmarkNew
  2. BenchmarkNew-16 1000000000 1.83 ns/op 0 B/op 0 allocs/op
  3. BenchmarkNewUseReflect
  4. BenchmarkNewUseReflect-16 2992928 372 ns/op 128 B/op 2 allocs/op
  5. BenchmarkNewQuickReflect
  6. BenchmarkNewQuickReflect-16 12648523 98.7 ns/op 64 B/op 1 allocs/op
  7. BenchmarkQuickReflectWithPool
  8. BenchmarkQuickReflectWithPool-16 40309711 58.2 ns/op 0 B/op 0 allocs/op
  9. BenchmarkNewWithElemReflect
  10. BenchmarkNewWithElemReflect-16 12700314 89.0 ns/op 64 B/op 1 allocs/op

结果比较沮丧,我们仅提升了不到10ns,从53倍提升到48倍并且性能的提升也并不稳定
为此我们阅读reflect.New()elemValue.Interface()源码,发现了如下的片段:

  • reflect.New() ```go func New(typ Type) Value { if typ == nil {
    1. panic("reflect: New(nil)")
    } t := typ.(*rtype) ptr := unsafe_New(t) fl := flag(Ptr) return Value{t.ptrTo(), ptr, fl} }
  1. - elemValue.Interface()
  2. ```go
  3. if v.flag&flagAddr != 0 {
  4. // TODO: pass safe boolean from valueInterface so
  5. // we don't need to copy if safe==true?
  6. c := unsafe_New(t)
  7. typedmemmove(t, c, ptr)
  8. ptr = c
  9. }

reflect.New()的主要耗时都在这个unsafe_New()函数上,然而对于一个elemValueInterface()时,反射还是会调用unsafe_New()函数来创建一个新值。
当多次实验,性能测试之后,发现这种干掉reflect.New()的方式性能不够稳定,基本没有使用的必要。( T_T )

END

如上整个性能优化的从思路到实验,再到实现大概总共花了一周的空闲时间。越写越觉得我不像是在写Go而是在写c了。或许我应该让Go写的更像Go而不是想什么黑魔法来让Go更快(也更不安全)?很感谢需求不饱和让我还有摸鱼时间来研究这个(x

作者:余歌
链接:https://juejin.cn/post/6844904150304555015
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。