Go語(yǔ)言中恰到好處的內(nèi)存對(duì)齊

問(wèn)題

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

在開(kāi)始之前峦甩,希望你計(jì)算一下 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 這一個(gè)結(jié)構(gòu)體的占用內(nèi)存大小為 1+4+1+8+1 = 15 個(gè)字節(jié)膘盖。相信有的小伙伴是這么算的胧弛,看上去也沒(méi)什么毛病

真實(shí)情況是怎么樣的呢尤误?我們實(shí)際調(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 個(gè)字節(jié)结缚。這與前面所預(yù)期的結(jié)果完全不一樣损晤。這充分地說(shuō)明了先前的計(jì)算方式是錯(cuò)誤的。為什么呢红竭?

在這里要提到 “內(nèi)存對(duì)齊” 這一概念尤勋,才能夠用正確的姿勢(shì)去計(jì)算,接下來(lái)我們?cè)敿?xì)的講講它是什么

內(nèi)存對(duì)齊

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

上圖表示一個(gè)坑一個(gè)蘿卜的內(nèi)存讀取方式最冰。但實(shí)際上 CPU 并不會(huì)以一個(gè)一個(gè)字節(jié)去讀取和寫(xiě)入內(nèi)存。相反 CPU 讀取內(nèi)存是一塊一塊讀取的稀火,塊的大小可以為 2暖哨、4、6凰狞、8篇裁、16 字節(jié)等大小。塊大小我們稱其為內(nèi)存訪問(wèn)粒度赡若。如下圖:

在樣例中达布,假設(shè)訪問(wèn)粒度為 4。 CPU 是以每 4 個(gè)字節(jié)大小的訪問(wèn)粒度去讀取和寫(xiě)入內(nèi)存的逾冬。這才是正確的姿勢(shì)

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

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

另外作為一個(gè)工程師,你也很有必要學(xué)習(xí)這塊知識(shí)點(diǎn)哦 :)

為什么要做對(duì)齊

  • 平臺(tái)(移植性)原因:不是所有的硬件平臺(tái)都能夠訪問(wèn)任意地址上的任意數(shù)據(jù)粉渠。例如:特定的硬件平臺(tái)只允許在特定地址獲取特定類型的數(shù)據(jù)分冈,否則會(huì)導(dǎo)致異常情況
  • 性能原因:若訪問(wèn)未對(duì)齊的內(nèi)存,將會(huì)導(dǎo)致 CPU 進(jìn)行兩次內(nèi)存訪問(wèn)霸株,并且要花費(fèi)額外的時(shí)鐘周期來(lái)處理對(duì)齊及運(yùn)算雕沉。而本身就對(duì)齊的內(nèi)存僅需要一次訪問(wèn)就可以完成讀取動(dòng)作

在上圖中,假設(shè)從 Index 1 開(kāi)始讀取去件,將會(huì)出現(xiàn)很崩潰的問(wèn)題坡椒。因?yàn)樗膬?nèi)存訪問(wèn)邊界是不對(duì)齊的。因此 CPU 會(huì)做一些額外的處理工作尤溜。如下:

  1. CPU 首次讀取未對(duì)齊地址的第一個(gè)內(nèi)存塊倔叼,讀取 0-3 字節(jié)。并移除不需要的字節(jié) 0
  2. CPU 再次讀取未對(duì)齊地址的第二個(gè)內(nèi)存塊宫莱,讀取 4-7 字節(jié)丈攒。并移除不需要的字節(jié) 5、6、7 字節(jié)
  3. 合并 1-4 字節(jié)的數(shù)據(jù)
  4. 合并后放入寄存器

從上述流程可得出巡验,不做 “內(nèi)存對(duì)齊” 是一件有點(diǎn) "麻煩" 的事际插。因?yàn)樗鼤?huì)增加許多耗費(fèi)時(shí)間的動(dòng)作

而假設(shè)做了內(nèi)存對(duì)齊,從 Index 0 開(kāi)始讀取 4 個(gè)字節(jié)显设,只需要讀取一次框弛,也不需要額外的運(yùn)算。這顯然高效很多捕捂,是標(biāo)準(zhǔn)的空間換時(shí)間做法

默認(rèn)系數(shù)

在不同平臺(tái)上的編譯器都有自己默認(rèn)的 “對(duì)齊系數(shù)”瑟枫,可通過(guò)預(yù)編譯命令 #pragma pack(n) 進(jìn)行變更,n 就是代指 “對(duì)齊系數(shù)”指攒。一般來(lái)講慷妙,我們常用的平臺(tái)的系數(shù)如下:

  • 32 位:4
  • 64 位:8

另外要注意,不同硬件平臺(tái)占用的大小和對(duì)齊值都可能是不一樣的幽七。因此本文的值不是唯一的,調(diào)試的時(shí)候需按本機(jī)的實(shí)際情況考慮

成員對(duì)齊

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 來(lái)返回相應(yīng)類型的對(duì)齊系數(shù)溅呢。通過(guò)觀察輸出結(jié)果澡屡,可得知基本都是 2^n,最大也不會(huì)超過(guò) 8咐旧。這是因?yàn)槲沂痔幔?4 位)編譯器默認(rèn)對(duì)齊系數(shù)是 8驶鹉,因此最大值不會(huì)超過(guò)這個(gè)數(shù)

整體對(duì)齊

在上小節(jié)中,提到了結(jié)構(gòu)體中的成員變量要做字節(jié)對(duì)齊铣墨。那么想當(dāng)然身為最終結(jié)果的結(jié)構(gòu)體室埋,也是需要做字節(jié)對(duì)齊的

對(duì)齊規(guī)則

  • 結(jié)構(gòu)體的成員變量,第一個(gè)成員變量的偏移量為 0伊约。往后的每個(gè)成員變量的對(duì)齊值必須為編譯器默認(rèn)對(duì)齊長(zhǎng)度#pragma pack(n))或當(dāng)前成員變量類型的長(zhǎng)度unsafe.Sizeof)姚淆,取最小值作為當(dāng)前類型的對(duì)齊值。其偏移量必須為對(duì)齊值的整數(shù)倍
  • 結(jié)構(gòu)體本身屡律,對(duì)齊值必須為編譯器默認(rèn)對(duì)齊長(zhǎng)度#pragma pack(n))或結(jié)構(gòu)體的所有成員變量類型中的最大長(zhǎng)度腌逢,取最大數(shù)的最小整數(shù)倍作為對(duì)齊值
  • 結(jié)合以上兩點(diǎn),可得知若編譯器默認(rèn)對(duì)齊長(zhǎng)度#pragma pack(n))超過(guò)結(jié)構(gòu)體內(nèi)成員變量的類型最大長(zhǎng)度時(shí)超埋,默認(rèn)對(duì)齊長(zhǎng)度是沒(méi)有任何意義的

分析流程

接下來(lái)我們一起分析一下搏讶,“它” 到底經(jīng)歷了些什么,影響了 “預(yù)期” 結(jié)果

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

成員對(duì)齊

  • 第一個(gè)成員 a
    • 類型為 bool
    • 大小/對(duì)齊值為 1 字節(jié)
    • 初始地址霍殴,偏移量為 0媒惕。占用了第 1 位
  • 第二個(gè)成員 b
    • 類型為 int32
    • 大小/對(duì)齊值為 4 字節(jié)
    • 根據(jù)規(guī)則 1,其偏移量必須為 4 的整數(shù)倍来庭。確定偏移量為 4妒蔚,因此 2-4 位為 Padding。而當(dāng)前數(shù)值從第 5 位開(kāi)始填充,到第 8 位面睛。如下:axxx|bbbb
  • 第三個(gè)成員 c
    • 類型為 int8
    • 大小/對(duì)齊值為 1 字節(jié)
    • 根據(jù)規(guī)則1絮蒿,其偏移量必須為 1 的整數(shù)倍。當(dāng)前偏移量為 8叁鉴。不需要額外對(duì)齊土涝,填充 1 個(gè)字節(jié)到第 9 位。如下:axxx|bbbb|c...
  • 第四個(gè)成員 d
    • 類型為 int64
    • 大小/對(duì)齊值為 8 字節(jié)
    • 根據(jù)規(guī)則 1幌墓,其偏移量必須為 8 的整數(shù)倍但壮。確定偏移量為 16,因此 9-16 位為 Padding常侣。而當(dāng)前數(shù)值從第 17 位開(kāi)始寫(xiě)入蜡饵,到第 24 位。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd
  • 第五個(gè)成員 e
    • 類型為 byte
    • 大小/對(duì)齊值為 1 字節(jié)
    • 根據(jù)規(guī)則 1胳施,其偏移量必須為 1 的整數(shù)倍溯祸。當(dāng)前偏移量為 24。不需要額外對(duì)齊舞肆,填充 1 個(gè)字節(jié)到第 25 位焦辅。如下:axxx|bbbb|cxxx|xxxx|dddd|dddd|e...

整體對(duì)齊

在每個(gè)成員變量進(jìn)行對(duì)齊后,根據(jù)規(guī)則 2椿胯,整個(gè)結(jié)構(gòu)體本身也要進(jìn)行字節(jié)對(duì)齊筷登,因?yàn)榭砂l(fā)現(xiàn)它可能并不是 2^n,不是偶數(shù)倍哩盲。顯然不符合對(duì)齊的規(guī)則

根據(jù)規(guī)則 2前方,可得出對(duì)齊值為 8。現(xiàn)在的偏移量為 25廉油,不是 8 的整倍數(shù)惠险。因此確定偏移量為 32。對(duì)結(jié)構(gòu)體進(jìn)行對(duì)齊

結(jié)果

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

小結(jié)

通過(guò)本節(jié)的分析抒线,可得知先前的 “推算” 為什么錯(cuò)誤莺匠?

是因?yàn)閷?shí)際內(nèi)存管理并非 “一個(gè)蘿卜一個(gè)坑” 的思想。而是一塊一塊十兢。通過(guò)空間換時(shí)間(效率)的思想來(lái)完成這塊讀取趣竣、寫(xiě)入。另外也需要兼顧不同平臺(tái)的內(nèi)存操作情況

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

在上一小節(jié)旱物,可得知根據(jù)成員變量的類型不同遥缕,其結(jié)構(gòu)體的內(nèi)存會(huì)產(chǎn)生對(duì)齊等動(dòng)作。那假設(shè)字段順序不同宵呛,會(huì)不會(huì)有什么變化呢单匣?我們一起來(lái)試試吧 :-)

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

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

接下來(lái)我們一起剖析一下 Part2户秤,看看它的內(nèi)部到底和上一位之間有什么區(qū)別码秉,才導(dǎo)致了這樣的結(jié)果?

分析流程

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

成員對(duì)齊

  • 第一個(gè)成員 e
    • 類型為 byte
    • 大小/對(duì)齊值為 1 字節(jié)
    • 初始地址鸡号,偏移量為 0转砖。占用了第 1 位
  • 第二個(gè)成員 c
    • 類型為 int8
    • 大小/對(duì)齊值為 1 字節(jié)
    • 根據(jù)規(guī)則1,其偏移量必須為 1 的整數(shù)倍鲸伴。當(dāng)前偏移量為 2府蔗。不需要額外對(duì)齊
  • 第三個(gè)成員 a
    • 類型為 bool
    • 大小/對(duì)齊值為 1 字節(jié)
    • 根據(jù)規(guī)則1,其偏移量必須為 1 的整數(shù)倍汞窗。當(dāng)前偏移量為 3姓赤。不需要額外對(duì)齊
  • 第四個(gè)成員 b
    • 類型為 int32
    • 大小/對(duì)齊值為 4 字節(jié)
    • 根據(jù)規(guī)則1,其偏移量必須為 4 的整數(shù)倍仲吏。確定偏移量為 4不铆,因此第 3 位為 Padding。而當(dāng)前數(shù)值從第 4 位開(kāi)始填充裹唆,到第 8 位誓斥。如下:ecax|bbbb
  • 第五個(gè)成員 d
    • 類型為 int64
    • 大小/對(duì)齊值為 8 字節(jié)
    • 根據(jù)規(guī)則1,其偏移量必須為 8 的整數(shù)倍品腹。當(dāng)前偏移量為 8岖食。不需要額外對(duì)齊红碑,從 9-16 位填充 8 個(gè)字節(jié)舞吭。如下:ecax|bbbb|dddd|dddd

整體對(duì)齊

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

結(jié)果

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

總結(jié)

通過(guò)對(duì)比 Part1Part2 的內(nèi)存布局析珊,你會(huì)發(fā)現(xiàn)兩者有很大的不同羡鸥。如下:

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

仔細(xì)一看,Part1 存在許多 Padding忠寻。顯然它占據(jù)了不少空間惧浴,那么 Padding 是怎么出現(xiàn)的呢?

通過(guò)本文的介紹奕剃,可得知是由于不同類型導(dǎo)致需要進(jìn)行字節(jié)對(duì)齊衷旅,以此保證內(nèi)存的訪問(wèn)邊界

那么也不難理解,為什么調(diào)整結(jié)構(gòu)體內(nèi)成員變量的字段順序就能達(dá)到縮小結(jié)構(gòu)體占用大小的疑問(wèn)了纵朋,是因?yàn)榍擅畹販p少了 Padding 的存在柿顶。讓它們更 “緊湊” 了。這一點(diǎn)對(duì)于加深 Go 的內(nèi)存布局印象和大對(duì)象的優(yōu)化非常有幫

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末操软,一起剝皮案震驚了整個(gè)濱河市嘁锯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖家乘,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝗羊,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡仁锯,警方通過(guò)查閱死者的電腦和手機(jī)耀找,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)扑馁,“玉大人涯呻,你說(shuō)我怎么就攤上這事∧逡” “怎么了复罐?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)雄家。 經(jīng)常有香客問(wèn)我效诅,道長(zhǎng),這世上最難降的妖魔是什么趟济? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任乱投,我火速辦了婚禮,結(jié)果婚禮上顷编,老公的妹妹穿的比我還像新娘戚炫。我一直安慰自己,他們只是感情好媳纬,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布双肤。 她就那樣靜靜地躺著,像睡著了一般钮惠。 火紅的嫁衣襯著肌膚如雪茅糜。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天素挽,我揣著相機(jī)與錄音蔑赘,去河邊找鬼。 笑死预明,一個(gè)胖子當(dāng)著我的面吹牛缩赛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播撰糠,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼酥馍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了窗慎?” 一聲冷哼從身側(cè)響起物喷,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤卤材,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后峦失,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體扇丛,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年尉辑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了帆精。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡隧魄,死狀恐怖卓练,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情购啄,我是刑警寧澤襟企,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站狮含,受9級(jí)特大地震影響顽悼,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜几迄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一蔚龙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧映胁,春花似錦木羹、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至妆距,卻和暖如春穷遂,著一層夾襖步出監(jiān)牢的瞬間函匕,已是汗流浹背娱据。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盅惜,地道東北人中剩。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像抒寂,于是被迫代替她去往敵國(guó)和親结啼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

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