Go 内存对齐的那些事儿
在討論內存對齊前我們先看一個思考題,我們都知道Go的結構體在內存中是由一塊連續的內存表示的,那么下面的結構體占用的內存大小是多少呢?
type?ST1?struct?{A?byteB?int64C?byte }在64位系統下 byte 類型就只占1字節,int64 占用的是8個字節,按照數據類型占的字節數推理,很快就能得出結論:這個結構體的內存大小是10個字節 (1 + 8 +1 )。這個推論到底對不對呢?我們讓 Golang 自己揭曉一下答案。
package?mainimport?("fmt""unsafe" )type?ST1?struct?{A?byteB?int64C?byte }func?main()?{fmt.Println("ST1.A 占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST1{}.A)))fmt.Println("ST1.A 對齊的字節數是:"?+?fmt.Sprint(unsafe.Alignof(ST1{}.A)))fmt.Println("ST1.B 占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST1{}.B)))fmt.Println("ST1.B 對齊的字節數是:"?+?fmt.Sprint(unsafe.Alignof(ST1{}.B)))fmt.Println("ST1.C 占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST1{}.C)))fmt.Println("ST1.C 對齊的字節數是:"?+?fmt.Sprint(unsafe.Alignof(ST1{}.C)))fmt.Println("ST1結構體?占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST1{})))fmt.Println("ST1結構體?對齊的字節數是:"?+?fmt.Sprint(unsafe.Alignof(ST1{}))) }##?輸出 ST1.A 占用的字節數是:1 ST1.A 對齊的字節數是:1 ST1.B 占用的字節數是:8 ST1.B 對齊的字節數是:8 ST1.C 占用的字節數是:1 ST1.C 對齊的字節數是:1 ST1結構體?占用的字節數是:24 ST1結構體?對齊的字節數是:8Golang 告訴我們 ST1 結構體占用的字節數是24。但是每個字段占用的字節數總共加起來確實是只有10個字節,這是怎么回事呢?
因為字段B占用的字節數是8,內存對齊的字節數也是8,A字段所在的8個字節里不足以存放字段B,所以只好留下7個字節的空洞,在下一個 8 字節存放字段B。又因為結構體ST1是8字節對齊的(可以理解為占的內存空間必須是8字節的倍數,且起始地址能夠整除8),所以 C 字段占據了下一個8字節,但是又留下了7個字節的空洞。
這樣ST1結構體總共占用的字節數正好是 24 字節。
既然知道了 Go 編譯器在對結構體進行內存對齊的時候會在字段之間留下內存空洞,那么我們把只需要 1 個字節對齊的字段 C 放在需要 8 個字節內存對齊的字段 B 前面就能讓結構體 ST1 少占 8 個字節。下面我們把 ST1 的 C 字段放在 B 的前面再觀察一下 ST1 結構體的大小。
package?mainimport?("fmt""unsafe" )type?ST1?struct?{A?byteC?byteB?int64 }func?main()?{fmt.Println("ST1.A 占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST1{}.A)))fmt.Println("ST1.A 對齊的字節數是:"?+?fmt.Sprint(unsafe.Alignof(ST1{}.A)))fmt.Println("ST1.B 占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST1{}.B)))fmt.Println("ST1.B 對齊的字節數是:"?+?fmt.Sprint(unsafe.Alignof(ST1{}.B)))fmt.Println("ST1.C 占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST1{}.C)))fmt.Println("ST1.C 對齊的字節數是:"?+?fmt.Sprint(unsafe.Alignof(ST1{}.C)))fmt.Println("ST1結構體?占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST1{})))fmt.Println("ST1結構體?對齊的字節數是:"?+?fmt.Sprint(unsafe.Alignof(ST1{}))) }##?輸出ST1.A 占用的字節數是:1 ST1.A 對齊的字節數是:1 ST1.B 占用的字節數是:8 ST1.B 對齊的字節數是:8 ST1.C 占用的字節數是:1 ST1.C 對齊的字節數是:1 ST1結構體?占用的字節數是:16 ST1結構體?對齊的字節數是:8重排字段后,ST1 結構體的內存布局變成了下圖這樣
僅僅只是調換了一下順序,結構體 ST1 就減少了三分之一的內存占用空間。在實際編程應用時大部分時候我們不用太過于注意內存對齊對數據結構空間的影響,不過作為工程師了解內存對齊這個知識還是很重要的,它實際上是一種典型的以空間換時間的策略。
內存對齊
操作系統在讀取數據的時候并非按照我們想象的那樣一個字節一個字節的去讀取,而是一個字一個字的去讀取。
字是用于表示其自然的數據單位,也叫machine word。字是系統用來一次性處理事務的一個固定長度。
字長 / 步長 就是一個字可容納的字節數,一般 N 位系統的字長是 (N / 8) 個字節。
因此,當 CPU 從存儲器讀數據到寄存器,或者從寄存器寫數據到存儲器,每次 IO 的數據長度是字長。如 32 位系統訪問粒度是 4 字節(bytes),64 位系統的就是 8 字節。當被訪問的數據長度為 n 字節且該數據的內存地址為 n 字節對齊,那么操作系統就可以高效地一次定位到數據,無需多次讀取、處理對齊運算等額外操作。
內存對齊的原則是:將數據盡量的存儲在一個字長內,避免跨字長的存儲。
Go 官方文檔中對數據類型的內存對齊也有如下保證:
對于任何類型的變量 x,unsafe.Alignof(x) 的結果最小為1 (類型最小是一字節對齊的)。
對于一個結構體類型的變量 x,unsafe.Alignof(x) 的結果為 x 的所有字段的對齊字節數中的最大值。
對于一個數組類型的變量 x , unsafe.Alignof(x) 的結果和此數組的元素類型的一個變量的對齊字節數相等,也就是 unsafe.Alignof(x) == unsafe.Alignof(x[i])。
下面這個表格列出了每種數據類型對齊的字節數
| bool, byte, unit8 int8 | 1 |
| uint16, int16 | 2 |
| uint32, int32, float32, complex64 | 4 |
| uint64, int64, float64, complex64 | 8 |
| array | 由其元素類型決定 |
| struct | 由其字段類型決定, 最小為1 |
| 其他類型 | 8 |
零字節類型的對齊
我們都知道 struct{} 類型占用的字節數是 0,但其實它的內存對齊數是 1,這么設定的原因為了保證當它作為結構體的末尾字段時,不會訪問到其他數據結構的地址。比如像下面這個結構體 ST2
type?ST2?struct?{A?uint32B?uint64C?struct{} }雖然字段 C 占用的字節數為0,但是編譯器會為它補 8 個字節,這樣就能保證訪問字段 C 的時候不會訪問到其他數據結構的內存地址。
type?ST2?struct?{A?uint32B?uint64C?struct{} }func?main()?{fmt.Println("ST2.C 占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST2{}.C)))fmt.Println("ST2.C 對齊的字節數是:"?+?fmt.Sprint(unsafe.Alignof(ST2{}.C)))fmt.Println("ST2 結構體占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST2{}))) }##?輸出ST2.C 占用的字節數是:0 ST2.C 對齊的字節數是:1 ST2 結構體占用的字節數是:24當然因為 C 前一個字段 B 占據了整個字長,如果把 A 和 B 的順序調換一下,因為 A 只占 4 個字節,C 的對齊字節數是 1, 足夠排在這個字剩余的字節里。這樣一來 ST2 結構體的占用空間就能減少到 16 個字節。
type?ST2?struct?{B?uint64A?uint32C?struct{} }func?main()?{fmt.Println("ST2.C 占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST2{}.C)))fmt.Println("ST2.C 對齊的字節數是:"?+?fmt.Sprint(unsafe.Alignof(ST2{}.C)))fmt.Println("ST2 結構體占用的字節數是:"?+?fmt.Sprint(unsafe.Sizeof(ST2{}))) }##?輸出 ST2.C 占用的字節數是:0 ST2.C 對齊的字節數是:1 ST2 結構體占用的字節數是:16總結
內存對齊在我理解就是為了計算機訪問數據的效率,對于像結構體、數組等這樣的占用連續內存空間的復合數據結構來說:
數據結構占用的字節數是對齊字節數的整數倍。
數據結構的邊界地址能夠整除整個數據結構的對齊字節數。
這樣 CPU 既減少了對內存的讀取次數,也不需要再對讀取到的數據進行篩選和拼接,是一種典型的以空間換時間的方法。
希望通過這篇文章能讓你更了解 Go 語言也更了解內存對齊這個計算機操作系統減少內存訪問頻率的機制。
總結
以上是生活随笔為你收集整理的Go 内存对齐的那些事儿的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 面试官:说说你对高性能秒杀系统的设计思考
- 下一篇: 你真的懂 timeout 吗?