一文帶你了解Go中的內(nèi)存對(duì)齊
前言
在一次工作中,需要使用 Go 調(diào)用 DLL 文件,其中就涉及到內(nèi)存對(duì)齊的相關(guān)知識(shí),如果自定義的結(jié)構(gòu)體內(nèi)存布局和所調(diào)用的 DLL 結(jié)構(gòu)體內(nèi)存布局不一致,就會(huì)無(wú)法正確調(diào)用。所以,一旦涉及到較為底層的編程,特別是與硬件交互,內(nèi)存對(duì)齊是一個(gè)必修的課題。
基礎(chǔ)知識(shí)
在正式了解內(nèi)存對(duì)齊前,我們先來(lái)看一個(gè)方法 unsafe.Sizeof(),它可以獲取任意一個(gè)變量占據(jù)的內(nèi)存大小,即這個(gè)變量在內(nèi)存中所占據(jù)的字節(jié)數(shù)。Go 內(nèi)置的變量類(lèi)型占據(jù)內(nèi)存大小情況如下:
| 類(lèi)型 | 字節(jié)數(shù) |
|---|---|
| bool | 1 |
| string | 2 * 計(jì)算機(jī)字長(zhǎng)/8 (64位16個(gè)字節(jié),32位8個(gè)字節(jié)) |
| int、uint、uintptr | 計(jì)算機(jī)字長(zhǎng)/8 (64位8個(gè)字節(jié),32位4個(gè)字節(jié)) |
| *T, map, func, chan | 計(jì)算機(jī)字長(zhǎng)/8 (64位8個(gè)字節(jié),32位4個(gè)字節(jié)) |
| intN, uintN, floatN, complexN | N/8個(gè)字節(jié)(int32是4個(gè)字節(jié),float64是8個(gè)字節(jié)) |
| interface | 2 * 計(jì)算機(jī)字長(zhǎng)/8 (64位16個(gè)字節(jié),32位8個(gè)字節(jié)) |
| []T | 3 * 計(jì)算機(jī)字長(zhǎng)/8 (64位24個(gè)字節(jié),32位12個(gè)字節(jié)) |
對(duì)于切片類(lèi)型而言,字節(jié)數(shù)是固定的24字節(jié)或者12字節(jié)(32位系統(tǒng)上),而對(duì)于數(shù)組類(lèi)型而言,它的大小是元素?cái)?shù)量 * 元素類(lèi)型字節(jié)數(shù):
var (
slice []int8
array [3]int8
)
fmt.Printf("切片:%v\n", unsafe.Sizeof(slice))
fmt.Printf("數(shù)組:%v\n", unsafe.Sizeof(array))
// 結(jié)果
切片:24
數(shù)組:3對(duì)于復(fù)合結(jié)構(gòu),也就是結(jié)構(gòu)體,其情況就會(huì)變的復(fù)雜起來(lái):
type MemStruct struct {
b bool // 1
i8 int8 // 1
i16 int16 // 2
i32 int32 // 4
}
type MemStruct2 struct {
b bool // 1
i8 int8 // 1
i32 int32 // 4
}
func TestStruct(t *testing.T) {
fmt.Println("MemStruct:", unsafe.Sizeof(MemStruct{}))
fmt.Println("MemStruct2:", unsafe.Sizeof(MemStruct2{}))
}
// 結(jié)果
MemStruct: 8
MemStruct2: 8嗯,看完代碼,是不是發(fā)現(xiàn)了哪里不對(duì),MemStruct 和 MemStruct2 兩個(gè)結(jié)構(gòu)體的 SizeOf 值居然是一樣的?MemStruct 還好理解, 1 + 1 + 2 + 4 = 8 當(dāng)然沒(méi)問(wèn)題,那 MemStruct2 是怎么回事?這個(gè)問(wèn)題我們暫且按下不表, 先來(lái)看一下 CPU 是怎么訪問(wèn)內(nèi)存的。
CPU 訪問(wèn)內(nèi)存
CPU 訪問(wèn)內(nèi)存時(shí),并不是逐個(gè)字節(jié)訪問(wèn),而是按照字長(zhǎng)位單位訪問(wèn),比如 32 位系統(tǒng),CPU 一次性讀取 4 字節(jié),64位則一次性讀取 8 字節(jié)。對(duì)于上文的 MemStruct2 結(jié)構(gòu)體,假使它在內(nèi)存中是這樣的(實(shí)際上不是,這里是為了方便理解):

