在 Go 中恰到好處的內(nèi)存對齊

image

原文地址:在 Go 中恰到好處的內(nèi)存對齊

問題

type Part1 struct {
    a bool
    b int32
    c int8
    d int64
    e byte
}

在開始之前澜术,希望你計算一下 Part1 共占用的大小是多少呢?

func main() {
    fmt.Printf("bool size: %d\n", unsafe.Sizeof(bool(true)))
    fmt.Printf("int32 size: %d\n", unsafe.Sizeof(int32(0)))
    fmt.Printf("int8 size: %d\n", unsafe.Sizeof(int8(0)))
    fmt.Printf("int64 size: %d\n", unsafe.Sizeof(int64(0)))
    fmt.Printf("byte size: %d\n", unsafe.Sizeof(byte(0)))
    fmt.Printf("string size: %d\n", unsafe.Sizeof("EDDYCJY"))
}

輸出結(jié)果:

bool size: 1
int32 size: 4
int8 size: 1
int64 size: 8
byte size: 1
string size: 16

這么一算募谎,Part1 這一個結(jié)構(gòu)體的占用內(nèi)存大小為 1+4+1+8+1 = 15 個字節(jié)左电。相信有的小伙伴是這么算的,看上去也沒什么毛病

真實情況是怎么樣的呢肖方?我們實際調(diào)用看看,如下:

type Part1 struct {
    a bool
    b int32
    c int8
    d int64
    e byte
}

func main() {
    part1 := Part1{}
    
    fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1))
}

輸出結(jié)果:

part1 size: 32, align: 8

最終輸出為占用 32 個字節(jié)未状。這與前面所預期的結(jié)果完全不一樣俯画。這充分地說明了先前的計算方式是錯誤的。為什么呢司草?

在這里要提到 “內(nèi)存對齊” 這一概念艰垂,才能夠用正確的姿勢去計算,接下來我們詳細的講講它是什么

內(nèi)存對齊

有的小伙伴可能會認為內(nèi)存讀取埋虹,就是一個簡單的字節(jié)數(shù)組擺放

image

上圖表示一個坑一個蘿卜的內(nèi)存讀取方式猜憎。但實際上 CPU 并不會以一個一個字節(jié)去讀取和寫入內(nèi)存。相反 CPU 讀取內(nèi)存是一塊一塊讀取的搔课,塊的大小可以為 2胰柑、4、6、8柬讨、16 字節(jié)等大小崩瓤。塊大小我們稱其為內(nèi)存訪問粒度。如下圖:

image

在樣例中踩官,假設訪問粒度為 4却桶。 CPU 是以每 4 個字節(jié)大小的訪問粒度去讀取和寫入內(nèi)存的。這才是正確的姿勢

為什么要關(guān)心對齊

  • 你正在編寫的代碼在性能(CPU蔗牡、Memory)方面有一定的要求
  • 你正在處理向量方面的指令
  • 某些硬件平臺(ARM)體系不支持未對齊的內(nèi)存訪問

另外作為一個工程師颖系,你也很有必要學習這塊知識點哦 :)

為什么要做對齊

  • 平臺(移植性)原因:不是所有的硬件平臺都能夠訪問任意地址上的任意數(shù)據(jù)。例如:特定的硬件平臺只允許在特定地址獲取特定類型的數(shù)據(jù)蛋逾,否則會導致異常情況
  • 性能原因:若訪問未對齊的內(nèi)存集晚,將會導致 CPU 進行兩次內(nèi)存訪問,并且要花費額外的時鐘周期來處理對齊及運算区匣。而本身就對齊的內(nèi)存僅需要一次訪問就可以完成讀取動作
image

