一、go語言內(nèi)存布局
想象一下,你有一個如下的結(jié)構(gòu)體。
復(fù)制代碼 代碼如下:
type MyData struct {
aByte byte
aShort int16
anInt32 int32
aSlice []byte
}
那么這個結(jié)構(gòu)體究竟是什么呢? 從根本上說,它描述了如何在內(nèi)存中布局數(shù)據(jù)。 這是什么意思?編譯器又是如何展現(xiàn)出來呢? 我們來看一下。 首先讓我們使用反射來檢查結(jié)構(gòu)中的字段。
二、反射之上
下面是一些使用反射來找出字段大小及其偏移量(它們相對于結(jié)構(gòu)的開始位于內(nèi)存中的位置)的代碼。 反射可以告訴我們編譯器是怎么看待類型(包括結(jié)構(gòu))的。
復(fù)制代碼 代碼如下:
// First ask Go to give us some information about the MyData type
typ := reflect.TypeOf(MyData{})
fmt.Printf("Struct is %d bytes long\n", typ.Size())
// We can run through the fields in the structure in order
n := typ.NumField()
for i := 0; i n; i++ {
field := typ.Field(i)
fmt.Printf("%s at offset %v, size=%d, align=%d\n",
field.Name, field.Offset, field.Type.Size(),
field.Type.Align())
}
除了每個字段的偏移和大小,我還打印了每個字段的對齊方式,我稍后會解釋。結(jié)果如下:
復(fù)制代碼 代碼如下:
Struct is 32 bytes long
aByte at offset 0, size=1, align=1
aShort at offset 2, size=2, align=2
anInt32 at offset 4, size=4, align=4
aSlice at offset 8, size=24, align=8
aByte是我們結(jié)構(gòu)體中的第一個字段,偏移量為0.它使用1字節(jié)的內(nèi)存。
aShort是第二個字段。它使用2字節(jié)的內(nèi)存。奇怪的是偏移量是2。這是為什么呢?答案是對齊, CPU更好地訪問位于2字節(jié)(“2字節(jié)邊界”)的倍數(shù)的地址處的2個字節(jié),并訪問位于4字節(jié)邊界上的4個字節(jié),直到CPU的自然整數(shù)大小,在現(xiàn)代CPU上是8字節(jié)(64位)。
在一些較舊的RISC CPU訪問錯誤對齊的數(shù)字引起一個故障:在一些UNIX系統(tǒng)上,這將是一個SIGBUS,它會停止你的程序(或內(nèi)核)。一些系統(tǒng)能夠處理這些錯誤并修復(fù)錯誤:您的代碼將運行,但會緩慢的運行,因為額外的代碼將由操作系統(tǒng)運行以修復(fù)錯誤。我相信英特爾和ARM的CPU也只是處理芯片上的任何不對齊:也許我們將在以后的文章中測試這一點,以及任何性能的影響。
無論如何,對齊是Go編譯器跳過一個字節(jié)放置字段aShort以便它位于2字節(jié)邊界的原因。因為這樣,我們可以將另一個字段放進結(jié)構(gòu)體中,而不使它占用更大內(nèi)存。這里是我們的結(jié)構(gòu)的新版本,在aByte之后立即有一個新字段anotherByte。
復(fù)制代碼 代碼如下:
type MyData struct {
aByte byte
anotherByte byte
aShort int16
anInt32 int32
aSlice []byte
}
我們再次運行反射代碼,可以看到anotherByte正好在aByte和aShort之間的空閑空間。 它坐落在偏移1,aShort仍然在偏移2.現(xiàn)在可能是時候注意我之前提到的那個神秘對齊字段。 它告訴我們和Go編譯器,這個字段需要如何對齊。
復(fù)制代碼 代碼如下:
Struct is 32 bytes long
aByte at offset 0, size=1, align=1
anotherByte at offset 1, size=1, align=1
aShort at offset 2, size=2, align=2
anInt32 at offset 4, size=4, align=4
aSlice at offset 8, size=24, align=8
三、看看內(nèi)存
然而我們的結(jié)構(gòu)體在內(nèi)存中到底是什么樣子? 讓我們看看我們能不能找到答案。 首先讓我們構(gòu)建一個MyData實例,并填充一些值。我選擇了應(yīng)該容易在內(nèi)存中找到的值。
復(fù)制代碼 代碼如下:
data := MyData{
aByte: 0x1,
aShort: 0x0203,
anInt32: 0x04050607,
aSlice: []byte{
0x08, 0x09, 0x0a,
},
}
現(xiàn)在一些代碼訪問組成這個結(jié)構(gòu)的字節(jié)。 我們想要獲取這個結(jié)構(gòu)的實例,在內(nèi)存中找到它的地址,并打印出該內(nèi)存中的字節(jié)。
我們使用unsafe包來幫助我們這樣做。 這讓我們繞過Go類型系統(tǒng)將指向我們的結(jié)構(gòu)的指針轉(zhuǎn)換為32字節(jié)數(shù)組,這個數(shù)組就是組成我們的結(jié)構(gòu)體的內(nèi)存數(shù)據(jù)。
復(fù)制代碼 代碼如下:
dataBytes := (*[32]byte)(unsafe.Pointer(data))
fmt.Printf("Bytes are %#v\n", dataBytes)
我們運行以上代碼。 這是結(jié)果,第一個字段,aByte,從我們的結(jié)構(gòu)中以粗體顯示。 這是希望你期望的,單字節(jié)aByte = 0x01在偏移0。
復(fù)制代碼 代碼如下:
Bytes are [32]uint8{**0x1**, 0x0, 0x3, 0x2, 0x7, 0x6, 0x5, 0x4, 0x5a, 0x5, 0x1, 0x20, 0xc4, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
接下來我們來看看AShort。 這是在偏移量2的位置并且長度為2.如果你記得,aShort = 0x0203
,但數(shù)據(jù)顯示的字節(jié)是倒序。 這是因為大多數(shù)現(xiàn)代CPU都是Little-Endian:該值的最低位字節(jié)首先出現(xiàn)在內(nèi)存中。
復(fù)制代碼 代碼如下:
Bytes are [32]uint8{0x1, 0x0, **0x3, 0x2**, 0x7, 0x6, 0x5, 0x4, 0x5a, 0x5, 0x1, 0x20, 0xc4, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
同樣的事情發(fā)生在Int32 = 0x04050607。 最低位字節(jié)首先出現(xiàn)在內(nèi)存中。
復(fù)制代碼 代碼如下:
Bytes are [32]uint8{0x1, 0x0, 0x3, 0x2, **0x7, 0x6, 0x5, 0x4**, 0x5a, 0x5, 0x1, 0x20, 0xc4, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
四、神秘的插曲
現(xiàn)在我們看到什么? 這是aSlice = [] byte {0x08,0x09,0x0a}
,在偏移量8的24個字節(jié)。我沒有看到我的序列0x08,0x09,0x0a的任何地方的任何符號。 這是怎么回事?
復(fù)制代碼 代碼如下:
Bytes are [32]uint8{0x1, 0x0, 0x3, 0x2, 0x7, 0x6, 0x5, 0x4, **0x5a, 0x5, 0x1, 0x20, 0xc4, 0x0, 0x0, 0x0, 0x3, 0x0**, **0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0**}
Go反射包里自有答案。 slice在Go語言中由以下結(jié)構(gòu)體表示,該結(jié)構(gòu)從指針數(shù)據(jù)開始,該數(shù)據(jù)指向保存切片中的數(shù)據(jù)的存儲器; 然后是該存儲器中的有用數(shù)據(jù)的長度Len,以及該存儲器的大小Cap。
復(fù)制代碼 代碼如下:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
如果把它提供給我們的代碼,我們得到以下偏移和大小。 數(shù)據(jù)指針和兩個長度各為8個字節(jié),具有8個字節(jié)對齊。
復(fù)制代碼 代碼如下:
Struct is 24 bytes long
Data at offset 0, size=8, align=8
Len at offset 8, size=8, align=8
Cap at offset 16, size=8, align=8
如果我們再看一下后面的內(nèi)存結(jié)構(gòu),我們可以看到數(shù)據(jù)是在地址0x000000c42001055a。 之后,我們看到Len和Cap都是3,這是我們的數(shù)據(jù)的長度。
復(fù)制代碼 代碼如下:
Bytes are [32]uint8{0x1, 0x0, 0x3, 0x2, 0x7, 0x6, 0x5, 0x4, **0x5a, 0x5, 0x1, 0x20, 0xc4, 0x0, 0x0, 0x0**, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
我們可以直接用以下代碼訪問這些數(shù)據(jù)字節(jié)。 首先讓我們直接訪問slice頭,然后打印出數(shù)據(jù)指向的內(nèi)存。
復(fù)制代碼 代碼如下:
dataslice := *(*reflect.SliceHeader)(unsafe.Pointer(data.aSlice))
fmt.Printf("Slice data is %#v\n",
(*[3]byte)(unsafe.Pointer(dataslice.Data)))
這是輸出:
復(fù)制代碼 代碼如下:
Slice data is [3]uint8{0x8, 0x9, 0xa}
總結(jié)
以上就是關(guān)于Go語言內(nèi)存布局的全部內(nèi)容了,希望本文的內(nèi)容對大家學(xué)習(xí)或者使用Go語言能有所幫助,如果有疑問大家可以留言交流。
您可能感興趣的文章:- Go pprof內(nèi)存指標含義備忘錄及案例分析
- golang切片內(nèi)存應(yīng)用技巧詳解
- 解決golang內(nèi)存溢出的方法
- go語言中切片與內(nèi)存復(fù)制 memcpy 的實現(xiàn)操作