摘要:例如特定的硬件平臺只允許在特定地址獲取特定類型的數據,否則會導致異常情況性能原因若訪問未對齊的內存,將會導致進行兩次內存訪問,并且要花費額外的時鐘周期來處理對齊及運算。因為它的內存訪問邊界是不對齊的。
原文地址:在 Go 中恰到好處的內存對齊
問題type Part1 struct { a bool b int32 c int8 d int64 e byte }
在開始之前,希望你計算一下 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")) }
輸出結果:
bool size: 1 int32 size: 4 int8 size: 1 int64 size: 8 byte size: 1 string size: 16
這么一算,Part1 這一個結構體的占用內存大小為 1+4+1+8+1 = 15 個字節。相信有的小伙伴是這么算的,看上去也沒什么毛病
真實情況是怎么樣的呢?我們實際調用看看,如下:
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)) }
輸出結果:
part1 size: 32, align: 8
最終輸出為占用 32 個字節。這與前面所預期的結果完全不一樣。這充分地說明了先前的計算方式是錯誤的。為什么呢?
在這里要提到 “內存對齊” 這一概念,才能夠用正確的姿勢去計算,接下來我們詳細的講講它是什么
內存對齊有的小伙伴可能會認為內存讀取,就是一個簡單的字節數組擺放
上圖表示一個坑一個蘿卜的內存讀取方式。但實際上 CPU 并不會以一個一個字節去讀取和寫入內存。相反 CPU 讀取內存是一塊一塊讀取的,塊的大小可以為 2、4、6、8、16 字節等大小。塊大小我們稱其為內存訪問粒度。如下圖:
在樣例中,假設訪問粒度為 4。 CPU 是以每 4 個字節大小的訪問粒度去讀取和寫入內存的。這才是正確的姿勢
為什么要關心對齊你正在編寫的代碼在性能(CPU、Memory)方面有一定的要求
你正在處理向量方面的指令
某些硬件平臺(ARM)體系不支持未對齊的內存訪問
另外作為一個工程師,你也很有必要學習這塊知識點哦 :)
為什么要做對齊平臺(移植性)原因:不是所有的硬件平臺都能夠訪問任意地址上的任意數據。例如:特定的硬件平臺只允許在特定地址獲取特定類型的數據,否則會導致異常情況
性能原因:若訪問未對齊的內存,將會導致 CPU 進行兩次內存訪問,并且要花費額外的時鐘周期來處理對齊及運算。而本身就對齊的內存僅需要一次訪問就可以完成讀取動作
在上圖中,假設從 Index 1 開始讀取,將會出現很崩潰的問題。因為它的內存訪問邊界是不對齊的。因此 CPU 會做一些額外的處理工作。如下:
CPU 首次讀取未對齊地址的第一個內存塊,讀取 0-3 字節。并移除不需要的字節 0
CPU 再次讀取未對齊地址的第二個內存塊,讀取 4-7 字節。并移除不需要的字節 5、6、7 字節
合并 1-4 字節的數據
合并后放入寄存器
從上述流程可得出,不做 “內存對齊” 是一件有點 "麻煩" 的事。因為它會增加許多耗費時間的動作
而假設做了內存對齊,從 Index 0 開始讀取 4 個字節,只需要讀取一次,也不需要額外的運算。這顯然高效很多,是標準的空間換時間做法
默認系數在不同平臺上的編譯器都有自己默認的 “對齊系數”,可通過預編譯命令 #pragma pack(n) 進行變更,n 就是代指 “對齊系數”。一般來講,我們常用的平臺的系數如下:
32 位:4
64 位:8
另外要注意,不同硬件平臺占用的大小和對齊值都可能是不一樣的。因此本文的值不是唯一的,調試的時候需按本機的實際情況考慮
成員對齊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{})) }
輸出結果:
bool align: 1 int32 align: 4 int8 align: 1 int64 align: 8 byte align: 1 string align: 8 map align: 8
在 Go 中可以調用 unsafe.Alignof 來返回相應類型的對齊系數。通過觀察輸出結果,可得知基本都是 2^n,最大也不會超過 8。這是因為我手提(64 位)編譯器默認對齊系數是 8,因此最大值不會超過這個數
整體對齊在上小節中,提到了結構體中的成員變量要做字節對齊。那么想當然身為最終結果的結構體,也是需要做字節對齊的
對齊規則結構體的成員變量,第一個成員變量的偏移量為 0。往后的每個成員變量的對齊值必須為編譯器默認對齊長度(#pragma pack(n))或當前成員變量類型的長度(unsafe.Sizeof),取最小值作為當前類型的對齊值。其偏移量必須為對齊值的整數倍
結構體本身,對齊值必須為編譯器默認對齊長度(#pragma pack(n))或結構體的所有成員變量類型中的最大長度,取最大數的最小整數倍作為對齊值
結合以上兩點,可得知若編譯器默認對齊長度(#pragma pack(n))超過結構體內成員變量的類型最大長度時,默認對齊長度是沒有任何意義的
分析流程接下來我們一起分析一下,“它” 到底經歷了些什么,影響了 “預期” 結果
成員變量 | 類型 | 偏移量 | 自身占用 |
---|---|---|---|
a | bool | 0 | 1 |
字節對齊 | 無 | 1 | 3 |
b | int32 | 4 | 4 |
c | int8 | 8 | 1 |
字節對齊 | 無 | 9 | 7 |
d | int64 | 16 | 8 |
e | byte | 24 | 1 |
字節對齊 | 無 | 25 | 7 |
總占用大小 | - | - | 32 |
第一個成員 a
類型為 bool
大小/對齊值為 1 字節
初始地址,偏移量為 0。占用了第 1 位
第二個成員 b
類型為 int32
大小/對齊值為 4 字節
根據規則 1,其偏移量必須為 4 的整數倍。確定偏移量為 4,因此 2-4 位為 Padding。而當前數值從第 5 位開始填充,到第 8 位。如下:axxx|bbbb
第三個成員 c
類型為 int8
大小/對齊值為 1 字節
根據規則1,其偏移量必須為 1 的整數倍。當前偏移量為 8。不需要額外對齊,填充 1 個字節到第 9 位。如下:axxx|bbbb|c...
第四個成員 d
類型為 int64
大小/對齊值為 8 字節
根據規則 1,其偏移量必須為 8 的整數倍。確定偏移量為 16,因此 9-16 位為 Padding。而當前數值從第 17 位開始寫入,到第 24 位。如下:axxx|bbbb|cxxx|xxxx|ffffdd|ffffdd
第五個成員 e
類型為 byte
大小/對齊值為 1 字節
根據規則 1,其偏移量必須為 1 的整數倍。當前偏移量為 24。不需要額外對齊,填充 1 個字節到第 25 位。如下:axxx|bbbb|cxxx|xxxx|ffffdd|ffffdd|e...
整體對齊在每個成員變量進行對齊后,根據規則 2,整個結構體本身也要進行字節對齊,因為可發現它可能并不是 2^n,不是偶數倍。顯然不符合對齊的規則
根據規則 2,可得出對齊值為 8。現在的偏移量為 25,不是 8 的整倍數。因此確定偏移量為 32。對結構體進行對齊
結果Part1 內存布局:axxx|bbbb|cxxx|xxxx|ffffdd|ffffdd|exxx|xxxx
小結通過本節的分析,可得知先前的 “推算” 為什么錯誤?
是因為實際內存管理并非 “一個蘿卜一個坑” 的思想。而是一塊一塊。通過空間換時間(效率)的思想來完成這塊讀取、寫入。另外也需要兼顧不同平臺的內存操作情況
巧妙的結構體在上一小節,可得知根據成員變量的類型不同,其結構體的內存會產生對齊等動作。那假設字段順序不同,會不會有什么變化呢?我們一起來試試吧 :-)
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)) }
輸出結果:
part1 size: 32, align: 8 part2 size: 16, align: 8
通過結果可以驚喜的發現,只是 “簡單” 對成員變量的字段順序進行改變,就改變了結構體占用大小
接下來我們一起剖析一下 Part2,看看它的內部到底和上一位之間有什么區別,才導致了這樣的結果?
分析流程成員變量 | 類型 | 偏移量 | 自身占用 |
---|---|---|---|
e | byte | 0 | 1 |
c | int8 | 1 | 1 |
a | bool | 2 | 1 |
字節對齊 | 無 | 3 | 1 |
b | int32 | 4 | 4 |
d | int64 | 8 | 8 |
總占用大小 | - | - | 16 |
第一個成員 e
類型為 byte
大小/對齊值為 1 字節
初始地址,偏移量為 0。占用了第 1 位
第二個成員 c
類型為 int8
大小/對齊值為 1 字節
根據規則1,其偏移量必須為 1 的整數倍。當前偏移量為 2。不需要額外對齊
第三個成員 a
類型為 bool
大小/對齊值為 1 字節
根據規則1,其偏移量必須為 1 的整數倍。當前偏移量為 3。不需要額外對齊
第四個成員 b
類型為 int32
大小/對齊值為 4 字節
根據規則1,其偏移量必須為 4 的整數倍。確定偏移量為 4,因此第 3 位為 Padding。而當前數值從第 4 位開始填充,到第 8 位。如下:ecax|bbbb
第五個成員 d
類型為 int64
大小/對齊值為 8 字節
根據規則1,其偏移量必須為 8 的整數倍。當前偏移量為 8。不需要額外對齊,從 9-16 位填充 8 個字節。如下:ecax|bbbb|ffffdd|ffffdd
整體對齊符合規則 2,不需要額外對齊
結果Part2 內存布局:ecax|bbbb|ffffdd|ffffdd
總結通過對比 Part1 和 Part2 的內存布局,你會發現兩者有很大的不同。如下:
Part1:axxx|bbbb|cxxx|xxxx|ffffdd|ffffdd|exxx|xxxx
Part2:ecax|bbbb|ffffdd|ffffdd
仔細一看,Part1 存在許多 Padding。顯然它占據了不少空間,那么 Padding 是怎么出現的呢?
通過本文的介紹,可得知是由于不同類型導致需要進行字節對齊,以此保證內存的訪問邊界
那么也不難理解,為什么調整結構體內成員變量的字段順序就能達到縮小結構體占用大小的疑問了,是因為巧妙地減少了 Padding 的存在。讓它們更 “緊湊” 了。這一點對于加深 Go 的內存布局印象和大對象的優化非常有幫
當然了,沒什么特殊問題,你可以不關注這一塊。但你要知道這塊知識點
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/29846.html
摘要:原文作者原題的代碼庫使用編碼風格。此外,推薦的用于連續行的編碼風格毫無一點品味,絕不允許在代碼庫使用與開局定界符對齊。但是,關于靈活設定行長的部分,我舉雙手雙腳贊同。關于代碼風格,沒有絕對完全一致的標準。 showImg(https://segmentfault.com/img/bVbnv2L?w=6000&h=4000); 原文:https://www.kennethreitz.or...
摘要:使用類來顯示文本字體和顏色大小和文本的外觀風格被指定。為了換行的情況發生,屬性不能為無,必須有一些限制寬窄文本塊默認文本塊不換行剪裁文本塊換行文本塊清理邊距對齊方式屬性指定的尺寸內繪制文字點排列方式。注驗證稍后完善。 使用TextBlocks類來顯示文本. 字體和顏色 大小和文本的外觀風格被指定TextBlock.font。值可以是任何CSS字體符串。文本顏色使用TextBlock.s...
摘要:表板中的每個對象被放入由的值索引的和。面板會看行和列的所有在面板中的對象,以確定該表應多少行和列。一行一列一行二列二行一列二行三列請注意,并非表中的每一個列需要有一個存在。屬性指定的寬度和或高度是否應該承擔全部由面板給它的空間。 表板中的每個對象被放入由的值索引的GraphObject.row和GraphObject.column。面板會看行和列的所有在面板中的對象,以確定該表應多少行...
閱讀 1136·2019-08-30 12:44
閱讀 642·2019-08-29 13:03
閱讀 2551·2019-08-28 18:15
閱讀 2419·2019-08-26 10:41
閱讀 3082·2019-08-26 10:28
閱讀 3029·2019-08-23 16:54
閱讀 1983·2019-08-23 15:16
閱讀 802·2019-08-23 14:55