那么,此時(shí)我們以 4 字長(zhǎng)來(lái)讀取 Int32 變量,CPU 就必須要讀取兩次內(nèi)存,然后將兩次讀取的結(jié)果進(jìn)行整理,最終得到完整的數(shù)據(jù):

讀取一個(gè)變量需要訪問(wèn)兩次內(nèi)存訪問(wèn)?這既不優(yōu)雅也不高效,而且不利于變量操作的原子性。那么,Go 編譯器是怎么解決這個(gè)問(wèn)題呢,可能你也想到了,我們把結(jié)構(gòu)體內(nèi)存調(diào)整一下,在 Int8 之后填充兩個(gè)空字節(jié):

經(jīng)過(guò)調(diào)整,我們就可以一次性的讀取出 Int32。這種調(diào)整方式有一個(gè)響亮的名字——內(nèi)存對(duì)齊。內(nèi)存對(duì)齊是 Go 編譯器來(lái)完成的,它對(duì)于程序員是透明的。我們的 MemStruct2 結(jié)構(gòu)體之所以會(huì)多出 2 個(gè)字節(jié),正是因?yàn)閮?nèi)存對(duì)齊的原因。
為什么需要內(nèi)存對(duì)齊
如上文所說(shuō),內(nèi)存對(duì)齊可以保障變量被 CPU 一次性的讀取出來(lái),這可以減少CPU訪問(wèn)內(nèi)存的次數(shù),加大CPU訪問(wèn)內(nèi)存的吞吐量。一次性的讀取變量,也保證變量的原子操作性。除此之外,有些硬件平臺(tái)不支持訪問(wèn)任意地址的任意數(shù)據(jù),如果不進(jìn)行內(nèi)存對(duì)齊,編程語(yǔ)言就喪失了平臺(tái)可移植性。內(nèi)存對(duì)齊賦予了編程語(yǔ)言的可移植性。
當(dāng)然,內(nèi)存對(duì)齊也有一些缺點(diǎn):
- 因?yàn)闀?huì)置空一些內(nèi)存,所以會(huì)造成一定量的內(nèi)存浪費(fèi);
- 會(huì)增加編譯器的復(fù)雜度,編譯器需要根據(jù)不同的平臺(tái)和指令集來(lái)確定合適的對(duì)齊方式,并且需要處理一些特殊的情況,比如位域、聯(lián)合體、指針等。
對(duì)齊保證
unsafe.Alignof(x) 返回一個(gè)類(lèi)型的對(duì)齊系數(shù),對(duì)于 Go 的基礎(chǔ)類(lèi)型來(lái)說(shuō),這個(gè)值會(huì)取 計(jì)算機(jī)字長(zhǎng)/8 和 unsafe.Sizeof(x) 中較小的一個(gè)值。即 min(計(jì)算機(jī)字長(zhǎng)/8,unsafe.Sizeof(x)):
func TestAlignOf(t *testing.T) {
var (
s string
i8 int8
)
fmt.Printf("string sizeof:%v, alignof: %v\n", unsafe.Sizeof(s), unsafe.Alignof(s)) // min(8, 1) = 1
fmt.Printf("int8 sizeof:%v, alignof: %v\n", unsafe.Sizeof(i8), unsafe.Alignof(i8)) // min(8, 16) = 16
}對(duì)于 64 位操作系統(tǒng)來(lái)說(shuō),計(jì)算機(jī)字長(zhǎng)64/8 = 8。int8 的 SizeOf 是 1,與 8 對(duì)比較小,所以 int8 的對(duì)齊系數(shù)就是1;string 的 SizeOf 是16,與 8 相比較大,所以 string 的對(duì)齊系數(shù)就是 8。
對(duì)于數(shù)組和結(jié)構(gòu)體類(lèi)型來(lái)說(shuō),情況則有些特殊。在 The Go Programming Language Specification 一文中提到了三點(diǎn),其中后兩點(diǎn)說(shuō)的就是結(jié)構(gòu)體和數(shù)組:
- For a variable x of any type: unsafe.Alignof(x) is at least 1.
- For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1.
- For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array's element type.
這段話翻譯過(guò)來(lái)就是:
- 對(duì)于任意類(lèi)型的變量 x ,unsafe.Alignof(x) 至少為 1;
- 對(duì)于 struct 結(jié)構(gòu)體類(lèi)型的變量 x,計(jì)算 x 每一個(gè)字段 f 的 unsafe.Alignof(x.f),unsafe.Alignof(x) 等于其中的最大值;
- 對(duì)于 array 數(shù)組類(lèi)型的變量 x,unsafe.Alignof(x) 等于構(gòu)成數(shù)組的元素類(lèi)型的對(duì)齊倍數(shù)。
第一點(diǎn)容易理解,沒(méi)有哪個(gè)類(lèi)型對(duì)齊系數(shù)會(huì)小于1的,不然那不就亂套了嗎。第二點(diǎn)的意思就是說(shuō)對(duì)于任意結(jié)構(gòu)體而言,它的對(duì)齊系數(shù)會(huì)等于它所包含字段中對(duì)齊系數(shù)最大的那一個(gè):
type MemStruct struct {
b bool // alignof: 1
i8 int8 // alignof: 1
i32 int32 // alignof: 4
s string // alignof: 8
}
func TestStruct(t *testing.T) {
fmt.Printf("MemStruct的對(duì)齊系數(shù):{%v}, 等于string的對(duì)齊系數(shù):{%v}", unsafe.Alignof(MemStruct{}), unsafe.Alignof(string("1")))
}
// 結(jié)果
MemStruct的對(duì)齊系數(shù):{8}, 等于string的對(duì)齊系數(shù):{8}第三點(diǎn)就是說(shuō)對(duì)于數(shù)組類(lèi)型而言,它的對(duì)齊系數(shù)等于它構(gòu)成元素類(lèi)型的對(duì)齊系數(shù):
func TestArray(t *testing.T) {
var (
it interface{}
arr [3]interface{}
)
fmt.Printf("數(shù)組interface{}的對(duì)齊系數(shù):{%v}, 等于interface的對(duì)齊系數(shù):{%v}", unsafe.Alignof(arr), unsafe.Alignof(it))
}
// 結(jié)果
數(shù)組interface{}的對(duì)齊系數(shù):{8}, 等于interface的對(duì)齊系數(shù):{8}以上兩個(gè)例子的基于 64 位操作系統(tǒng)。
結(jié)構(gòu)體對(duì)齊技巧
合理的布局可以減少內(nèi)存浪費(fèi),假使我們現(xiàn)在有一個(gè)結(jié)構(gòu)體有 int8、int16、int32 三個(gè)字段,那么這三個(gè)字段在結(jié)構(gòu)體的順序會(huì)影響結(jié)構(gòu)體的內(nèi)存占用嗎?我們來(lái)看一個(gè)例子:
type S1 struct {
i8 int8
i16 int16
i32 int32
}
type S2 struct {
i8 int8
i32 int32
i16 int16
}
func TestLeastMem(t *testing.T) {
fmt.Printf("S1的占用: %v\n", unsafe.Sizeof(S1{}))
fmt.Printf("S2的占用: %v\n", unsafe.Sizeof(S2{}))
}
// 結(jié)果
S1的占用: 8
S2的占用: 12可以看到,S1 明顯占用的內(nèi)存更少。讓我們來(lái)分析一下,S1 和 S2 的對(duì)齊系數(shù)都等于其子字段 int16 的對(duì)齊系數(shù),也就是 4。對(duì)于S1,經(jīng)過(guò)內(nèi)存對(duì)齊,它們?cè)趦?nèi)存中的布局是這樣的:

