淺窺關於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

escapeif\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}{可以看到,讓外部變量獲取對值引用是能否使變量逃逸的關鍵}


escapestest114\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包轉化的方式來進行對結構中值的訪問。

map\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

發佈了19 篇原創文章 · 獲贊 7 · 訪問量 5151
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章