Skip to content

Golang 反射性能优化

转载:https://zhuanlan.zhihu.com/p/138777955

Golang的反射最为人诟病的就是它极差的性能,接下来我们尝试优化它的性能。

如果我们使用正常的流程来创建一个对象,将会是如下的代码片段:

go
type People struct {
    Age   int
    Name  string
}

func New() *People {
    return &People{
        Age:   18,
        Name:  "shiina",
    }
}

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

go
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"))
    return v.Interface()
}

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

简单的性能测试

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

go
func BenchmarkNew(b *testing.B) {
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        New()
    }
}

func BenchmarkNewUseReflect(b *testing.B) {
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        NewUseReflect()
    }
}

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

bash
BenchmarkNew
BenchmarkNew-16                 1000000000           1.55 ns/op        0 B/op          0 allocs/op
BenchmarkNewUseReflect
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{
        Age:  18,
        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
  • 无成员变量:
go
type People struct{}

func New() interface{} {
    return &People{}
}

func NewUseReflect() interface{} {
    var p People
    t := reflect.TypeOf(p)
    v := reflect.New(t)
    return v.Interface()
}

——————————————————————————————————————————
BenchmarkNew
BenchmarkNew-16                 1000000000           1.32 ns/op        0 B/op          0 allocs/op
BenchmarkNewUseReflect
BenchmarkNewUseReflect-16       17362648            62.3 ns/op         0 B/op          0 allocs/op

我们猜测,反射性能的损耗具体分为两个部分,一个部分是reflect.New(),另一个部分是value.Field().Set()

这时候我们可以使用Go原生自带的性能分析工具pprof来分析一下它们的主要耗时,来验证我们的猜测。

我们对四个成员变量测试用例使用pprof

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

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

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

干掉 value.Field().Set()

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

unsafe

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

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

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

go
    str := ""
    // 获得该字符串的地址
    p := uintptr(unsafe.Pointer(&str))
    // 在该地址上赋值
    *(*string)(unsafe.Pointer(p))="test"
    fmt.Println(str)
-----------------
test

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

操作结构体

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

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

接下来我们可以尝试去操作结构体了。

Go的结构体有以下的两个特点:

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

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

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

我们逐个来获得获得。

Gointerface类型是以这样的形式保存的:

go
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

这个结构体的定义可以在reflect/Value.go找到。

在这个结构体中typ是该interface的具体类型,word指针保存了指向结构体的地址。

现在我们了解了interface的存储类型后,我们只需要将一个空接口interface{}转换为emptyInterface类型,然后得到其中的word,就可以拿到结构体的地址了,即解决了第一步。

结构体类型强转

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

go
type Test1 struct {
    Test1 string
}

type Test2 struct {
    test2 string
}

func TestStruct(t *testing.T) {
    t1 := Test1{
        Test1: "hello",
    }

    t2 := *(*Test2)(unsafe.Pointer(&t1))
    fmt.Println(t2)
}
----------------
{hello}

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

go
type Test1 struct {
    a int32
    b []byte
}

type Test2 struct {
    b int16
    a string
}

func TestStruct(t *testing.T) {
    t1 := Test1{
        a:1,
        b:[]byte("asdasd"),
    }

    t2 := *(*Test2)(unsafe.Pointer(&t1))
    fmt.Println(t2)
}
----------------
{1 asdasd}

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

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

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

go
type emptyInterface struct {
    typ  *struct{}
    word unsafe.Pointer
}

func TestStruct(t *testing.T) {
    var in interface{}
    in = People{
        Age:   18,
        Name:  "shiina",
        Test1: "test1",
        Test2: "test2",
    }

    t2 := uintptr(((*emptyInterface)(unsafe.Pointer(&in))).word)
    *(*int)(unsafe.Pointer(t2))=111
    fmt.Println(in)
}
---------------
{111 shiina test1 test2}

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

我们可以通过反射,来轻松的获得每一个成员变量的偏移量,进而根据结构体的地址,获得每一个成员变量的地址。

当我们获得了每一个成员变量的地址后,就可以很轻易的修改它了。

go
var in interface{}
    in = People{
        Age:   18,
        Name:  "shiina",
        Test1: "test1",
        Test2: "test2",
    }

    typeP := reflect.TypeOf(in)
    offset1 := typeP.Field(1).Offset
    offset2 := typeP.Field(2).Offset
    offset3 := typeP.Field(3).Offset

    t2 := uintptr(((*emptyInterface)(unsafe.Pointer(&in))).word)

    *(*int)(unsafe.Pointer(t2)) = 111
    *(*string)(unsafe.Pointer(t2 + offset1)) = "hello"
    *(*string)(unsafe.Pointer(t2 + offset2)) = "hello1"
    *(*string)(unsafe.Pointer(t2 + offset3)) = "hello2"
    fmt.Println(in)
---------------------
{111 hello hello1 hello2}

我们刚刚成功的利用地址修改了结构体的成员变量,没有使用到value.Field().Set()。接下来我们利用刚刚的技巧,修改反射函数,并再次进行性能测试。

我们保留以前的反射函数做对比,新建一个NewQuickReflect()来使用这种技巧创建对象:

go
var (
    offset1 uintptr
    offset2 uintptr
    offset3 uintptr
    p       People
    t       = reflect.TypeOf(p)
)

func init() {
    offset1 = t.Field(1).Offset
    offset2 = t.Field(2).Offset
    offset3 = t.Field(3).Offset
}

type People struct {
    Age   int
    Name  string
    Test1 string
    Test2 string
}

type emptyInterface struct {
    typ  *struct{}
    word unsafe.Pointer
}

func New() *People {
    return &People{
        Age:  18,
        Name: "shiina",
    Test1: "test1",
        Test2: "test2",
    }
}

func NewUseReflect() interface{} {
    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()
}

func NewQuickReflect() interface{} {
    v := reflect.New(t)

    p := v.Interface()
    ptr0 := uintptr((*emptyInterface)(unsafe.Pointer(&p)).word)
    ptr1 := ptr0 + offset1
    ptr2 := ptr0 + offset2
    ptr3 := ptr0 + offset3
    *((*int)(unsafe.Pointer(ptr0))) = 18
    *((*string)(unsafe.Pointer(ptr1))) = "shiina"
    *((*string)(unsafe.Pointer(ptr2))) = "test1"
    *((*string)(unsafe.Pointer(ptr3))) = "test2"
    return p
}

func BenchmarkNew(b *testing.B) {
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        New()
    }
}