對(duì)于 S1:
- i8 是第一個(gè)字段,默認(rèn)已經(jīng)對(duì)齊,從 0 開(kāi)始占據(jù) 1 個(gè)字節(jié);
- i16 是第二個(gè)字段,對(duì)齊系數(shù)為 2,因此,必須填充 1 個(gè)字節(jié),其偏移量才是 2 的倍數(shù),從 2 開(kāi)始占據(jù) 2 字節(jié);
- i32 是第三個(gè)字段,對(duì)齊系數(shù)為 4,此時(shí),內(nèi)存已經(jīng)是對(duì)齊的,從第 4 開(kāi)始占據(jù) 4 字節(jié)即可;
因此 S1 在內(nèi)存占用了 8 個(gè)字節(jié),浪費(fèi)了 1 個(gè)字節(jié)。
對(duì)于 S2:
- i8 是第一個(gè)字段,默認(rèn)已經(jīng)對(duì)齊,從 0 開(kāi)始占據(jù) 1 個(gè)字節(jié);
- i32 是第二個(gè)字段,對(duì)齊系數(shù)為 4,因此,必須填充 3 個(gè)字節(jié),其偏移量才是 4 的倍數(shù),從第 4 開(kāi)始占據(jù) 4 字節(jié);
- i16 是第三個(gè)字段,對(duì)齊系數(shù)為 2,此時(shí),內(nèi)存已經(jīng)是對(duì)齊的,從第 8 開(kāi)始占據(jù) 2 字節(jié)即可。
因此 S2 在內(nèi)存占用了 12 個(gè)字節(jié),浪費(fèi)了 5 個(gè)字節(jié)。
空結(jié)構(gòu)體對(duì)齊保證
對(duì)于空結(jié)構(gòu)體,其 Sizeof 為0,Alignof 是 1,一般其作為其他結(jié)構(gòu)體字段時(shí),不需要內(nèi)存對(duì)齊,但有一種情況除外:在結(jié)構(gòu)體末尾。為什么呢?我們先要知道一件事情:因?yàn)榭战Y(jié)構(gòu)體的 Size 為0,所以編譯器會(huì)把 zerobase 的地址分配出去,這體現(xiàn)在 src/runtime/malloc.go 878 行中(Go 1.20.4):
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
}zerobase 是 Go 定義的一個(gè) uintptr 的特殊全局變量,占據(jù) 8 個(gè)字節(jié)。因?yàn)樗锌战涌隗w的地址都指向 zerobase,所以所有空結(jié)構(gòu)體的內(nèi)存地址都是一樣的!這樣做就可以使所有的空結(jié)構(gòu)體有一個(gè)獨(dú)一無(wú)二的內(nèi)存地址,不與 nil 混淆,而且多個(gè)空結(jié)構(gòu)體不會(huì)占用額外的內(nèi)存。空結(jié)構(gòu)體有內(nèi)存地址卻不占用內(nèi)存,這個(gè)概念很重要!
有了這個(gè)概念,我們就比較容易理解為什么空結(jié)構(gòu)體在末尾需要內(nèi)存對(duì)齊了。當(dāng)空結(jié)構(gòu)體類(lèi)型作為結(jié)構(gòu)體的最后一個(gè)字段時(shí),如果有指向該字段的指針,那么就會(huì)返回該結(jié)構(gòu)體之外的地址,導(dǎo)致內(nèi)存泄露。為了避免這種情況就需要進(jìn)行一次內(nèi)存對(duì)齊,且內(nèi)存占用大小和前一個(gè)變量的大小保持一致:
type emptyStruct struct{}
type S1 struct {
empty emptyStruct
i8 int8
}
type S2 struct {
i8 int8
empty emptyStruct
}
type S3 struct {
i16 int16
empty emptyStruct
}
type S4 struct {
i16 int16
i8 int8
empty emptyStruct
}
func TestSpaceStructMem(t *testing.T) {
fmt.Printf("S1的占用: %v\n", unsafe.Sizeof(S1{}))
fmt.Printf("S2的占用: %v\n", unsafe.Sizeof(S2{}))
fmt.Printf("S3的占用: %v\n", unsafe.Sizeof(S3{}))
fmt.Printf("S4的占用: %v\n", unsafe.Sizeof(S4{}))
// S3 空結(jié)構(gòu)從第二位開(kāi)始,往后補(bǔ)充兩個(gè)字節(jié)
fmt.Printf("S3的空結(jié)構(gòu)體偏移量: %v\n", unsafe.Offsetof(S3{}.empty))
// S4 空結(jié)構(gòu)從第三位開(kāi)始,往后補(bǔ)充一個(gè)字節(jié)
fmt.Printf("S4的空結(jié)構(gòu)體偏移量: %v\n", unsafe.Offsetof(S4{}.empty))
}
// 結(jié)果
S1的占用: 1
S2的占用: 2
S3的占用: 4
S4的占用: 4
S3的空結(jié)構(gòu)體偏移量: 2
S4的空結(jié)構(gòu)體偏移量: 3以上就是一文帶你了解Go中的內(nèi)存對(duì)齊的詳細(xì)內(nèi)容,更多關(guān)于go內(nèi)存對(duì)齊的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang基于Vault實(shí)現(xiàn)敏感信息保護(hù)
Vault?是一個(gè)強(qiáng)大的敏感信息管理工具,自帶了多種認(rèn)證引擎和密碼引擎,本文主要探討應(yīng)用程序如何安全地從?Vault?獲取敏感信息,并進(jìn)一步實(shí)現(xiàn)自動(dòng)輪轉(zhuǎn),感興趣的可以了解一下2023-06-06
Go實(shí)現(xiàn)MD5加密的三種方法小結(jié)
本文主要介紹了Go實(shí)現(xiàn)MD5加密的三種方法小結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03
基于go interface{}==nil 的幾種坑及原理分析
這篇文章主要介紹了基于go interface{}==nil 的幾種坑及原理分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-04-04
詳解如何用Golang處理每分鐘100萬(wàn)個(gè)請(qǐng)求
在項(xiàng)目開(kāi)發(fā)中,我們常常會(huì)遇到處理來(lái)自數(shù)百萬(wàn)個(gè)端點(diǎn)的大量POST請(qǐng)求,本文主要介紹了Golang實(shí)現(xiàn)處理每分鐘100萬(wàn)個(gè)請(qǐng)求的方法,希望對(duì)大家有所幫助2023-04-04
淺析Go項(xiàng)目中的依賴(lài)包管理與Go?Module常規(guī)操作
這篇文章主要為大家詳細(xì)介紹了Go項(xiàng)目中的依賴(lài)包管理與Go?Module常規(guī)操作,文中的示例代碼講解詳細(xì),對(duì)我們深入了解Go語(yǔ)言有一定的幫助,需要的可以跟隨小編一起學(xué)習(xí)一下2023-10-10
VSCode1.4 搭建Golang的開(kāi)發(fā)調(diào)試環(huán)境(遇到很多問(wèn)題)
這篇文章主要介紹了VSCode1.4 搭建Golang的開(kāi)發(fā)調(diào)試環(huán)境(遇到很多問(wèn)題),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04

