1. 程式人生 > 其它 >go 如何讓在強制轉換型別時不發生記憶體拷貝?

go 如何讓在強制轉換型別時不發生記憶體拷貝?

當你使用要對一個變數從一個型別強制轉換成另一個型別,其實都會發生記憶體的拷貝,而這種拷貝會對效能有所影響的,因此如果可以在轉換的時候避免記憶體的拷貝就好了。

慶幸的是,在一些特定的型別下,這種想法確實是可以實現的。

比如將字串轉成 []byte 型別。

正常的轉換方法是

//stringto[]byte
s1:="hello"
b:=[]byte(s1)

//[]bytetostring
s2:=string(b)

具體的程式碼如下

funcmain(){
msg1:="hello"
sh:=*(*reflect.StringHeader)(unsafe.Pointer(&msg1))
bh:=reflect.SliceHeader{
Data:sh.Data,
Len:sh.Len,
Cap:sh.Len,
}
msg2:=*(*[]byte)(unsafe.Pointer(&bh))
fmt.Printf("%v",msg2)
}

這段程式碼是不是看著有點暈啊,各種奇奇怪怪的寫法,見都沒見過。

其實核心知識點有三個:

  1. 一種定義變數的怪異方法

  2. 字串的底層資料結構

  3. 切片的底層資料結構

正常我們所熟知的變數的宣告定義方法是下面兩種吧

//第一種
varnamestring="Go程式設計時光"

//第二種
name:="Go程式設計時光"

但還有一種方法,可能新手不知道,這種方法,我在之前的文章有提到過 詳細圖解:靜態型別與動態型別

還是用上面的等價例子,它還可以這麼寫

name:=(string)("Go程式設計時光")

再回過頭來理解最上面那段怪異的程式碼

  • 第一個括號:肯定是某個型別對應的指標型別

  • 第二個括號:就是第一個括號裡型別對應的值

tmp:=*(*reflect.StringHeader)(unsafe.Pointer(&msg))

由於第一個括號裡是個指標型別,那麼第二個括號裡肯定要是指標的值。

而通過unsafe.Pointer就可以將&msg指標的記憶體地址取出來。

兩個括號合起來就是,宣告並定義了一個 *reflect.StringHeader型別的指標變數,對應的指標值還是原來 msg1 的記憶體地址。

那最前面的的那個那個*,大家應該都知道,是從*reflect.StringHeader型別的指標變數中取出值。

那麼你肯定要問了,int 和 bool、string 這些型別我都知道啊,這個reflect.StringHeader 是什麼型別??沒見過啊

其實他就是字串的底層結構,是字串最原始的樣子。

typeStringHeaderstruct{
Datauintptr
Lenint
}

同樣的,SliceHeader則是切片的底層資料結構

typeSliceHeaderstruct{
Datauintptr
Lenint
Capint
}

是不是覺得他們很像?

對咯,只要把StringHeader裡的 Data 塞給SliceHeader裡的 Data,再把SliceHeader裡的 Len 塞給SliceHeader裡的 Len 和 Cap ,就多費任何的空間創造出一個新的變數。

bh:=reflect.SliceHeader{
Data:sh.Data,
Len:sh.Len,
Cap:sh.Len,
}

最後再把SliceHeader通過上面的強制轉換方法,再轉成[]byte就可以了,中間就不會有任何的記憶體拷貝的過程。

是不是真的有效果呢?來測試一下效能便知

先準備 demo.go

packagemain

import(
"reflect"
"unsafe"
)

funcString2Bytes(sstring)[]byte{
sh:=(*reflect.StringHeader)(unsafe.Pointer(&s))
bh:=reflect.SliceHeader{
Data:sh.Data,
Len:sh.Len,
Cap:sh.Len,
}
return*(*[]byte)(unsafe.Pointer(&bh))
}

再準備demo_test.go

packagemain

import(
"bytes"
"testing"
)

funcTestString2Bytes(t*testing.T){
x:="HelloGopher!"
y:=String2Bytes(x)
z:=[]byte(x)

if!bytes.Equal(y,z){
t.Fail()
}
}


//測試標準轉換[]byte效能
funcBenchmark_NormalString2Bytes(b*testing.B){
x:="HelloGopher!HelloGopher!HelloGopher!"
fori:=0;i<b.N;i++{
_=[]byte(x)
}
}

//測試強轉換string到[]byte效能
funcBenchmark_String2Bytes(b*testing.B){
x:="HelloGopher!HelloGopher!HelloGopher!"
fori:=0;i<b.N;i++{
_=String2Bytes(x)
}
}

並在當前目錄下執行

gomodinit

最後就可以執行如下命令進行測試,從輸出的結果來看使用我們的黑魔法轉換的效率要比普通的方法快太多了

$gotest-bench="."-benchmem
goos:darwin
goarch:amd64
pkg:demo
Benchmark_NormalString2Bytes-83659667428.5ns/op48B/op1allocs/op
Benchmark_String2Bytes-810000000000.253ns/op0B/op0allocs/op

是不是很簡單呢?

本系列的所有文章,我都開放到 Github 上:https://github.com/iswbm/golang-interview

這個號沒有留言功能呢,如果文章有寫得不對的地方,可以去那裡提交 issue 幫我指正。順便可以幫我點個小 ⭐⭐,在那裡我對題庫進行了分類整理,方便索引查詢。

加油噢,我們下篇見!