func BenchmarkNewUseReflect(b *testing.B) {
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        NewUseReflect()
    }
}

func BenchmarkNewQuickReflect(b *testing.B) {
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        NewQuickReflect()
    }
}

运行后我们的测试结果:

bash
BenchmarkNew
BenchmarkNew-16                 1000000000           1.34 ns/op        0 B/op          0 allocs/op
BenchmarkNewUseReflect
BenchmarkNewUseReflect-16        3715539           276 ns/op          64 B/op          1 allocs/op
BenchmarkNewQuickReflect
BenchmarkNewQuickReflect-16     12772573            94.7 ns/op        64 B/op          1 allocs/op

可以看出我们的性能从原生205倍提升到了70倍,并且这个优化的程度将会随着结构体成员变量越多而越明显。

我们对新写的NewQuickReflect函数使用pprof分析一下,继续观察有没有可以优化的点。

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

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

干掉 reflect.New()

池化

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

go
var (
  /**
  ...........
  **/
  pool sync.Pool
)
func init() {
  /**
  ............
  **/
    pool.New = func() interface{} {
        return reflect.New(t)
    }
    for i := 0; i < 100; i++ {
        pool.Put(reflect.New(t).Elem())
    }
}

/**
  ............
  **/

func NewQuickReflectWithPool() interface{} {
    p := pool.Get()

    ptr0 := uintptr((*emptyInterface)(unsafe.Pointer(&p)).word)
    ptr1 := ptr0 + offset1
    ptr2 := ptr0 + offset2
    ptr3 := ptr0 + offset3

    *((*int)(unsafe.Pointer(ptr0))) = 18
    *((*string)(unsafe.Pointer(ptr1))) = "shiina"
    *((*string)(unsafe.Pointer(ptr2))) = "test1"
    *((*string)(unsafe.Pointer(ptr3))) = "test2"
    return p
}

