浅窥关于golang reflect获取interface值的性能问题以及用interface传递参的变量逃逸问题

在使用interface作为参数的API时,其灵活的特性着实给我们带来了不少方便,其功能的实现主要依赖于go的标准库reflect的value与type两种类型以及相关的一系列方法。然而最近在博客上看到了这样的说法:

通过reflect.ValueOf(interface)获取object值的速度非常之慢

由于想要获取interface各field的值是绝大部分interface参数型api所需要做的一件事,所以这句话引起了我的兴趣,之后便开始在go的官方包里一探究竟。

可以看到reflect.Value类型的实现如下:

type Value struct {
	typ  *rtype						//数据的类型信息
	ptr  unsafe.Pointer				//数据本身或指向数据的指针
	flag uintptr					//标识信息,类似常用的配置位
}

使用ValueOf获取将interface拆解后重组装成的Value变量

func ValueOf(i interface{}) Value {
	if i == nil {
		return Value{}
	}
	escapes(i)

	return unpackEface(i)
}

这其中有一个escapes(i)需要注意一下,因为go的绝大部分内存都是由自身管理的,采用逃逸检查技术可以让内存尽可能地分配到栈上,这样做的代价会比使用全局堆分配来的更低。下面有对这个escapes进行的一部分分析,不感兴趣的同学可以先点此跳过,,,
CO↑CO↓

Note: some of the noescape annotations below are technically a lie,
// but safe in the context of this package. Functions like chansend
// and mapassign don’t escape the referent, but may escape anything
// the referent points to (they do shallow copies of the referent).
// It is safe in this package because the referent may only point
// to something a Value may point to, and that is always in the heap
// (due to the escapes() call in ValueOf).

大意://go:noescape在技术上实际是存在问题的,但是能做到在reflect自身包内能够安全使用(因为一个引用只可能指向一个Value所指向的值(对函数内各个变量的引用只使用了浅拷贝),不会造成意外的panic,这是由于在ValueOf方法中使用了escapes,所以顶层变量被分配到了堆上)

我们通过golang的编译器检查(添加编译参数-gcflags “-m -l(可选)”),并写一段小代码来模拟一下这个escapes所做的工作:

package main

import (
	"unsafe"
)

type nsp struct {
	data byte
}

func main() {
	test := nsp{}
	println(dummy.x)
	println(&dummy.x)
	println()
	
	println(unsafe.Pointer(&test))
	println()
	escapes(test)
	
	println()
	println(dummy.x)
	println(&dummy.x)
}

//go:noinline
func escapes(x interface{}) {
	println(&x)
	println(x)
	////////////////////////////
	//if dummy.b {
	//	dummy.x = x
	//}
	///////////////////////////
	escapes2(x)
}

func escapes2(xx interface{}) {
	println(&xx)
	println(xx)
}

var dummy struct {
	b bool
	x interface{}
}

build message:当赋值语句存在时,x成为泄露参数,test逃逸到堆;反之则出现三个函数内的变量都不逃逸

全加注释
.\main.go:37:15: escapes2 xx does not escape
.\main.go:25:14: escapes x does not escape
.\main.go:18:9: main test does not escape

解放赋值语句
.\main.go:37:15: escapes2 xx does not escape
.\main.go:25:14: leaking param: x
.\main.go:18:9: test escapes to heap

e s c a p e i f \color{red}{让我们分别去掉escape中的整个if部分,再恢复其中的赋值部分,再全部恢复,分别运行查看结果}

(0x0,0x0)
0x4c65c8

0xc00003ff3e

0xc00003ff28
(0x467080,0xc00003ff3f) //实际测试这个指针可以转化到上层堆栈的变量
0xc00003ff08
(0x467080,0xc00003ff3f)

(0x0,0x0)
0x4c65c8
(0x0,0x0)
0x4c65c8

0xc00003ff3e

0xc00003ff18
(0x467080,0xc00000a038) //有了赋值语句后,拷贝的可能性出现,此时变量已经逃逸
0xc00003fef8
(0x467080,0xc00000a038)

(0x467080,0xc00000a038)
0x4c65c8
(0x0,0x0)
0x4c65c8

0xc00003ff3e

0xc00003ff18
(0x467080,0xc00000a038)
0xc00003fef8
(0x467080,0xc00000a038)

(0x0,0x0)
0x4c65c8

使 \color{red}{可以看到,让外部变量获取对值引用是能否使变量逃逸的关键}


e s c a p e s t e s t 114 \color{blue}{下面修改一下函数escapes的内容,test的初值设定为114}

//go:noinline
func escapes(x interface{}) {
	println()
	println(&x)
	println(x)
	////////////////////////////
	//if dummy.b {
		dummy.x = x
	//}
	///////////////////////////
	escapes2(x)
	
	println("下面尝试使用unsafe访问eface中储存的dataPointer所指向的地址取值, 请输入interfaceData[1]中的数值:")
	
	var addr uintptr
	_, _ = fmt.Scanln(&addr)
	
	println(addr)
	println((*(*nsp)(unsafe.Pointer(addr))).data)
}

运行结果:

(0x0,0x0)
0x56e898

0xc000089f3e

0xc000089f18
(0x4b2d80,0xc00000a0c8)
0xc000089eb8
(0x4b2d80,0xc00000a0c8)

下面尝试使用unsafe访问eface中储存的dataPointer所指向的地址取值:
0xc00000a0c8
824633761992
114
0x56e898
0xc000089f3e

访 ! \color{lightgreen}{内存访问没有问题,应该是成功在堆上生成了这个类型的拷贝!}