在上圖中偷拔,假設從 Index 1 開始讀取,將會出現(xiàn)很崩潰的問題亏钩。因為它的內(nèi)存訪問邊界是不對齊的莲绰。因此 CPU 會做一些額外的處理工作。如下:

  1. CPU 首次讀取未對齊地址的第一個內(nèi)存塊姑丑,讀取 0-3 字節(jié)蛤签。并移除不需要的字節(jié) 0
  2. CPU 再次讀取未對齊地址的第二個內(nèi)存塊,讀取 4-7 字節(jié)栅哀。并移除不需要的字節(jié) 5震肮、6、7 字節(jié)
  3. 合并 1-4 字節(jié)的數(shù)據(jù)
  4. 合并后放入寄存器

從上述流程可得出留拾,不做 “內(nèi)存對齊” 是一件有點 "麻煩" 的事戳晌。因為它會增加許多耗費時間的動作

而假設做了內(nèi)存對齊,從 Index 0 開始讀取 4 個字節(jié)痴柔,只需要讀取一次沦偎,也不需要額外的運算。這顯然高效很多咳蔚,是標準的空間換時間做法

默認系數(shù)

在不同平臺上的編譯器都有自己默認的 “對齊系數(shù)”豪嚎,可通過預編譯命令 #pragma pack(n) 進行變更,n 就是代指 “對齊系數(shù)”谈火。一般來講侈询,我們常用的平臺的系數(shù)如下:

  • 32 位:4
  • 64 位:8

另外要注意,不同硬件平臺占用的大小和對齊值都可能是不一樣的堆巧。因此本文的值不是唯一的妄荔,調(diào)試的時候需按本機的實際情況考慮

成員對齊

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

在 Go 中可以調(diào)用 unsafe.Alignof 來返回相應類型的對齊系數(shù)泼菌。通過觀察輸出結(jié)果,可得知基本都是 2^n啦租,最大也不會超過 8哗伯。這是因為我手提(64 位)編譯器默認對齊系數(shù)是 8,因此最大值不會超過這個數(shù)

整體對齊

在上小節(jié)中篷角,提到了結(jié)構(gòu)體中的成員變量要做字節(jié)對齊焊刹。那么想當然身為最終結(jié)果的結(jié)構(gòu)體,也是需要做字節(jié)對齊的

對齊規(guī)則

  • 結(jié)構(gòu)體的成員變量恳蹲,第一個成員變量的偏移量為 0虐块。往后的每個成員變量的對齊值必須為編譯器默認對齊長度#pragma pack(n))或當前成員變量類型的長度unsafe.Sizeof),取最小值作為當前類型的對齊值嘉蕾。其偏移量必須為對齊值的整數(shù)倍
  • 結(jié)構(gòu)體本身贺奠,對齊值必須為編譯器默認對齊長度#pragma pack(n))或結(jié)構(gòu)體的所有成員變量類型中的最大長度,取最大數(shù)的最小整數(shù)倍作為對齊值
  • 結(jié)合以上兩點错忱,可得知若編譯器默認對齊長度#pragma pack(n))超過結(jié)構(gòu)體內(nèi)成員變量的類型最大長度時儡率,默認對齊長度是沒有任何意義的

分析流程

接下來我們一起分析一下,“它” 到底經(jīng)歷了些什么以清,影響了 “預期” 結(jié)果

成員變量 類型 偏移量 自身占用
a bool 0 1
字節(jié)對齊 1 3
b int32 4 4
c int8 8 1
字節(jié)對齊 9 7
d int64 16 8
e byte 24 1
字節(jié)對齊 25 7
總占用大小 - - 32