func BenchmarkQuickReflectWithPool(b *testing.B) {
    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        obj := NewQuickReflectWithPool()
        pool.Put(obj)
    }
}

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

bash
BenchmarkNew
BenchmarkNew-16                         1000000000           1.26 ns/op        0 B/op          0 allocs/op
BenchmarkNewUseReflect
BenchmarkNewUseReflect-16                5515128           226 ns/op          64 B/op          1 allocs/op
BenchmarkNewQuickReflect
BenchmarkNewQuickReflect-16             21561645            91.4 ns/op        64 B/op          1 allocs/op
BenchmarkQuickReflectWithPool
BenchmarkQuickReflectWithPool-16        40770750            55.6 ns/op         0 B/op          0 allocs/op

我们可以发现在对象池对象充足的情况下,没有了malloc带来的耗时,我们的性能从原生72倍提升到原生的44倍

但是当对象池不充足情况下,就没有这么可喜的效率了。

另一个思路

我们能够发现现在主要的耗时都在利用反射的创建对象上,这个时候我脑海里有一个思路:

在我们需要的是值类型(例如Person{}),而不是指针的时候(例如&Person)时,我们是不是可以利用Go的这个特性:

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

来在使用反射的前提下,利用值传递特性获得一个原生级别对象拷贝?

如果不使用反射,已知类型的情况下会是如下的代码:

go
func TestStruct(t *testing.T) {
    p1 := People{}

    var p2 interface{}
    p2 = p1

    ptr0 := uintptr((*emptyInterface)(unsafe.Pointer(&p2)).word)
    ptr1 := ptr0 + offset1
    ptr2 := ptr0 + offset2
    ptr3 := ptr0 + offset3

    *((*int)(unsafe.Pointer(ptr0))) = 18
    *((*string)(unsafe.Pointer(ptr1))) = "shiina"
    *((*string)(unsafe.Pointer(ptr2))) = "test1"
    *((*string)(unsafe.Pointer(ptr3))) = "test2"

    fmt.Println(p1)
    fmt.Println(p2)
}
------------------------
{0   }
{18 shiina test1 test2}

我们可以看到,我们使用这样一个值传递的特性,得到了一份p1的拷贝

很可惜的是,当我们不能直接指定类型的时候,想象中这样场景一直实现不了,会直接修改原变量的值,最终我找到了这样的调用方法:

go
func TestNew(t *testing.T) {
    elemValue := reflect.New(reflect.TypeOf(People{})).Elem()
    p := elemValue.Interface()

    ptr0 := uintptr((*emptyInterface)(unsafe.Pointer(&p)).word)
    ptr1 := ptr0 + offset1
    ptr2 := ptr0 + offset2
    ptr3 := ptr0 + offset3

    *((*int)(unsafe.Pointer(ptr0))) = 18
    *((*string)(unsafe.Pointer(ptr1))) = "shiina"
    *((*string)(unsafe.Pointer(ptr2))) = "test1"
    *((*string)(unsafe.Pointer(ptr3))) = "test2"

    fmt.Println(p)
    fmt.Println(elemValue)
}
-------------------
{18 shiina test1 test2}
{0   }

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

bash
BenchmarkNew
BenchmarkNew-16                         1000000000           1.83 ns/op        0 B/op          0 allocs/op
BenchmarkNewUseReflect
BenchmarkNewUseReflect-16                2992928           372 ns/op         128 B/op          2 allocs/op
BenchmarkNewQuickReflect
BenchmarkNewQuickReflect-16             12648523            98.7 ns/op        64 B/op          1 allocs/op
BenchmarkQuickReflectWithPool
BenchmarkQuickReflectWithPool-16        40309711            58.2 ns/op         0 B/op          0 allocs/op
BenchmarkNewWithElemReflect
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 {
		panic("reflect: New(nil)")
	}
	t := typ.(*rtype)
	ptr := unsafe_New(t)
	fl := flag(Ptr)
	return Value{t.ptrTo(), ptr, fl}
}
  • elemValue.Interface()