综上所述,unsafe包中的这个escapes函数通过无法到达的代码欺骗了编译器,让其产生了interface值可能被拷贝,其中的内存会被外部引用的错觉,,巧妙且安全的达成了目的!!!(至于汇编层面的证明,留到后日吧())

综上,当一个函数的形参为 interface 类型时,在编译阶段编译器无法确定其具体的类型,如果这个值在内层函数中向外抛出则会因此产生逃逸,最终使得传入前的参数分配到堆上,看来go的interface饱受各位大手子诟病的龟速就是来源于类型断定的诸多鸡毛事以及会导致内存分配在堆上,各种层面降低了缓存命中率…

言归正传

根据上面的分析,我们发现如果结构体是以interface的形式传递进来的,但是经验告诉我们,在返回新构建的结构体时,reflect中的函数会在构造Value类型变量时进行malloc,当此方法多次使用后,效率自然会变得低下。而在使用type时只需要查找索引,便可用之前博客中的宏方式操作指针得到成员。所以我们尝试采用reflect中的Type类型的offset方法,结合结构指针计算,利用unsafe包转化的方式来进行对结构中值的访问。

m a p \color{red}{似乎对map类型还没有找到合适的解决方法}

基于我之前的一篇博客,我们可以使用偏移量+结构体基地址来访问结构体中的成员,由于go对OO的实现方式比较独特,私有成员最大的用处就是防止包外直接调用,所以在保证内存本身安全不被篡改的情况下获取想要的部分基本是没有问题的。

go对interface的实现分为两种,一种是没有成员函数表的空interface——eface,相反的则称为iface

type eface struct {
	_type *_type
	data  unsafe.Pointer
}
type iface struct {
	tab  *itab
	data unsafe.Pointer
}

可以看到,存在相同类型的成员(指向data的指针)而itab中也包含了实现了该接口所有方法的struct类型的type指针

type itab struct {
	inter *interfacetype
	_type *_type
	hash  uint32 // copy of _type.hash. Used for type switches.
	_     [4]byte
	fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

操作struct类型

\color{blue}{准备代码如下:}

package main

import (
	"reflect"
	"unsafe"
)

type nsp struct {
	data byte
}

func main() {
	test := nsp{114}
	println(test.data)
	
	api(test)
	
	println(test.data)
}

// 模拟EmptyInterface实现
type eFace struct {
	_type *struct{}
	_data unsafe.Pointer
}

func api(itf interface{}) {
	// 获取指向结构体的指针
	structAddr := (*eFace)(unsafe.Pointer(&itf))._data
	
	// 按变量名称得到需要获取的field信息(主要是为了获得offset)
	// 这里取出来的 field 对象是 reflect.StructField 类型
	// 但是它没有办法用来取得对应对象上的值
	// 如果要取值,得用另外一套对object,而不是type的反射
	field, _   := reflect.TypeOf(itf)/*.Elem()*/.FieldByName("data")
	// 此处的Elem()返回一个类型的元素类型,但是仅限数组,管道,映射,指针或切片
	// 否则会引起panic。我们这里用到的是struct,所以不需要调用Elem()
	
	// 获取该成员field的指针
	fieldAddr  := uintptr(structAddr) + field.Offset
	
	// 得到变量或者对其进行操作
	*(*byte)(unsafe.Pointer(fieldAddr)) = 111
}

在这里插入图片描述
在这里插入图片描述
运行发现,虽然函数内itf的data被更改了,但是main.main中的test值没有发生变化。如果我们想要修改main中的值,将传入的值变为(&test),并恢复.Elem()的调用即可。

func main() {
	test := nsp{114}
	println(test.data)
	
	api(&test)
	
	println(test.data)
}
func api(itf interface{}) {
	structAddr := (*eFace)(unsafe.Pointer(&itf))._data
	field, _   := reflect.TypeOf(itf).Elem().FieldByName("data")
	fieldAddr  := uintptr(structAddr) + field.Offset
	*(*byte)(unsafe.Pointer(fieldAddr)) = 111
}

在这里插入图片描述

操作slice类型

package main

import (
	"fmt"
	"reflect"
	"unsafe"
)

type sliceHeader struct {
	// 模拟切片头类型
	_data unsafe.Pointer
	_len  int
	_cap  int
}

func main() {
	test := []int{114, 514}
	fmt.Println(test)
	
	api(test)
	
	fmt.Println(test)
}

// 模拟EmptyInterface实现
type eFace struct {
	_type *struct{}
	_data unsafe.Pointer
}

func api(itf interface{}) {
	fmt.Println(reflect.TypeOf(itf))
	
	// 获取指向切片头的指针
	headerAddr  := (*eFace)(unsafe.Pointer(&itf))._data
	// 底层数组指针
	arrayAddr   := (*sliceHeader)(headerAddr)._data
	// 数组长度指针
	_len        := (*sliceHeader)(headerAddr)._len
	fmt.Println(arrayAddr, _len)
	
	
	// 获取切片内元素的类型
	elementType := reflect.TypeOf(itf).Elem()
	fmt.Println(elementType)
	
	firstElemAddr   := uintptr(arrayAddr)
	secondElemAddr  := uintptr(arrayAddr) + elementType.Size()
	
	*(*int)(unsafe.Pointer(firstElemAddr))  = 114514
	*(*int)(unsafe.Pointer(secondElemAddr)) = 1919
	// 这里可以用类型断言操作
}

操作成功
在这里插入图片描述

参考:Jsoniter-golang

发布了24 篇原创文章 · 获赞 14 · 访问量 6141

猜你喜欢

转载自blog.csdn.net/qq_35587463/article/details/104221280