Go 內存分配:結構體中的優化技巧
在使用 Golang 進行內存分配時,我們需要遵循一系列規則。在深入瞭解這些規則之前,我們需要先了解變量的對齊方式。
Golang 的unsafe
包中有一個函數Alignof
,簽名如下:
func Alignof(x ArbitraryType) uintptr
對於任何類型爲v
的變量x
,AlignOf
函數會返回該變量的對齊方式。我們將對齊方式記爲m
。現在,Golang 確保m
是滿足變量x的內存地址 % m == 0
的最大可能數,也就是說,變量 x 的內存地址是 m 的倍數。
讓我們來看看一些數據類型的對齊方式:
-
byte
,int8
,uint8
-> 1 -
int16
,uint16
-> 2 -
int32
,uint32
,float32
,complex64
-> 4 -
int
,int64
,uint64
,float64
,complex128
-> 8 -
string
,slice
-> 8
對於結構體中的字段,行爲可能會有所不同,詳細信息請參考包的文檔。
爲了更好地理解結構體內存分配的情況,我們將使用unsafe
包中的另一個函數Offsetof
。該函數返回字段相對於結構體起始位置的位置,換句話說,它返回字段起始位置與結構體起始位置之間的字節數。
func Offsetof(x ArbitraryType) uintptr
爲了更好地理解結構體內存分配,讓我們以一個示例結構體爲例:
type Example struct {
a int8
b string
c int8
d int32
}
現在,我們將找出類型爲Example
的變量所佔用的總內存,並嘗試優化分配。
var v = Example{
a: 10,
b: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus rhoncus.",
c: 20,
d: 100,
}
fmt.Println("字段a的偏移量:", unsafe.Offsetof(v.a)) // 輸出:0
fmt.Println("字段b的偏移量:", unsafe.Offsetof(v.b)) // 輸出:8
fmt.Println("字段c的偏移量:", unsafe.Offsetof(v.c)) // 輸出:24
fmt.Println("字段d的偏移量:", unsafe.Offsetof(v.d)) // 輸出:28
現在,問題出現了:“爲什麼結構體中字段 b 的偏移量是 8?它應該是 1,因爲字段 a 的類型是 int8,只佔用 1 個字節。” 回到字符串數據類型的對齊方式,它的值爲 8,這意味着地址需要被 8 整除,因此在其中插入了 7 個字節的 “填充”,以確保這種行爲。
爲什麼字段 c 的偏移量是 24?字段 b 中的字符串看起來比 16 個字節要長得多,如果字符串的偏移量是 8,那麼字段 c 的偏移量應該更大一些。
上述問題的答案是,在 Go 中,字符串並不是在結構體內的同一位置分配內存的。有一個單獨的數據結構來保存字符串描述符,並且該字符串描述符以原地方式存儲在結構體中,用於類型爲 string 的字段,該描述符的大小爲 16 個字節。
現在,讓我們來看看unsafe
包中的另一個函數Sizeof
。正如其名稱所示,該函數估計並返回類型爲 x 的變量所佔用的字節數。
注意:它是根據結構體中可能存在的不同大小的字段來估計大小的。
func Sizeof(x ArbitraryType) uintptr
現在,讓我們來看看我們的結構體 Example 的大小。
fmt.Println("Example的大小:", unsafe.Sizeof(v)) // 輸出:32
我們如何優化這個結構體以最小化填充呢?
爲了優化這個結構體的內存,我們將查看不同數據類型的對齊方式,並嘗試減少填充。讓我們嘗試將兩個 int8 類型的字段放在一起。
type y struct {
a int8
c int8
b string
d int32
}
var v = y{}
fmt.Println("字段a的偏移量:", unsafe.Offsetof(v.a)) // 輸出:0
fmt.Println("字段b的偏移量:", unsafe.Offsetof(v.b)) // 輸出:8
fmt.Println("字段c的偏移量:", unsafe.Offsetof(v.c)) // 輸出:1
fmt.Println("字段d的偏移量:", unsafe.Offsetof(v.d)) // 輸出:24
fmt.Println("Example的大小:", unsafe.Sizeof(v)) // 輸出:32
太棒了,我們去掉了一些填充,但是爲什麼大小仍然是 32?大小應該是 1(a)+ 1(c)+ 6(填充)+ 16(b)+ 4(d)= 28
現在,當結構體的最後一個字段與架構的對齊要求不完全一致時,會在最後一個字段之後添加填充,以確保結構體的整體大小是其字段中最大對齊要求的倍數。因爲字符串數據類型的最大對齊方式爲 8,所以額外添加了填充,使大小成爲 8 的倍數,即在末尾填充了 4 個字節,使大小爲 32 字節。
我們能否進一步減少填充,使其更加優化?
讓我們嘗試通過移動字段位置來實現。
type y struct {
b string
d int32
a int8
c int8
}
var v = y{}
fmt.Println("字段a的偏移量:", unsafe.Offsetof(v.a)) // 輸出:20
fmt.Println("字段b的偏移量:", unsafe.Offsetof(v.b)) // 輸出:0
fmt.Println("字段c的偏移量:", unsafe.Offsetof(v.c)) // 輸出:21
fmt.Println("字段d的偏移量:", unsafe.Offsetof(v.d)) // 輸出:16
fmt.Println("Example的大小:", unsafe.Sizeof(v)) // 輸出:24
我們可以看到,通過重新排列字段的位置,使得對齊需要最小化填充,我們已經將結構體的大小從 32 減小到 24,這是內存優化的巨大進步,達到了 25%。
當前的內存佔用是 16(b)+ 4(d)+ 1(a)+ 1(b)+ 2(填充)。
遺憾的是,由於語言和架構的限制,我們無法進一步去除填充。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/9GHPeJpTXlFCWQB5ISYLGg