go
if v.flag&flagAddr != 0 {
   // TODO: pass safe boolean from valueInterface so
   // we don't need to copy if safe==true?
   c := unsafe_New(t)
   typedmemmove(t, c, ptr)
   ptr = c
}

reflect.New()的主要耗时都在这个unsafe_New()函数上,然而对于一个elemValueInterface()时,反射还是会调用unsafe_New()函数来创建一个新值。

当多次实验,性能测试之后,发现这种干掉reflect.New()的方式性能不够稳定,基本没有使用的必要。( T_T )

END

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

Golang 反射性能优化 - 对象创建

在上文中,我们从一个普通的反射调用说起,尝试使用unsafe来优化了结构体的赋值操作,并尝试使用池等方式来优化结构体的创建。

思路分析

在上一篇文章的最后,我们提出了一个思路:

  • 值类型传递值时会进行拷贝

我们可以尝试通过这个特性来获得一个原生级别的对象拷贝,再对新值中的成员变量进行赋值,来获得一个近似原生级别的对象创建。

我们可以创建一个简单的函数来展示这一点特性:

go
  myCopy := func(p People) People {
    return p
  }
  
  p1 := People{
    Age: 18,
  }

  p2 := myCopy(p1)
  p2.Age = 28

  fmt.Println(p1.Age, p2.Age)
------------------------
18 28

在泛型(Go 1.18)发布之前,是无法实现一个通用的上述myCopy()函数的。

尽管interface{}可以接受任何类型的值,如上文所说,interface{}的本质是一个储存了类型和值指针的结构体。所以,一切在interface{}上的操作都会操作指向的值本身。

操作interface{}会操作指向的值,但是在下面这个样例中,会发现对interface{}的修改并没有作用在原值上:

go
  p1 := People{
    Age: 18,
  }
  var in interface{} = p1

  *(*int)((*emptyInterface)(unsafe.Pointer(&in)).word) = 28

  fmt.Println(p1, in)
------------------------
{18   } {28   }

这是因为在第四行:

go
  var in interface{} = p1

在这里的时候,传递了值,所以对结构体进行了拷贝。

我们可以打印结构体和interface{}中的值的地址,来比较它们的地址是否相同,下面有两个样例,一个是地址相同,另一个是不相同,区别仅仅是入参传递的变量不同:

go
  myCopy := func(p interface{}) interface{} {
    return p
  }

  p1 := People{
    Age: 18,
  }

  // case 1
  p2 := myCopy(p1)

  // case 2
  var in interface{} = p1
  p3 := myCopy(in)

  fmt.Println(unsafe.Pointer(&p1))
  fmt.Println((*emptyInterface)(unsafe.Pointer(&p2)).word)
  fmt.Println((*emptyInterface)(unsafe.Pointer(&in)).word)
  fmt.Println((*emptyInterface)(unsafe.Pointer(&p3)).word)
------------------------
0xc000076800
0xc000076840
0xc000076880
0xc000076880

case1中,我们传入的值类型为People,Go 自动做了case2中的People类型到interface{}类型的转换。而在转换中如上文所述,进行了值的拷贝。故可以看到p1p2的地址不同,inp3的地址不同。

接下来我们使用泛型来实现myCopy函数。

原理与实现

在我们讲实现之前,我需要先讲一下在Go中泛型的原理。

泛型原理

泛型大多由这两种方式之一实现:

  • 虚函数表 在此方法中是为每种类型创建一个表,该类型的指针实例可以通过这个表来查询到类型下函数或方法的地址。 C++和Java中的多态是以此方式实现的。Java中的泛型也是以此来实现。
  • 单态化 单态化指为每种用到的类型创建一组函数副本。例如在Go的sort库中,sdk为常用的基本类型分别创建了SortXxx()函数。 C++的泛型(模板)是以此方式实现的。

在Go中,官方综合了两种方式,对于值类型使用单态化的方式实现,对于指针类型和接口类型则使用虚函数表的方式实现。

干掉 unsafe_new()

