golang內(nèi)存對齊的概念及案例詳解
什么是內(nèi)存對齊
為保證程序順利高效的運行,編譯器會把各種類型的數(shù)據(jù)安排到合適的地址,并占用合適的長度,這就是內(nèi)存對齊。
每種類型的對齊值就是它的對齊邊界,內(nèi)存對齊要求數(shù)據(jù)存儲地址以及占用的字節(jié)數(shù)都要是它的對齊邊界的倍數(shù)。所以下述的int32要錯開兩個字節(jié),從4開始存,卻不能緊接著從2開始。
也可以這樣解釋:
CPU把內(nèi)存當(dāng)成是一塊一塊的,塊的大小可以是2,4,8,16字節(jié)大小,因此CPU在讀取內(nèi)存時是一塊一塊進(jìn)行讀取的。塊大小成為memory access granularity(粒度)。
如果不進(jìn)行內(nèi)存對齊
比如我們想從地址1開始讀8字節(jié)的數(shù)據(jù):
CPU會分兩次讀:
- 第一次從
0 - 7
但只取后7
字節(jié)。 - 第二次從
8 - 15
但只取第1
字節(jié)。
分兩次讀,這樣勢必會對性能造成影響。
為什么要內(nèi)存對齊
原因主要有兩點:
- 平臺原因(移植原因):不是所有的硬件平臺都能訪問任意地址上的任意數(shù)據(jù)的;某些硬件平臺只能在某些地址處取某些特定類型的數(shù)據(jù),否則拋出硬件異常。
- 性能原因:數(shù)據(jù)結(jié)構(gòu)(尤其是棧)應(yīng)該盡可能地在自然邊界上對齊。原因在于,為了訪問未對齊的內(nèi)存,處理器需要作兩次內(nèi)存訪問;而對齊的內(nèi)存訪問僅需要一次訪問。
對齊邊界
那該怎么確定每種數(shù)據(jù)的對齊邊界呢?這和平臺有關(guān),go語言支持這些平臺:
可以看到常見的32位平臺,指針寬度和寄存器寬度都是4字節(jié),64位平臺上都是8字節(jié)。而被go語言稱為寄存器寬度的這個值,就可以理解為機(jī)器字長,也是平臺對應(yīng)的最大對齊邊界。
而數(shù)據(jù)類型的對齊邊界,是取類型大小與平臺最大對齊邊界中較小的那個。不過要注意,同一個類型在不同平臺上,大小可能不同,對齊邊界也可能不同。
為什么不統(tǒng)一使用平臺最大對齊邊界呢?或者統(tǒng)一按各類型大小來對齊呢?
我們來試一下,假設(shè)目前是64位平臺,最大對齊邊界為8字節(jié)。int8只有1字節(jié),按照1字節(jié)對齊的話,它可以放在任何位置,因為總能通過一次讀取把它完整拿出來。如果統(tǒng)一對齊到8字節(jié),雖然同樣只要讀取一次,但每個int8的變量都要浪費7字節(jié),所以對齊到1。
int16占2字節(jié),按照2字節(jié)對齊,可以從這些地址開始存,而且能保證只用讀取一次。
如果按1字節(jié)對齊就可能存成這樣,那就要讀取兩次再截取拼接,會影響性能。
如果按8字節(jié)對齊,會與int8一樣浪費內(nèi)存,所以對齊到2。
這是小于最大對齊邊界的情況,再來看看大于的情況。
假設(shè)要在32位的平臺下存儲一個int64類型的數(shù)據(jù),在0和1位置被占用的情況下,就要從位置8開始存。而如果對齊到4,就可以從位置4開始,內(nèi)存浪費更少,所以選擇對齊到4。
因此類型對齊邊界會這樣選擇,依然是為了減少浪費提升性能。
GO 計算對齊邊界函數(shù)
在go語言中可以調(diào)用 unsafe.Alignof
來返回相應(yīng)類型的對齊邊界:
func main() { fmt.Printf("bool align: %d\n", unsafe.Alignof(bool(true))) fmt.Printf("int32 align: %d\n", unsafe.Alignof(int32(0))) fmt.Printf("int8 align: %d\n", unsafe.Alignof(int8(0))) fmt.Printf("int64 align: %d\n", unsafe.Alignof(int64(0))) fmt.Printf("byte align: %d\n", unsafe.Alignof(byte(0))) fmt.Printf("string align: %d\n", unsafe.Alignof("EDDYCJY")) fmt.Printf("map align: %d\n", unsafe.Alignof(map[string]string{})) }
運行結(jié)果:
bool align: 1
int32 align: 4
int8 align: 1
int64 align: 8
byte align: 1
string align: 8
map align: 8
確定結(jié)構(gòu)體的對齊邊界
對結(jié)構(gòu)體而言,首先要確定每個成員的對齊邊界,然后取其中最大的,這就是這個結(jié)構(gòu)體的對齊邊界。
然后來存儲這個結(jié)構(gòu)體變量:
內(nèi)存對齊要求一:
- 存儲這個結(jié)構(gòu)體的起始地址,是對齊邊界的倍數(shù)。
?假設(shè)從0開始存,結(jié)構(gòu)體的每個成員在存儲時,都要把這個起始地址當(dāng)作地址0,然后再用相對地址來決定自己該放在哪里。
內(nèi)存對齊要求2:
- 結(jié)構(gòu)體整體占用字節(jié)數(shù)需要是類型對齊邊界的倍數(shù),不夠的話要往后擴(kuò)張一下。
?所以最終上述結(jié)構(gòu)體類型的大小就是24字節(jié)。
案例
type Part1 struct { a bool b int32 c int8 d int64 e byte }
type Part2 struct { a bool c int8 e byte b int32 // 4個字節(jié) d int64 }
分別求以上兩個結(jié)構(gòu)體占用的字節(jié):
fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1)) fmt.Printf("part2 size: %d, align: %d\n", unsafe.Sizeof(part2), unsafe.Alignof(part2))
這里我們直接調(diào)用函數(shù)求得:
part1 size: 32, align: 8 part2 size: 16, align: 8
原因請讀者來思考。
參考資料:
https://blog.csdn.net/u010853261/article/details/102557188
https://www.bilibili.com/video/BV1Ja4y1i7AF?from=search&seid=16213689667007976568&spm_id_from=333.337.0.0
到此這篇關(guān)于golang內(nèi)存對齊的文章就介紹到這了,更多相關(guān)golang內(nèi)存對齊內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
從零封裝Gin框架實現(xiàn)數(shù)據(jù)庫初始化GORM
這篇文章主要為大家介紹了從零封裝Gin框架實現(xiàn)數(shù)據(jù)庫初始化GORM,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01go語法入門泛型type?parameters簡稱T(類型形參)兩種場景使用
這篇文章主要為大家介紹了go語法入門泛型type?parameters簡稱T(類型形參)兩種場景使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09十個Golang開發(fā)中應(yīng)該避免的錯誤總結(jié)
Go是一種靜態(tài)類型的、并發(fā)的、垃圾收集的編程語言,由谷歌開發(fā)。開發(fā)人員在編寫Go代碼時總會有一些常見的錯誤,下面是Go語言中需要避免的十大壞錯誤,希望對大家有所幫助2023-03-03Go+Vue開發(fā)一個線上外賣應(yīng)用的流程(用戶名密碼和圖形驗證碼)
這篇文章主要介紹了Go+Vue開發(fā)一個線上外賣應(yīng)用(用戶名密碼和圖形驗證碼),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11