從 CPU 角度理解 Go 中的結構體記憶體對齊
大家好,我是 Go 學堂的漁夫子。今天跟大家聊聊結構體欄位記憶體對齊相關的知識點。
原文連結:https://mp.weixin.qq.com/s/H3399AYE1MjaDRSllhaPrw
大家在寫 Go 時有沒有注意過,一個 struct 所佔的空間不見得等於各個欄位加起來的空間之和,甚至有時候把欄位的順序調整一下,struct 的所佔空間又有不同的結果。
本文就從 cpu 讀取記憶體的角度來談談記憶體對齊的原理。
01 結構體欄位對齊示例
我們先從一個示例開始。T1 結構體,共有 3 個欄位,型別分別為 int8,int64,int32。所以變數 t1 所屬的型別佔用的空間應該是 1+8+4=13 位元組。但執行程式後,實際上是 24 位元組。和我們計算的 13 位元組不一樣啊。如果我們把該結構體的欄位調整成 T2 那樣,結果是 16 位元組。但和 13 位元組還是不一樣。這是為什麼呢?
type T1 struct {
f1 int8 // 1 byte
f2 int64 // 8 bytes
f3 int32 // 4 bytes
}
type T2 struct {
f1 int8 // 1 byte
f3 int32 // 4 bytes
f2 int64 // 8 bytes
}
func main() {
fmt.Println(runtime.GOARCH) // amd64
t1 := T1{}
fmt.Println(unsafe.Sizeof(t1)) // 24 bytes
t2 := T2{}
fmt.Println(unsafe.Sizeof(t2)) // 16 bytes
}
02 CPU 按字的方式從記憶體讀取資料
在買電腦時或看自己電腦的屬性時,都會發現 CPU 的規格是 64 位或 32 位的,近些年的都是 64 位的。而這 64 位指的就是 CPU 一次可以從記憶體中讀取 64 位的資料,即 8 個位元組。這個長度也稱為 CPU 的字長(注意這裡和位元組的區別,位元組是固定的 8 位,而字長隨著 CPU 的規格變化,32 位的字長是 4 位元組,64 位的字長是 8 位元組)。
雖然 CPU 一次可以抓取 8 位元組,但也是想從哪裡抓就從哪裡抓取的。因為記憶體也會以 8 位元組為單位分成一個一個的字(如下圖),而 CPU 一次只能拿某一個字。所以,如果所需要讀取的資料正好跨了兩個字,那就得花兩個 CPU 週期的時間去讀取了。
03 struct 欄位記憶體對齊
瞭解了 CPU 從記憶體讀取資料是按塊讀取的之後,我們再來看看開頭的 T1 結構體各欄位在記憶體中如果緊密排列的話會是怎麼樣的。在 T1 結構體中各欄位的順序是按 int8、int64、int32 定義的,所以把各欄位在記憶體中的佈局應該形如下面這樣:因為第 2 個欄位需要 8 位元組,所以會有一個位元組的資料排列到第 2 個字中。
那這樣排列會有什麼問題呢?如果我們的程式想要讀取 t1.f2 欄位的資料,那 CPU 就得花兩個時鐘週期把 f2 欄位從記憶體中讀取出來,因為 f2 欄位分散在兩個字中。
所以,為了能讓 CPU 可以更快的存取到各個欄位,Go 編譯器會幫你把 struct 結構體做資料的對齊。所謂的資料對齊,是指記憶體地址是所儲存資料大小(按位元組為單位)的整數倍,以便 CPU 可以一次將該資料從記憶體中讀取出來。 編譯器通過在 T1 結構體的各個欄位之間填充一些空白已達到對齊的目的。
重新排列後,記憶體的佈局會長如下這樣,有 13 個位元組的空間是真正儲存資料的,而深色的 11 個位元組的空間則是為了對齊而填充上的,不儲存任何資料,以確保每個欄位的資料都會落到同一個字長裡面,所以才會有了開頭的 13 個位元組的資料型別實際上變成了 24 位元組。
04 如何減少 struct 的填充
雖然通過填充的方式可以提高 CPU 讀寫資料的效率,但這些填充的記憶體實際上是不存數任何資料的,也就相當於浪費掉了。以 T1 結構體為例,實際儲存資料的只有 13 位元組,但實際用了 24 位元組,浪費了將近一半,那有沒有什麼辦法既可以做到記憶體對齊提高 CPU 讀取效率又能減少記憶體浪費的嗎?
答案就是調整 struct 欄位的順序。我們再觀察下 T1 結構體的欄位分佈,就會發現下面的 f3 欄位的 4 個位元組可以挪到 f1 欄位所在的那一排的填充位置,畢竟第一排的填充空間超大,不用也是浪費,而且挪上去之後,每個欄位還是都在同一個字裡面。
一旦把 f3 移上去,就可以省掉最下面一整個 word(8 bytes) 的空間,所以 T2 整個 struct 就只需要 16 bytes,是原本 T1 24 bytes 的三分之二。
在 Go 程式中,Go 會按照結構體中欄位的順序在記憶體中進行佈局,所以需要將欄位 f2 和 f3 的位置交換,定義的順序變成 int8、int32、int64,這樣 Go 編譯器才會順利的按上圖那樣排列。
05 在同一個字(8 位元組)中的記憶體分佈
上面都是看到的跨字長(64 位系統下是 8 位元組)的儲存示例來說明 CPU 需要從記憶體讀取兩次才能將一個完整的資料完整的讀取出來。那如果是有 n 個小於一個字長的型別在同一個字長中是否可以連續分配呢?我們通過示例來講解一下:
var x struct {
a bool
b int16
}
我們計算一下各個欄位佔用的記憶體空間:1 位元組(a)+2 位元組(b)= 3 位元組。沒超過 1 個字長(8 位元組),但在記憶體中的分佈是如下圖這樣:
我們發現 b 並沒有直接在 a 的後面,而是在 a 中填充了一個空白後,放到了偏移量為 2 的位置上。為什麼呢?
答案還是從記憶體對齊的定義中推匯出來。我們上面說過,記憶體對齊是指資料存放的地址是資料大小的整數倍。也就是說會有資料存放的起始地址% 資料的大小=0
我們來驗證下上面的結構體的排列。假設結構體的起始地址為 0,那麼 a 從 0 開始佔用 1 個位元組。b 欄位如果放在地址 1 處,套用上面的公式 1 % 2 = 1,就不滿足對齊的要求。所以在地址為 2 處開始存放 b 欄位。 這也就解釋了很多文章中列出的原則:構體變數中成員的偏移量必須是成員大小的整數倍
06 什麼時候該關注結構體欄位順序
由此可知,對結構體欄位的重新排列會讓結構體更節省內。但我們需要這麼做嗎?以 Student 為例,我們看下 Student 的定義:
type Student struct {
id int8 //學號
name string //姓名
classID int8 //班級
phone [10]byte //聯絡電話
address string // 地址
grade int32 //成績
}
我們看下該結構體在記憶體中的分佈如下,可以看到有很多深色的填充空間,總計浪費了 16 位元組,好像還可以優化。
我們通過調整 Student 結構體的欄位順序來進行下優化,可以看到從開始的 64 位元組,可以優化到 48 位元組,共剩下了 25% 的空間。
type Student struct {
name string //姓名
address string // 地址
grade int32 //成績
phone [10]byte //聯絡電話
id int8 //學號
classID int8 //班級
}
調整後的結構體在記憶體中的分佈如下:
我們看到,通過調整結構體中的欄位順序確實節省了記憶體空間,那我們真的有必要這樣節省空間嗎?
以 student 結構體為例,經過重新排列後,節省了 16 位元組的空間,假設我們在程式中需要排列全校同學的成績,需要定義一個長度為 10 萬的 Student 型別的陣列,那剩下的記憶體也不過 16MB 的空間,跟現在個人電腦的 8G 到 16G 的記憶體比起來微不足道。而且在欄位重新排列後,可讀性也變的很差了。像 Student 原本是以學號,姓名,班級...這樣依次排列的,而重新調整後變成了姓名,地址,成績...,一直到最後才是學號跟班級,不符合人們的思維習慣。
所以,我的建議是對於結構體的欄位排列不需要過早的進行優化,除非一開始就知道你的程式瓶頸就卡在這裡。否則,就按照正常的習慣編寫 Go 程式即可。
07 總結
本文從 CPU 讀取記憶體的角度分析了為什麼需要進行資料對齊。該文目的是為了讓你更好的瞭解底層的執行機制,而非時刻關注結構體的欄位順序。在編寫程式碼時順其自然就好。到了這裡成為瓶頸的時候再記著調整下欄位順序就好。
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 結構體記憶體對齊結構體記憶體
- GO 記憶體對齊Go記憶體
- 理解記憶體對齊記憶體
- c 結構體記憶體對齊詳解結構體記憶體
- C++ struct結構體記憶體對齊C++Struct結構體記憶體
- C結構體中資料的記憶體對齊問題結構體記憶體
- 從萌新的角度理解JVM記憶體管理JVM記憶體
- 從記憶體角度深入看結構體(window/linux)大小記憶體結構體Linux
- 記憶體對齊記憶體
- struct結構體大小的計算(記憶體對齊)Struct結構體記憶體
- ORACLE 記憶體結構理解.Oracle記憶體
- 從Java記憶體模型角度理解安全初始化Java記憶體模型
- C++ 記憶體對齊C++記憶體
- 記憶體位元組對齊記憶體
- 關於記憶體對齊記憶體
- C# 記憶體對齊C#記憶體
- 理解JVM(一):記憶體結構JVM記憶體
- Go高效能程式設計-瞭解記憶體對齊以及Go中的型別如何對齊保證Go程式設計記憶體型別
- 由結構體對齊所引發的對C++類物件記憶體模型的思考(一)結構體C++物件記憶體模型
- 由結構體對齊所引發的對C++類物件記憶體模型的思考(二)結構體C++物件記憶體模型
- iOS 記憶體位元組對齊iOS記憶體
- C語言記憶體對齊C語言記憶體
- Go plan9 彙編:記憶體對齊和遞迴Go記憶體遞迴
- go中的記憶體逃逸Go記憶體
- Go記憶體分配和GC的理解Go記憶體GC
- Dig101:Go 之聊聊 struct 的記憶體對齊GoStruct記憶體
- 記憶體結構記憶體
- 結構體內部儲存中的對齊問題結構體
- Go 的記憶體對齊和指標運算詳解和實踐Go記憶體指標
- 結構體成員對齊的問題結構體
- iOS探索 記憶體對齊&malloc原始碼iOS記憶體原始碼
- C/C++記憶體對齊詳解C++記憶體
- C/C++記憶體對齊原則C++記憶體
- 深入理解 JVM 之 JVM 記憶體結構JVM記憶體
- Oracle中Buffer Cache記憶體結構Oracle記憶體
- Oracle體系結構之-記憶體結構Oracle記憶體
- Oracle記憶體體系結構Oracle記憶體
- c++記憶體中位元組對齊問題詳解C++記憶體