成員對齊

  • 第一個成員 a
    • 類型為 bool
    • 大小/對齊值為 1 字節(jié)
    • 初始地址儿普,偏移量為 0。占用了第 1 位
  • 第二個成員 b
    • 類型為 int32
    • 大小/對齊值為 4 字節(jié)
    • 根據(jù)規(guī)則 1掷倔,其偏移量必須為 4 的整數(shù)倍眉孩。確定偏移量為 4,因此 2-4 位為 Padding勒葱。而當前數(shù)值從第 5 位開始填充浪汪,到第 8 位。如下:axxx|bbbb
  • 第三個成員 c
    • 類型為 int8
    • 大小/對齊值為 1 字節(jié)
    • 根據(jù)規(guī)則1凛虽,其偏移量必須為 1 的整數(shù)倍吟宦。當前偏移量為 8。不需要額外對齊涩维,填充 1 個字節(jié)到第 9 位。如下:axxx|bbbb|c...
  • 第四個成員 d
    • 類型為 int64
    • 大小/對齊值為 8 字節(jié)
    • 根據(jù)規(guī)則 1袁波,其偏移量必須為 8 的整數(shù)倍瓦阐。確定偏移量為 16,因此 9-16 位為 Padding篷牌。而當前數(shù)值從第 17 位開始寫入睡蟋,到第 24 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd
  • 第五個成員 e
    • 類型為 byte
    • 大小/對齊值為 1 字節(jié)
    • 根據(jù)規(guī)則 1枷颊,其偏移量必須為 1 的整數(shù)倍戳杀。當前偏移量為 24该面。不需要額外對齊,填充 1 個字節(jié)到第 25 位信卡。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd|e...

整體對齊

在每個成員變量進行對齊后隔缀,根據(jù)規(guī)則 2,整個結(jié)構(gòu)體本身也要進行字節(jié)對齊傍菇,因為可發(fā)現(xiàn)它可能并不是 2^n猾瘸,不是偶數(shù)倍。顯然不符合對齊的規(guī)則

根據(jù)規(guī)則 2丢习,可得出對齊值為 8《枘簦現(xiàn)在的偏移量為 25卖毁,不是 8 的整倍數(shù)。因此確定偏移量為 32。對結(jié)構(gòu)體進行對齊

結(jié)果

Part1 內(nèi)存布局:axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx

小結(jié)

通過本節(jié)的分析撤蟆,可得知先前的 “推算” 為什么錯誤?

是因為實際內(nèi)存管理并非 “一個蘿卜一個坑” 的思想簿透。而是一塊一塊基跑。通過空間換時間(效率)的思想來完成這塊讀取、寫入锡宋。另外也需要兼顧不同平臺的內(nèi)存操作情況

巧妙的結(jié)構(gòu)體

在上一小節(jié)儡湾,可得知根據(jù)成員變量的類型不同,其結(jié)構(gòu)體的內(nèi)存會產(chǎn)生對齊等動作执俩。那假設字段順序不同徐钠,會不會有什么變化呢?我們一起來試試吧 :-)

type Part1 struct {
    a bool
    b int32
    c int8
    d int64
    e byte
}

type Part2 struct {
    e byte
    c int8
    a bool
    b int32
    d int64
}

func main() {
    part1 := Part1{}
    part2 := Part2{}

    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))
}

輸出結(jié)果:

part1 size: 32, align: 8
part2 size: 16, align: 8

通過結(jié)果可以驚喜的發(fā)現(xiàn)役首,只是 “簡單” 對成員變量的字段順序進行改變尝丐,就改變了結(jié)構(gòu)體占用大小

接下來我們一起剖析一下 Part2,看看它的內(nèi)部到底和上一位之間有什么區(qū)別衡奥,才導致了這樣的結(jié)果爹袁?

分析流程

成員變量 類型 偏移量 自身占用
e byte 0 1
c int8 1 1
a bool 2 1
字節(jié)對齊 3 1
b int32 4 4
d int64 8 8
總占用大小 - - 16

