前言
在學習 Go 時會了解到 struct 其實就是一串自訂的資料結構,而其中欄位的「順序」將直接影響到在記憶體中所佔用的實際大小甚至程式的速度。
為什麼需要記憶體填充?
理論上 CPU 可以從任何一個地址讀取資料,但實際上為了提高硬體的存取效率,CPU 讀取記憶體時通常並不是以單一 Byte 為單位讀取的而是以字長 (Word Size) 為單位進行讀取,通常來說:
- 在 32-bit 系統中,字長為 4 Bytes
- 在 64-bit 系統中,字長為 8 Bytes
假設在 64-bit 系統上,一個 int64 (8 Bytes) 的變數剛好跨越了兩個字長,CPU 就必須執行兩次讀取操作,並將結果拼接起來才能得到完整的數值。這不僅造成多餘的運算,在某些硬體架構上甚至會直接拋出異常。
char data[10]; // char 只有 1 byte,所以它可以放任何位置int *p = (int *)&data[1]; // 假設 &data[1] 是 0x1001(奇數位址)int val = *p; // 在嚴格對齊的架構(如 ARM, SPARC)上會有機會直接崩潰觸發 SIGBUS0x1000 0x1001 0x1002 0x1003 0x1004 [---------- int ----------]為了避免這種情況,編譯器會自動介入,確保資料的起始地址符合特定的規則,這就是「記憶體對齊」,而為了達成對齊,編譯器會在資料之間塞入空白的 Byte,這個動作稱為「記憶體填充」。
設計良好的記憶體佈局
type BadStruct struct { A int8 // 1 Byte B int64 // 8 Bytes C int16 // 2 Bytes}
func main() { var bad BadStruct fmt.Printf("BadStruct Size: %d Bytes\n", unsafe.Sizeof(bad)) // 24 Bytes}在不好的範例中 int64 必須從 8 的倍數地址開始,因此自動填充了 7 Byte 的記憶體空間,而 1+7+8+2 = 18 不是 8 的倍數所以又會在結尾填充 6 Byte 最終大小為 24 Byte。
type GoodStruct struct { B int64 // 8 Bytes C int16 // 2 Bytes A int8 // 1 Byte}
func main() { var good GoodStruct fmt.Printf("GoodStruct Size: %d Bytes\n", unsafe.Sizeof(good)) // 16 Bytes}在好的範例中欄位總大小只有 11 Bytes,struct 本身仍需滿足最大對齊需求(此例為 int64 的 8 Bytes 對齊),因此結尾會再填充 5 Bytes,最終大小為 16 Bytes。
以上範例僅是調整順序就省下了 24 - 16 = 8 Bytes (將近 33%) 的記憶體空間,如果該 struct 重複被初始化,節省的記憶體空間也能非常可觀。
空結構體與填充
type StructWithEmpty struct { A int32 B struct{} // 放在最後}如果空結構體被放在另一個結構體的最後一個欄位,它將會佔用記憶體,以避免產生超出邊界的指標,Go 編譯器會為作為最後一個欄位的空結構體進行填充。
檢測記憶體對齊
go vet -fieldalignmentfieldalignment -fix ./...總結
了解記憶體對齊的概念除了「節省空間」外也能為打造「更佳的緩存局部性(Cache locality)」。
- 將相同或相近大小的欄位定義在一起,通常是按照欄位大小由大到小(或由小到大)排序。
- 不要將空結構體放在最後一個欄位
- 記憶體對齊固然重要,但只是開發中的其中一個面向,避免過度工程
延伸閱讀
- What does it mean by word size in computer? - Stack Overflow
- WHY IS THE STACK SO FAST? - Core Dumped