詳解Go語言中的內(nèi)存對齊
前言
前面有篇文章我們學(xué)習(xí)了 Go 語言空結(jié)構(gòu)體詳解,最近又在看 unsafe包的知識,在查閱相關(guān)資料時不免會看到內(nèi)存對齊相關(guān)的內(nèi)容,雖然感覺這類知識比較底層,但是看到了卻不深究和渣男有什么區(qū)別?雖然我不會,但我可以學(xué),那么這篇文章,我們就一起來看下什么是內(nèi)存對齊吧!
說明:本文中的測試示例,均是基于Go1.17 64位機器
基礎(chǔ)知識
在Go語言中,我們可以通過 unsafe.Sizeof(x)
來確定一個變量占用的內(nèi)存字節(jié)數(shù)(不包含 x 所指向的內(nèi)容的大?。?。
例如對于字符串?dāng)?shù)組,在64位機器上,unsafe.Sizeof() 返回的任意字符串?dāng)?shù)組大小為 24 字節(jié),和其底層數(shù)據(jù)無關(guān):
func?main()?{ ?s?:=?[]string{"1",?"2",?"3"} ?s2?:=?[]string{"1"} ?fmt.Println(unsafe.Sizeof(s))??//?24 ?fmt.Println(unsafe.Sizeof(s2))?//?24 }
對于Go語言的內(nèi)置類型,占用內(nèi)存大小如下:
類型 | 字節(jié)數(shù) |
---|---|
bool | 1個字節(jié) |
intN, uintN, floatN, complexN | N/8 個字節(jié) (int32 是 4 個字節(jié)) |
int, uint, uintptr | 計算機字長/8 (64位 是 8 個字節(jié)) |
*T, map, func, chan | 計算機字長/8 (64位 是 8 個字節(jié)) |
string (data、len) | 2 * 計算機字長/8 (64位 是 16 個字節(jié)) |
interface (tab、data 或 _type、data) | 2 * 計算機字長/8 (64位 是 16 個字節(jié)) |
[]T (array、len、cap) | 3 * 計算機字長/8 (64位 是 24 個字節(jié)) |
func?main()?{ ?fmt.Println(unsafe.Sizeof(int(1)))??????????????????//?8 ?fmt.Println(unsafe.Sizeof(uintptr(1)))??????????//?8 ?fmt.Println(unsafe.Sizeof(map[string]string{}))????//?8 ?fmt.Println(unsafe.Sizeof(string("")))??????????//?16 ?fmt.Println(unsafe.Sizeof([]string{}))??????????//?24 ?var?a?interface{} ?fmt.Println(unsafe.Sizeof(a))??????????????????//?16 }
看個問題
基于上面的理解,那么對于一個結(jié)構(gòu)體來說,占用內(nèi)存大小就應(yīng)該等于多個基礎(chǔ)類型占用內(nèi)存大小的和,我們就結(jié)合幾個示例來看下:
type?Example?struct?{ ?a?bool?//?1個字節(jié) ?b?int??//?8個字節(jié) ?c?string?//?16個字節(jié) } func?main()?{ ?fmt.Println(unsafe.Sizeof(Example{}))?//?32 }
Example 結(jié)構(gòu)體的三個基礎(chǔ)類型,加起來一個 25字節(jié)
,但是最終輸出的卻是 32字節(jié)
。
我們再看兩個結(jié)構(gòu)體,即使這兩個結(jié)構(gòu)體包含的字段類型一致,但是順序不一致,最終輸出的大小也不一樣:
type?A?struct?{ ?a?int32 ?b?int64 ?c?int32 } type?B?struct?{ ?a?int32 ?b?int32 ?c?int64 } func?main()?{ ?fmt.Println(unsafe.Sizeof(A{}))?//?24 ?fmt.Println(unsafe.Sizeof(B{}))?//?16 }
是什么導(dǎo)致了上述問題的呢,這就引出了我們要看的知識點:內(nèi)存對齊。
什么是內(nèi)存對齊
我們知道,在計算機中訪問一個變量,需要訪問它的內(nèi)存地址,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是:在訪問特定類型變量的時候通常在特定的內(nèi)存地址訪問,這就需要對這些數(shù)據(jù)在內(nèi)存中存放的位置有限制,各種類型數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。
內(nèi)存對齊是編譯器的管轄范圍。表現(xiàn)為:編譯器為程序中的每個“數(shù)據(jù)單元”安排在適當(dāng)?shù)奈恢蒙稀?/p>
為什么需要內(nèi)存對齊
- 有些
CPU
可以訪問任意地址上的任意數(shù)據(jù),而有些CPU
只能在特定地址訪問數(shù)據(jù),因此不同硬件平臺具有差異性,這樣的代碼就不具有移植性,如果在編譯時,將分配的內(nèi)存進行對齊,這就具有平臺可以移植性了。 CPU
訪問內(nèi)存時并不是逐個字節(jié)訪問,而是以字長(word size)為單位訪問,例如 32位的CPU 字長是4字節(jié),64位的是8字節(jié)。如果變量的地址沒有對齊,可能需要多次訪問才能完整讀取到變量內(nèi)容,而對齊后可能就只需要一次內(nèi)存訪問,因此內(nèi)存對齊可以減少CPU訪問內(nèi)存的次數(shù),加大CPU訪問內(nèi)存的吞吐量。
假設(shè)每次訪問的步長為4個字節(jié),如果未經(jīng)過內(nèi)存對齊,獲取b的數(shù)據(jù)需要進行兩次內(nèi)存訪問,最后再進行數(shù)據(jù)整理得到b的完整數(shù)據(jù):
image-20220313230839425
如果經(jīng)過內(nèi)存對齊,一次內(nèi)存訪問就能得到b的完整數(shù)據(jù),減少了一次內(nèi)存訪問:
image-20220313231143302
unsafe.AlignOf()
unsafe.AlignOf(x) 方法的返回值是 m,當(dāng)變量進行內(nèi)存對齊時,需要保證分配到 x 的內(nèi)存地址能夠整除 m
。因此可以通過這個方法,確定變量x 在內(nèi)存對齊時的地址:
- 對于任意類型的變量 x ,
unsafe.Alignof(x)
至少為 1。 - 對于 struct 結(jié)構(gòu)體類型的變量 x,計算 x 每一個字段 f 的
unsafe.Alignof(x.f)
,unsafe.Alignof(x)
等于其中的最大值。 - 對于 array 數(shù)組類型的變量 x,
unsafe.Alignof(x)
等于構(gòu)成數(shù)組的元素類型的對齊倍數(shù)。
對于系統(tǒng)內(nèi)置基礎(chǔ)類型變量 x ,unsafe.Alignof(x)
的返回值就是 min(字長/8,unsafe.Sizeof(x))
,即計算機字長與類型占用內(nèi)存的較小值:
func?main()?{ ??fmt.Println(unsafe.Alignof(int(1)))?//?1?--?min(8,1) ??fmt.Println(unsafe.Alignof(int32(1)))?//?4?--?min?(8,4) ?fmt.Println(unsafe.Alignof(int64(1)))?//?8?--?min?(8,8) ??fmt.Println(unsafe.Alignof(complex128(1)))?//?8?--?min(8,16) }??
內(nèi)存對齊規(guī)則
我們講內(nèi)存對齊,就是把變量放在特定的地址,那么如何計算特定地址呢,這就涉及到內(nèi)存對齊規(guī)則:
成員對齊規(guī)則
針對一個基礎(chǔ)類型變量,如果 unsafe.AlignOf()
返回的值是 m,那么該變量的地址需要 被m整除
(如果當(dāng)前地址不能整除,填充空白字節(jié),直至可以整除)。
整體對齊規(guī)則
針對一個結(jié)構(gòu)體,如果 unsafe.AlignOf()
返回值是 m,需要保證該結(jié)構(gòu)體整體內(nèi)存占用是 m的整數(shù)倍
,如果當(dāng)前不是整數(shù)倍,需要在后面填充空白字節(jié)。
通過內(nèi)存對齊后,就可以保證在訪問一個變量地址時:
- 如果該變量占用內(nèi)存小于字長:保證一次訪問就能得到數(shù)據(jù);
- 如果該變量占用內(nèi)存大于字長:保證第一次內(nèi)存訪問的首地址,是該變量的首地址。
舉個例子
例1:
type?A?struct?{ ?a?int32 ?b?int64 ?c?int32 } func?main()?{ ?fmt.Println(unsafe.Sizeof(A{1,?1,?1}))??//?24 }
1.第一個字段是 int32 類型,unsafe.Sizeof(int32(1))=4,內(nèi)存占用為4個字節(jié),同時unsafe.Alignof(int32(1)) = 4,內(nèi)存對齊需保證變量首地址可以被4整除,我們假設(shè)地址從0開始,0可以被4整除:
成員變量1內(nèi)存對齊
2.第二個字段是 int64 類型,unsafe.Sizeof(int64(1)) = 8,內(nèi)存占用為 8 個字節(jié),同時unsafe.Alignof(int64(1)) = 8,需保證變量放置首地址可以被8整除,當(dāng)前地址為4,距離4最近的且可以被8整除的地址為8,因此需要添加四個空白字節(jié),從8開始放置:
成員變量2內(nèi)存對齊
3.第三個字段是 int32 類型,unsafe.Sizeof(int32(1))=4,內(nèi)存占用為4個字節(jié),同時unsafe.Alignof(int32(1)) = 4,內(nèi)存對齊需保證變量首地址可以被4整除,當(dāng)前地址為16,16可以被4整除:
成員變量3內(nèi)存對齊
4.所有成員對齊都已經(jīng)完成,現(xiàn)在我們需要看一下整體對齊規(guī)則:unsafe.Alignof(A{}) = 8,即三個變量成員的最大值,內(nèi)存對齊需要保證該結(jié)構(gòu)體的內(nèi)存占用是 8 的整數(shù)倍,當(dāng)前內(nèi)存占用是 20個字節(jié),因此需要再補充4個字節(jié):
整體對齊
5.最終該結(jié)構(gòu)體的內(nèi)存占用為 24字節(jié)。
例二:
type?B?struct?{ ?a?int32 ?b?int32 ?c?int64 } func?main()?{ ?fmt.Println(unsafe.Sizeof(B{1,?1,?1}))??//?16 }
1.第一個字段是 int32 類型,unsafe.Sizeof(int32(1))=4,內(nèi)存占用為4個字節(jié),同時unsafe.Alignof(int32(1)) = 4,內(nèi)存對齊需保證變量首地址可以被4整除,我們假設(shè)地址從0開始,0可以被4整除:
成員變量1內(nèi)存對齊
2.第二個字段是 int32 類型,unsafe.Sizeof(int32(1))=4,內(nèi)存占用為4個字節(jié),同時unsafe.Alignof(int32(1)) = 4,內(nèi)存對齊需保證變量首地址可以被4整除,當(dāng)前地址為4,4可以被4整除:
成員變量2內(nèi)存對齊
3.第三個字段是 int64 類型,unsafe.Sizeof(int64(1))=8,內(nèi)存占用為8個字節(jié),同時unsafe.Alignof(int64(1)) = 8,內(nèi)存對齊需保證變量首地址可以被8整除,當(dāng)前地址為8,8可以被8整除:
成員變量3內(nèi)存對齊
4.所有成員對齊都已經(jīng)完成,現(xiàn)在我們需要看一下整體對齊規(guī)則:unsafe.Alignof(B{}) = 8,即三個變量成員的最大值,內(nèi)存對齊需要保證該結(jié)構(gòu)體的內(nèi)存占用是 8 的整數(shù)倍,當(dāng)前內(nèi)存占用是 16個字節(jié),已經(jīng)符合規(guī)則,最終該結(jié)構(gòu)體的內(nèi)存占用為 16個字節(jié)。
空結(jié)構(gòu)體的對齊規(guī)則
如果空結(jié)構(gòu)體作為結(jié)構(gòu)體的內(nèi)置字段:當(dāng)變量位于結(jié)構(gòu)體的前面和中間時,不會占用內(nèi)存;當(dāng)該變量位于結(jié)構(gòu)體的末尾位置時,需要進行內(nèi)存對齊,內(nèi)存占用大小和前一個變量的大小保持一致。
type?C?struct?{ ?a?struct{} ?b?int64 ?c?int64 } type?D?struct?{ ?a?int64 ?b?struct{} ?c?int64 } type?E?struct?{ ?a?int64 ?b?int64 ?c?struct{} } type?F?struct?{ ?a?int32 ?b?int32 ?c?struct{} } func?main()?{ ?fmt.Println(unsafe.Sizeof(C{}))?//?16 ?fmt.Println(unsafe.Sizeof(D{}))?//?16 ?fmt.Println(unsafe.Sizeof(E{}))?//?24 ??fmt.Println(unsafe.Sizeof(F{}))?//?12 }
總結(jié)
本篇文章我們一起學(xué)習(xí)了Go 語言中的內(nèi)存對齊,主要內(nèi)容如下:
- unsafe.Sizeof(x) 返回了變量x的內(nèi)存占用大小
- 兩個結(jié)構(gòu)體,即使包含變量類型的數(shù)量相同,但是位置不同,占用的內(nèi)存大小也不同,由此引出了內(nèi)存對齊
- 內(nèi)存對齊包含成員對齊和整體對齊,與 unsafe.AlignOf(x) 息息相關(guān)
- 空結(jié)構(gòu)體作為成員變量時,是否占用內(nèi)存和所處位置有關(guān)
- 在實際開發(fā)中,我們可以通過調(diào)整變量位置,優(yōu)化內(nèi)存占用(一般按照變量內(nèi)存大小順序排列,整體占用內(nèi)存更小)
以上就是詳解Go語言中的內(nèi)存對齊的詳細內(nèi)容,更多關(guān)于Go語言內(nèi)存對齊的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang?四則運算計算器yacc歸約手寫實現(xiàn)
這篇文章主要為大家介紹了golang?四則運算?計算器?yacc?歸約的手寫實現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07Golang使用Gin框架實現(xiàn)路由分類處理請求流程詳解
Gin是一個golang的微框架,封裝比較優(yōu)雅,具有快速靈活,容錯方便等特點,這篇文章主要介紹了Golang使用Gin框架實現(xiàn)路由分類處理請求,感興趣的同學(xué)可以參考下文2023-05-05Golang服務(wù)的請求調(diào)度的實現(xiàn)
Golang服務(wù)請求調(diào)度是一種使用Go語言實現(xiàn)的服務(wù)請求管理方法,本文主要介紹了Golang服務(wù)的請求調(diào)度的實現(xiàn),具有一定的參考價值,感興趣的可以了解一下2023-08-08