Go语言内存对齐详解
你必須非常努力,才能看起來(lái)毫不費(fèi)力!
微信搜索公眾號(hào)[ 漫漫Coding路 ],一起From Zero To Hero !
前言
前面有篇文章我們學(xué)習(xí)了 Go語(yǔ)言空結(jié)構(gòu)體,最近又在看 unsafe包 的知識(shí),在查閱相關(guān)資料時(shí)不免會(huì)看到內(nèi)存對(duì)齊相關(guān)的內(nèi)容,雖然感覺(jué)這類(lèi)知識(shí)比較底層,但是看到了卻不深究和渣男有什么區(qū)別?雖然我不會(huì),但我可以學(xué)🐶,那么這篇文章,我們就一起來(lái)看下什么是內(nèi)存對(duì)齊吧!
說(shuō)明:本文中的測(cè)試示例,均是基于Go1.17 64位機(jī)器
基礎(chǔ)知識(shí)
在Go語(yǔ)言中,我們可以通過(guò) unsafe.Sizeof(x) 來(lái)確定一個(gè)變量占用的內(nèi)存字節(jié)數(shù)(不包含 x 所指向的內(nèi)容的大小)。
例如對(duì)于字符串?dāng)?shù)組,在64位機(jī)器上,unsafe.Sizeof() 返回的任意字符串?dāng)?shù)組大小為 24 字節(jié),和其底層數(shù)據(jù)無(wú)關(guān):
func main() {s := []string{"1", "2", "3"}s2 := []string{"1"}fmt.Println(unsafe.Sizeof(s)) // 24fmt.Println(unsafe.Sizeof(s2)) // 24 }對(duì)于Go語(yǔ)言的內(nèi)置類(lèi)型,占用內(nèi)存大小如下:
| bool | 1個(gè)字節(jié) |
| intN, uintN, floatN, complexN | N/8 個(gè)字節(jié) (int32 是 4 個(gè)字節(jié)) |
| int, uint, uintptr | 計(jì)算機(jī)字長(zhǎng)/8 (64位 是 8 個(gè)字節(jié)) |
| *T, map, func, chan | 計(jì)算機(jī)字長(zhǎng)/8 (64位 是 8 個(gè)字節(jié)) |
| string (data、len) | 2 * 計(jì)算機(jī)字長(zhǎng)/8 (64位 是 16 個(gè)字節(jié)) |
| interface (tab、data 或 _type、data) | 2 * 計(jì)算機(jī)字長(zhǎng)/8 (64位 是 16 個(gè)字節(jié)) |
| []T (array、len、cap) | 3 * 計(jì)算機(jī)字長(zhǎng)/8 (64位 是 24 個(gè)字節(jié)) |
看個(gè)問(wèn)題
基于上面的理解,那么對(duì)于一個(gè)結(jié)構(gòu)體來(lái)說(shuō),占用內(nèi)存大小就應(yīng)該等于多個(gè)基礎(chǔ)類(lèi)型占用內(nèi)存大小的和,我們就結(jié)合幾個(gè)示例來(lái)看下:
type Example struct {a bool // 1個(gè)字節(jié)b int // 8個(gè)字節(jié)c string // 16個(gè)字節(jié) }func main() {fmt.Println(unsafe.Sizeof(Example{})) // 32 }Example 結(jié)構(gòu)體的三個(gè)基礎(chǔ)類(lèi)型,加起來(lái)一個(gè) 25字節(jié),但是最終輸出的卻是 32字節(jié)。
我們?cè)倏磧蓚€(gè)結(jié)構(gòu)體,即使這兩個(gè)結(jié)構(gòu)體包含的字段類(lèi)型一致,但是順序不一致,最終輸出的大小也不一樣:
type A struct {a int32b int64c int32 }type B struct {a int32b int32c int64 }func main() {fmt.Println(unsafe.Sizeof(A{})) // 24fmt.Println(unsafe.Sizeof(B{})) // 16 }是什么導(dǎo)致了上述問(wèn)題的呢,這就引出了我們要看的知識(shí)點(diǎn):內(nèi)存對(duì)齊。
什么是內(nèi)存對(duì)齊
我們知道,在計(jì)算機(jī)中訪問(wèn)一個(gè)變量,需要訪問(wèn)它的內(nèi)存地址,從理論上講似乎對(duì)任何類(lèi)型的變量的訪問(wèn)可以從任何地址開(kāi)始,但實(shí)際情況是:在訪問(wèn)特定類(lèi)型變量的時(shí)候通常在特定的內(nèi)存地址訪問(wèn),這就需要對(duì)這些數(shù)據(jù)在內(nèi)存中存放的位置有限制,各種類(lèi)型數(shù)據(jù)按照一定的規(guī)則在空間上排列,而不是順序的一個(gè)接一個(gè)的排放,這就是對(duì)齊。
內(nèi)存對(duì)齊是編譯器的管轄范圍。表現(xiàn)為:編譯器為程序中的每個(gè)“數(shù)據(jù)單元”安排在適當(dāng)?shù)奈恢蒙稀?/p>
為什么需要內(nèi)存對(duì)齊
有些CPU可以訪問(wèn)任意地址上的任意數(shù)據(jù),而有些CPU只能在特定地址訪問(wèn)數(shù)據(jù),因此不同硬件平臺(tái)具有差異性,這樣的代碼就不具有移植性,如果在編譯時(shí),將分配的內(nèi)存進(jìn)行對(duì)齊,這就具有平臺(tái)可以移植性了。
CPU 訪問(wèn)內(nèi)存時(shí)并不是逐個(gè)字節(jié)訪問(wèn),而是以字長(zhǎng)(word size)為單位訪問(wèn),例如 32位的CPU 字長(zhǎng)是4字節(jié),64位的是8字節(jié)。如果變量的地址沒(méi)有對(duì)齊,可能需要多次訪問(wèn)才能完整讀取到變量?jī)?nèi)容,而對(duì)齊后可能就只需要一次內(nèi)存訪問(wèn),因此內(nèi)存對(duì)齊可以減少CPU訪問(wèn)內(nèi)存的次數(shù),加大CPU訪問(wèn)內(nèi)存的吞吐量。
假設(shè)每次訪問(wèn)的步長(zhǎng)為4個(gè)字節(jié),如果未經(jīng)過(guò)內(nèi)存對(duì)齊,獲取b的數(shù)據(jù)需要進(jìn)行兩次內(nèi)存訪問(wèn),最后再進(jìn)行數(shù)據(jù)整理得到b的完整數(shù)據(jù):
如果經(jīng)過(guò)內(nèi)存對(duì)齊,一次內(nèi)存訪問(wèn)就能得到b的完整數(shù)據(jù),減少了一次內(nèi)存訪問(wèn):
unsafe.AlignOf()
unsafe.AlignOf(x) 方法的返回值是 m,當(dāng)變量進(jìn)行內(nèi)存對(duì)齊時(shí),需要保證分配到 x 的內(nèi)存地址能夠整除 m。因此可以通過(guò)這個(gè)方法,確定變量x 在內(nèi)存對(duì)齊時(shí)的地址:
- 對(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ù)。
對(duì)于系統(tǒng)內(nèi)置基礎(chǔ)類(lèi)型變量 x ,unsafe.Alignof(x) 的返回值就是 min(字長(zhǎng)/8,unsafe.Sizeof(x)),即計(jì)算機(jī)字長(zhǎng)與類(lèi)型占用內(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)存對(duì)齊規(guī)則
我們講內(nèi)存對(duì)齊,就是把變量放在特定的地址,那么如何計(jì)算特定地址呢,這就涉及到內(nèi)存對(duì)齊規(guī)則:
- 成員對(duì)齊規(guī)則
針對(duì)一個(gè)基礎(chǔ)類(lèi)型變量,如果 unsafe.AlignOf() 返回的值是 m,那么該變量的地址需要 被m整除 (如果當(dāng)前地址不能整除,填充空白字節(jié),直至可以整除)。
- 整體對(duì)齊規(guī)則
針對(duì)一個(gè)結(jié)構(gòu)體,如果 unsafe.AlignOf() 返回值是 m,需要保證該結(jié)構(gòu)體整體內(nèi)存占用是 m的整數(shù)倍,如果當(dāng)前不是整數(shù)倍,需要在后面填充空白字節(jié)。
通過(guò)內(nèi)存對(duì)齊后,就可以在保證在訪問(wèn)一個(gè)變量地址時(shí):
舉個(gè)例子
例1:
type A struct {a int32b int64c int32 }func main() {fmt.Println(unsafe.Sizeof(A{1, 1, 1})) // 24 }例二:
type B struct {a int32b int32c int64 }func main() {fmt.Println(unsafe.Sizeof(B{1, 1, 1})) // 16 }空結(jié)構(gòu)體的對(duì)齊規(guī)則
如果空結(jié)構(gòu)體作為結(jié)構(gòu)體的內(nèi)置字段:當(dāng)變量位于結(jié)構(gòu)體的前面和中間時(shí),不會(huì)占用內(nèi)存;當(dāng)該變量位于結(jié)構(gòu)體的末尾位置時(shí),需要進(jìn)行內(nèi)存對(duì)齊,內(nèi)存占用大小和前一個(gè)變量的大小保持一致。
type C struct {a struct{}b int64c int64 }type D struct {a int64b struct{}c int64 }type E struct {a int64b int64c struct{} }type F struct {a int32b int32c struct{} }func main() {fmt.Println(unsafe.Sizeof(C{})) // 16fmt.Println(unsafe.Sizeof(D{})) // 16fmt.Println(unsafe.Sizeof(E{})) // 24fmt.Println(unsafe.Sizeof(F{})) // 12 }總結(jié)
本篇文章我們一起學(xué)習(xí)了Go 語(yǔ)言中的內(nèi)存對(duì)齊,主要內(nèi)容如下:
- unsafe.Sizeof(x) 返回了變量x的內(nèi)存占用大小
- 兩個(gè)結(jié)構(gòu)體,即使包含變量類(lèi)型的數(shù)量相同,但是位置不同,占用的內(nèi)存大小也不同,由此引出了內(nèi)存對(duì)齊
- 內(nèi)存對(duì)齊包含成員對(duì)齊和整體對(duì)齊,與 unsafe.AlignOf(x) 息息相關(guān)
- 空結(jié)構(gòu)體作為成員變量時(shí),是否占用內(nèi)存和所處位置有關(guān)
- 在實(shí)際開(kāi)發(fā)中,我們可以通過(guò)調(diào)整變量位置,優(yōu)化內(nèi)存占用(一般按照變量?jī)?nèi)存大小順序排列,整體占用內(nèi)存更小)
更多
個(gè)人博客: https://lifelmy.github.io/
微信公眾號(hào):漫漫Coding路
總結(jié)
以上是生活随笔為你收集整理的Go语言内存对齐详解的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ASP.NET系统退出(移除Sessio
- 下一篇: 关于a标签不能调用js方法的小细节,你注