成員對齊

  • 第一個成員 e
    • 類型為 byte
    • 大小/對齊值為 1 字節(jié)
    • 初始地址,偏移量為 0矮固。占用了第 1 位
  • 第二個成員 c
    • 類型為 int8
    • 大小/對齊值為 1 字節(jié)
    • 根據(jù)規(guī)則1失息,其偏移量必須為 1 的整數(shù)倍。當前偏移量為 2档址。不需要額外對齊
  • 第三個成員 a
    • 類型為 bool
    • 大小/對齊值為 1 字節(jié)
    • 根據(jù)規(guī)則1盹兢,其偏移量必須為 1 的整數(shù)倍。當前偏移量為 3守伸。不需要額外對齊
  • 第四個成員 b
    • 類型為 int32
    • 大小/對齊值為 4 字節(jié)
    • 根據(jù)規(guī)則1绎秒,其偏移量必須為 4 的整數(shù)倍。確定偏移量為 4尼摹,因此第 3 位為 Padding见芹。而當前數(shù)值從第 4 位開始填充剂娄,到第 8 位。如下:ecax|bbbb
  • 第五個成員 d
    • 類型為 int64
    • 大小/對齊值為 8 字節(jié)
    • 根據(jù)規(guī)則1玄呛,其偏移量必須為 8 的整數(shù)倍阅懦。當前偏移量為 8。不需要額外對齊把鉴,從 9-16 位填充 8 個字節(jié)故黑。如下:ecax|bbbb|dddd|dddd

整體對齊

符合規(guī)則 2,不需要額外對齊

結(jié)果

Part2 內(nèi)存布局:ecax|bbbb|dddd|dddd

總結(jié)

通過對比 Part1Part2 的內(nèi)存布局庭砍,你會發(fā)現(xiàn)兩者有很大的不同场晶。如下:

  • Part1:axxx|bbbb|cxxx|xxxx|dddd|dddd|exxx|xxxx

  • Part2:ecax|bbbb|dddd|dddd

仔細一看,Part1 存在許多 Padding怠缸。顯然它占據(jù)了不少空間诗轻,那么 Padding 是怎么出現(xiàn)的呢?

通過本文的介紹揭北,可得知是由于不同類型導致需要進行字節(jié)對齊扳炬,以此保證內(nèi)存的訪問邊界

那么也不難理解,為什么調(diào)整結(jié)構(gòu)體內(nèi)成員變量的字段順序就能達到縮小結(jié)構(gòu)體占用大小的疑問了搔体,是因為巧妙地減少了 Padding 的存在恨樟。讓它們更 “緊湊” 了。這一點對于加深 Go 的內(nèi)存布局印象和大對象的優(yōu)化非常有幫

當然了疚俱,沒什么特殊問題劝术,你可以不關(guān)注這一塊。但你要知道這塊知識點 ??

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末呆奕,一起剝皮案震驚了整個濱河市养晋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌梁钾,老刑警劉巖绳泉,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異姆泻,居然都是意外死亡零酪,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門拇勃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蛾娶,“玉大人,你說我怎么就攤上這事潜秋。” “怎么了胎许?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵峻呛,是天一觀的道長罗售。 經(jīng)常有香客問我,道長钩述,這世上最難降的妖魔是什么寨躁? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮牙勘,結(jié)果婚禮上职恳,老公的妹妹穿的比我還像新娘。我一直安慰自己方面,他們只是感情好放钦,可當我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著恭金,像睡著了一般操禀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上横腿,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天颓屑,我揣著相機與錄音,去河邊找鬼耿焊。 笑死揪惦,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的罗侯。 我是一名探鬼主播器腋,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼歇父!你這毒婦竟也來了蒂培?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤榜苫,失蹤者是張志新(化名)和其女友劉穎护戳,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體垂睬,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡媳荒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了驹饺。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钳枕。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖赏壹,靈堂內(nèi)的尸體忽然破棺而出鱼炒,到底是詐尸還是另有隱情,我是刑警寧澤蝌借,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布昔瞧,位于F島的核電站指蚁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏自晰。R本人自食惡果不足惜凝化,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酬荞。 院中可真熱鬧搓劫,春花似錦、人聲如沸混巧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽牲剃。三九已至遣疯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間凿傅,已是汗流浹背缠犀。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留聪舒,地道東北人辨液。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像箱残,于是被迫代替她去往敵國和親滔迈。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,440評論 2 359

推薦閱讀更多精彩內(nèi)容