Golang 是否有必要內存對齊?


原文:https://ms2008.github.io/2019/08/01/golang-memory-alignment/

內存模型

Posted by ms2008 on August 1, 2019

有些同學可能不知道,struct 中的字段順序不同,內存占用也有可能會相差很大。比如:

type T1 struct { a int8 b int64 c int16 } type T2 struct { a int8 c int16 b int64 }

在 64 bit 平台上,T1 占用 24 bytes,T2 占用 16 bytes 大小;而在 32 bit 平台上,T1 占用 16 bytes,T2 占用 12 bytes 大小。可見不同的字段順序,最終決定 struct 的內存大小,所以有時候合理的字段順序可以減少內存的開銷。

這是為什么呢?因為有內存對齊的存在,編譯器使用了內存對齊,那么最后的大小結果就會不一樣。至於為什么要做對齊,主要考慮下面兩個原因:

  • 平台(移植性)

    不是所有的硬件平台都能夠訪問任意地址上的任意數據。例如:特定的硬件平台只允許在特定地址獲取特定類型的數據,否則會導致異常情況

  • 性能

    若訪問未對齊的內存,將會導致 CPU 進行兩次內存訪問,並且要花費額外的時鍾周期來處理對齊及運算。而本身就對齊的內存僅需要一次訪問就可以完成讀取動作,這顯然高效很多,是標准的空間換時間做法

有的小伙伴可能會認為內存讀取,就是一個簡單的字節數組擺放。但實際上 CPU 並不會以一個一個字節去讀取和寫入內存,相反 CPU 讀取內存是一塊一塊讀取的,塊的大小可以為 2、4、6、8、16 字節等大小,塊大小我們稱其為內存訪問粒度。假設訪問粒度為 4,那么 CPU 就會以每 4 個字節大小的訪問粒度去讀取和寫入內存。

在不同平台上的編譯器都有自己默認的 “對齊系數”。一般來講,我們常用的 x86 平台的系數為 4;x86_64 平台系數為 8。需要注意的是,除了這個默認的對齊系數外,還有不同數據類型的對齊系數。數據類型的對齊系數在不同平台上可能會不一致。例如,在 x86_64 平台上,int64 的對齊系數為 8,而在 x86 平台上其對齊系數就是 4。

還是拿上面的 T1、T2 來說,在 x86_64 平台上,T1 的內存布局為:

T2 的內存布局為(int16 的對齊系數為 2):

仔細看,T1 存在許多 padding,顯然它占據了不少空間。那么也就不難理解,為什么調整結構體內成員變量的字段順序就能達到縮小結構體占用大小的疑問了,是因為巧妙地減少了 padding 的存在。讓它們更 “緊湊” 了。

其實內存對齊除了可以降低內存占用之外,還有一種情況是必須要手動對齊的:在 x86 平台上原子操作 64bit 指針。之所以要強制對齊,是因為在 32bit 平台下進行 64bit 原子操作要求必須 8 字節對齊,否則程序會 panic。詳情可以參考 atomic 官方文檔(這么重要的信息竟然放在頁面的最底部!!!😱):

Bugs

On x86-32, the 64-bit functions use instructions unavailable before the Pentium MMX. On non-Linux ARM, the 64-bit functions use instructions unavailable before the ARMv6k core. On ARM, x86-32, and 32-bit MIPS, it is the caller’s responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.

比如,下面這段代碼:

package main import ( "sync/atomic" ) type T3 struct { b int64 c int32 d int64 } func main() { a := T3{} atomic.AddInt64(&a.d, 1) }

編譯為 64bit 可執行文件,運行沒有任何問題;但是當編譯為 32bit 可執行文件,運行就會 panic:

$ GOARCH=386 go build aligned.go $ $ ./aligned panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x8049f2c] goroutine 1 [running]: runtime/internal/atomic.Xadd64(0x941218c, 0x1, 0x0, 0x809a4c0, 0x944e070) /usr/local/go/src/runtime/internal/atomic/asm_386.s:105 +0xc main.main() /root/gofourge/src/lab/archive/aligned.go:18 +0x42 

原因就是 T3 在 32bit 平台上是 4 字節對齊,而在 64bit 平台上是 8 字節對齊。在 64bit 平台上其內存布局為:

可以看到編譯器為了讓 d 8 字節對齊,在 c 后面 padding 了 4 個字節。而在 32bit 平台上其內存布局為:

編譯器用的是 4 字節對齊,所以 c 后面 4 個字節並沒有 padding,而是直接排列 d 的高低位字節。

為了解決這種情況,我們必須手動 padding T3,讓其 “看起來” 像是 8 字節對齊的:

type T3 struct { b int64 c int32 _ int64 d int64 }

這樣 T3 的內存布局就變成了:

看起來就像 8 字節對齊了一樣,這樣就能完美兼容 32bit 平台了。其實很多知名的項目,都是這么處理的,比如 groupcache

type Group struct { _ int32 // force Stats to be 8-byte aligned on 32-bit platforms // Stats are statistics on the group. Stats Stats }

說了這么多,但是在我們實際編碼的時候,多數情況都不會考慮到最優的內存對齊。那有沒有什么辦法能自動檢測當前的內存布局是最優呢?答案是:有的。

golang-sizeof.tips 這個網站就可以可視化 struct 的內存布局,但是只支持 8 字節對齊,是個缺點。還有一種方法,就是用 golangci-lint 做靜態檢測,比如在我的一個項目中檢測結果是這樣的:

$ golangci-lint run --disable-all -E maligned config/config.go:79:11: struct of size 48 bytes could be of size 40 bytes (maligned) type SASL struct { ^ 

提示有一處 struct 可以優化,來看一下這個 struct 的定義:

type SASL struct { Enable bool Username string Password string Handshake bool }

通過 golang-sizeof.tips 對比,顯然字段按照下面這樣排序更為合理:

type SASL struct { Username string Password string Handshake bool Enable bool }

參考文獻



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM