摘要:例如特定的硬件平臺(tái)只允許在特定地址獲取特定類(lèi)型的數(shù)據(jù),否則會(huì)導(dǎo)致異常情況性能原因若訪(fǎng)問(wèn)未對(duì)齊的內(nèi)存,將會(huì)導(dǎo)致進(jìn)行兩次內(nèi)存訪(fǎng)問(wèn),并且要花費(fèi)額外的時(shí)鐘周期來(lái)處理對(duì)齊及運(yùn)算。因?yàn)樗膬?nèi)存訪(fǎng)問(wèn)邊界是不對(duì)齊的。
原文地址:在 Go 中恰到好處的內(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 ", unsafe.Sizeof(bool(true))) fmt.Printf("int32 size: %d ", unsafe.Sizeof(int32(0))) fmt.Printf("int8 size: %d ", unsafe.Sizeof(int8(0))) fmt.Printf("int64 size: %d ", unsafe.Sizeof(int64(0))) fmt.Printf("byte size: %d ", unsafe.Sizeof(byte(0))) fmt.Printf("string size: %d ", 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 ", 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é)等大小。塊大小我們稱(chēng)其為內(nèi)存訪(fǎng)問(wèn)粒度。如下圖:
在樣例中,假設(shè)訪(fǎng)問(wèn)粒度為 4。 CPU 是以每 4 個(gè)字節(jié)大小的訪(fǎng)問(wèn)粒度去讀取和寫(xiě)入內(nèi)存的。這才是正確的姿勢(shì)
為什么要關(guān)心對(duì)齊你正在編寫(xiě)的代碼在性能(CPU、Memory)方面有一定的要求
你正在處理向量方面的指令
某些硬件平臺(tái)(ARM)體系不支持未對(duì)齊的內(nèi)存訪(fǎng)問(wèn)
另外作為一個(gè)工程師,你也很有必要學(xué)習(xí)這塊知識(shí)點(diǎn)哦 :)
為什么要做對(duì)齊平臺(tái)(移植性)原因:不是所有的硬件平臺(tái)都能夠訪(fǎng)問(wèn)任意地址上的任意數(shù)據(jù)。例如:特定的硬件平臺(tái)只允許在特定地址獲取特定類(lèi)型的數(shù)據(jù),否則會(huì)導(dǎo)致異常情況
性能原因:若訪(fǎng)問(wèn)未對(duì)齊的內(nèi)存,將會(huì)導(dǎo)致 CPU 進(jìn)行兩次內(nèi)存訪(fǎng)問(wèn),并且要花費(fèi)額外的時(shí)鐘周期來(lái)處理對(duì)齊及運(yùn)算。而本身就對(duì)齊的內(nèi)存僅需要一次訪(fǎng)問(wèn)就可以完成讀取動(dòng)作
在上圖中,假設(shè)從 Index 1 開(kāi)始讀取,將會(huì)出現(xiàn)很崩潰的問(wèn)題。因?yàn)樗膬?nèi)存訪(fǎng)問(wèn)邊界是不對(duì)齊的。因此 CPU 會(huì)做一些額外的處理工作。如下:
CPU 首次讀取未對(duì)齊地址的第一個(gè)內(nèi)存塊,讀取 0-3 字節(jié)。并移除不需要的字節(jié) 0
CPU 再次讀取未對(duì)齊地址的第二個(gè)內(nèi)存塊,讀取 4-7 字節(jié)。并移除不需要的字節(jié) 5、6、7 字節(jié)
合并 1-4 字節(jié)的數(shù)據(jù)
合并后放入寄存器
從上述流程可得出,不做 “內(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 ", unsafe.Alignof(bool(true))) fmt.Printf("int32 align: %d ", unsafe.Alignof(int32(0))) fmt.Printf("int8 align: %d ", unsafe.Alignof(int8(0))) fmt.Printf("int64 align: %d ", unsafe.Alignof(int64(0))) fmt.Printf("byte align: %d ", unsafe.Alignof(byte(0))) fmt.Printf("string align: %d ", unsafe.Alignof("EDDYCJY")) fmt.Printf("map align: %d ", 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)類(lèi)型的對(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)前成員變量類(lèi)型的長(zhǎng)度(unsafe.Sizeof),取最小值作為當(dāng)前類(lèi)型的對(duì)齊值。其偏移量必須為對(duì)齊值的整數(shù)倍
結(jié)構(gòu)體本身,對(duì)齊值必須為編譯器默認(rèn)對(duì)齊長(zhǎng)度(#pragma pack(n))或結(jié)構(gòu)體的所有成員變量類(lèi)型中的最大長(zhǎng)度,取最大數(shù)的最小整數(shù)倍作為對(duì)齊值
結(jié)合以上兩點(diǎn),可得知若編譯器默認(rèn)對(duì)齊長(zhǎng)度(#pragma pack(n))超過(guò)結(jié)構(gòu)體內(nèi)成員變量的類(lèi)型最大長(zhǎng)度時(shí),默認(rèn)對(duì)齊長(zhǎng)度是沒(méi)有任何意義的
分析流程接下來(lái)我們一起分析一下,“它” 到底經(jīng)歷了些什么,影響了 “預(yù)期” 結(jié)果
成員變量 | 類(lèi)型 | 偏移量 | 自身占用 |
---|---|---|---|
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 |
第一個(gè)成員 a
類(lèi)型為 bool
大小/對(duì)齊值為 1 字節(jié)
初始地址,偏移量為 0。占用了第 1 位
第二個(gè)成員 b
類(lèi)型為 int32
大小/對(duì)齊值為 4 字節(jié)
根據(jù)規(guī)則 1,其偏移量必須為 4 的整數(shù)倍。確定偏移量為 4,因此 2-4 位為 Padding。而當(dāng)前數(shù)值從第 5 位開(kāi)始填充,到第 8 位。如下:axxx|bbbb
第三個(gè)成員 c
類(lèi)型為 int8
大小/對(duì)齊值為 1 字節(jié)
根據(jù)規(guī)則1,其偏移量必須為 1 的整數(shù)倍。當(dāng)前偏移量為 8。不需要額外對(duì)齊,填充 1 個(gè)字節(jié)到第 9 位。如下:axxx|bbbb|c...
第四個(gè)成員 d
類(lèi)型為 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|ffffdd|ffffdd
第五個(gè)成員 e
類(lèi)型為 byte
大小/對(duì)齊值為 1 字節(jié)
根據(jù)規(guī)則 1,其偏移量必須為 1 的整數(shù)倍。當(dāng)前偏移量為 24。不需要額外對(duì)齊,填充 1 個(gè)字節(jié)到第 25 位。如下:axxx|bbbb|cxxx|xxxx|ffffdd|ffffdd|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?,F(xiàn)在的偏移量為 25,不是 8 的整倍數(shù)。因此確定偏移量為 32。對(duì)結(jié)構(gòu)體進(jìn)行對(duì)齊
結(jié)果Part1 內(nèi)存布局:axxx|bbbb|cxxx|xxxx|ffffdd|ffffdd|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ù)成員變量的類(lèi)型不同,其結(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 ", unsafe.Sizeof(part1), unsafe.Alignof(part1)) fmt.Printf("part2 size: %d, align: %d ", 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é)果?
分析流程成員變量 | 類(lèi)型 | 偏移量 | 自身占用 |
---|---|---|---|
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 |
第一個(gè)成員 e
類(lèi)型為 byte
大小/對(duì)齊值為 1 字節(jié)
初始地址,偏移量為 0。占用了第 1 位
第二個(gè)成員 c
類(lèi)型為 int8
大小/對(duì)齊值為 1 字節(jié)
根據(jù)規(guī)則1,其偏移量必須為 1 的整數(shù)倍。當(dāng)前偏移量為 2。不需要額外對(duì)齊
第三個(gè)成員 a
類(lèi)型為 bool
大小/對(duì)齊值為 1 字節(jié)
根據(jù)規(guī)則1,其偏移量必須為 1 的整數(shù)倍。當(dāng)前偏移量為 3。不需要額外對(duì)齊
第四個(gè)成員 b
類(lèi)型為 int32
大小/對(duì)齊值為 4 字節(jié)
根據(jù)規(guī)則1,其偏移量必須為 4 的整數(shù)倍。確定偏移量為 4,因此第 3 位為 Padding。而當(dāng)前數(shù)值從第 4 位開(kāi)始填充,到第 8 位。如下:ecax|bbbb
第五個(gè)成員 d
類(lèi)型為 int64
大小/對(duì)齊值為 8 字節(jié)
根據(jù)規(guī)則1,其偏移量必須為 8 的整數(shù)倍。當(dāng)前偏移量為 8。不需要額外對(duì)齊,從 9-16 位填充 8 個(gè)字節(jié)。如下:ecax|bbbb|ffffdd|ffffdd
整體對(duì)齊符合規(guī)則 2,不需要額外對(duì)齊
結(jié)果Part2 內(nèi)存布局:ecax|bbbb|ffffdd|ffffdd
總結(jié)通過(guò)對(duì)比 Part1 和 Part2 的內(nèi)存布局,你會(huì)發(fā)現(xiàn)兩者有很大的不同。如下:
Part1:axxx|bbbb|cxxx|xxxx|ffffdd|ffffdd|exxx|xxxx
Part2:ecax|bbbb|ffffdd|ffffdd
仔細(xì)一看,Part1 存在許多 Padding。顯然它占據(jù)了不少空間,那么 Padding 是怎么出現(xiàn)的呢?
通過(guò)本文的介紹,可得知是由于不同類(lèi)型導(dǎo)致需要進(jìn)行字節(jié)對(duì)齊,以此保證內(nèi)存的訪(fǎng)問(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)化非常有幫
當(dāng)然了,沒(méi)什么特殊問(wèn)題,你可以不關(guān)注這一塊。但你要知道這塊知識(shí)點(diǎn)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/29846.html
摘要:原文作者原題的代碼庫(kù)使用編碼風(fēng)格。此外,推薦的用于連續(xù)行的編碼風(fēng)格毫無(wú)一點(diǎn)品味,絕不允許在代碼庫(kù)使用與開(kāi)局定界符對(duì)齊。但是,關(guān)于靈活設(shè)定行長(zhǎng)的部分,我舉雙手雙腳贊同。關(guān)于代碼風(fēng)格,沒(méi)有絕對(duì)完全一致的標(biāo)準(zhǔn)。 showImg(https://segmentfault.com/img/bVbnv2L?w=6000&h=4000); 原文:https://www.kennethreitz.or...
摘要:使用類(lèi)來(lái)顯示文本字體和顏色大小和文本的外觀風(fēng)格被指定。為了換行的情況發(fā)生,屬性不能為無(wú),必須有一些限制寬窄文本塊默認(rèn)文本塊不換行剪裁文本塊換行文本塊清理邊距對(duì)齊方式屬性指定的尺寸內(nèi)繪制文字點(diǎn)排列方式。注驗(yàn)證稍后完善。 使用TextBlocks類(lèi)來(lái)顯示文本. 字體和顏色 大小和文本的外觀風(fēng)格被指定TextBlock.font。值可以是任何CSS字體符串。文本顏色使用TextBlock.s...
摘要:表板中的每個(gè)對(duì)象被放入由的值索引的和。面板會(huì)看行和列的所有在面板中的對(duì)象,以確定該表應(yīng)多少行和列。一行一列一行二列二行一列二行三列請(qǐng)注意,并非表中的每一個(gè)列需要有一個(gè)存在。屬性指定的寬度和或高度是否應(yīng)該承擔(dān)全部由面板給它的空間。 表板中的每個(gè)對(duì)象被放入由的值索引的GraphObject.row和GraphObject.column。面板會(huì)看行和列的所有在面板中的對(duì)象,以確定該表應(yīng)多少行...
閱讀 1146·2019-08-30 12:44
閱讀 653·2019-08-29 13:03
閱讀 2562·2019-08-28 18:15
閱讀 2431·2019-08-26 10:41
閱讀 3092·2019-08-26 10:28
閱讀 3040·2019-08-23 16:54
閱讀 1992·2019-08-23 15:16
閱讀 817·2019-08-23 14:55