在上一篇文章中最后我们分析到:在反射中去除成员变量赋值的损耗之外,性能损耗完全来自unsafe_new() ,而泛型对值类型使用单态化实现,那么就意味着我们可以调用一个泛型函数,在函数中传递一个值。在传递时Go会拷贝该值,我们将其作为返回值,最终获得了某一个类型的新值。

Go中另一个特性是当我们创建一个变量时,如果该变量是值类型,则自动为其赋空值。于是我们可以给函数的返回值命名,直接修改返回值变量的值。这样的好处是我们操作的值完全为一个新的值,传入值中成员变量的值不会造成影响。

以下是实现的样例,我们实现了两个函数,一个是获得某个类型的值,另一个是获得某一个类型的新指针:

go
// 在这里依旧需要入参传入一个值,用该值来指定变量的类型。
// 如果入参没有该参数,那么我们无法通过变量来创建新值,
// NewGen(p) ✅
// NewGen[p.(type)]() ❌ we can't do it
func NewGen[T any](T) (rst T) {
  *(*int)(unsafe.Pointer(&rst)) = 18
  *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&rst)) + offset1)) = "shiina"
  *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&rst)) + offset2)) = "test1"
  *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&rst)) + offset3)) = "test2"
  return rst
}

func NewGenPointer[T any](T) (rst *T) {
  var t T
  // 这里实际上可以直接用NewGen的,但是为了让两个函数的结果在下面的调用中有区分度,所以这里给age换了值。
  *(*int)(unsafe.Pointer(&t)) = 28
  *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&t)) + offset1)) = "shiina"
  *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&t)) + offset2)) = "test1"
  *(*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&t)) + offset3)) = "test2"
  return &t
}

func main() {
  p1 := People{Age: 8}
  p2 := NewGen(p1)
  p3 := NewGenPointer(p1)

  fmt.Println(p1)
  fmt.Println(p2)
  fmt.Println(p3)
}
------------------------
{8   }
{18 shiina test1 test2}
&{28 shiina test1 test2}

这下我们成功通过泛型,利用Go特性替代了unsafe_New()。接下来我们对我们的函数进行性能测试。

Benchmark

新创建两个函数,对两个新函数进行测试:

(其余的测试函数可以在上一篇文章找到)

go
func BenchmarkNewGen(b *testing.B) {
  b.ReportAllocs()
  b.ResetTimer()
  for i := 0; i < b.N; i++ {
    NewGen(p)
  }
}

func BenchmarkNewGenPointer(b *testing.B) {
  b.ReportAllocs()
  b.ResetTimer()
  for i := 0; i < b.N; i++ {
    NewGen(p)
  }
}

运行之后,结果如我们所愿:

go
BenchmarkNew
BenchmarkNew-12                 1000000000           0.2839 ns/op        0 B/op        0 allocs/op
BenchmarkNewUseReflect
BenchmarkNewUseReflect-12        8814205         135.6 ns/op        64 B/op        1 allocs/op
BenchmarkNewQuickReflect
BenchmarkNewQuickReflect-12     22986240          57.19 ns/op       64 B/op        1 allocs/op
BenchmarkNewGen
BenchmarkNewGen-12              1000000000           0.2968 ns/op        0 B/op        0 allocs/op
BenchmarkNewGenPointer
BenchmarkNewGenPointer-12       1000000000           0.2943 ns/op        0 B/op        0 allocs/op

我们可以看到,新函数与原生的性能相似,我们完美达到了我们的目的。

END

工作以后没有多少时间写文章,写自己的东西了。(其实也不是工作太忙,而是需求太不饱和,太容易摸鱼,并且生活中快乐的事情太多,想不起来写代码写文章。)泛型已经发布了有一段时间了,但是我还没有实际用上。在前两天突然有了灵感,想起来了这一点,于是去做了一些尝试,并且发现成功了,才补上了这一篇文章。

诚然,如果要让反射系统用上泛型来优化,需要做相当多的改动,并且大部分的系统的瓶颈也不会在反射上,这一篇文章也只是在以玩玩具的心态,边研究Go的内存模型边做的。

写代码真好玩。

文章来源于自己总结和网络转载,内容如有任何问题,请大佬斧正!联系我