什么是內(nèi)存對(duì)齊呢
簡(jiǎn)單說就是程序運(yùn)行過程中疗杉,程序中的變量在內(nèi)存中的分布情況,為什么要有對(duì)齊這個(gè)問題呢椎组,是因?yàn)椴煌愋偷淖兞空加脙?nèi)存的大小是不一樣的丹拯,但是cpu每次讀取的內(nèi)存長(zhǎng)度是固定的,為了cpu能高效的讀寫數(shù)據(jù)(cpu讀取數(shù)據(jù)不是一個(gè)字節(jié)一個(gè)字節(jié)讀取的斤程,一次讀取的一塊內(nèi)存)角寸,所以編譯器在編譯的時(shí)候會(huì)通過填充空數(shù)據(jù)(數(shù)據(jù)不是連續(xù)的),讓一個(gè)變量忿墅,使cpu能一次操作就能完成讀寫扁藕。還有跨平臺(tái)的問題,有的平臺(tái)不支持訪問任意地址上的任意數(shù)據(jù)疚脐,必須按照順序依次按塊讀取亿柑。
一個(gè)簡(jiǎn)單的例子看一下:
package main
import (
"fmt"
"unsafe"
)
type One struct {
a bool
b int8
c int32
}
type Two struct {
a bool
c int32
b int8
}
func main() {
fmt.Println(unsafe.Sizeof(One{}))
fmt.Println(unsafe.Sizeof(Two{}))
}
這兩個(gè)結(jié)構(gòu)體在運(yùn)行時(shí),占用的內(nèi)存大小棍弄,看起來應(yīng)該來說是一樣大的橄杨,畢竟內(nèi)部的變量是一樣的,只是順序不同而已照卦。
但實(shí)際上式矫,占用的內(nèi)存是不一樣的,看一下運(yùn)行結(jié)果:
go run main.go
4
12
為什么會(huì)這樣呢役耕,畫一下內(nèi)存的示意圖就清楚了
One的內(nèi)存
Two的內(nèi)存
圖中灰色的塊是為了對(duì)齊而填充的無用區(qū)域采转,可見 One 的 內(nèi)存利用率比 Two 要高好多,因?yàn)閏pu一次讀取的內(nèi)存大小是4個(gè)字節(jié)瞬痘,也就是32位故慈。為了能讓cpu一次就讀取完這個(gè)變量,所以編譯器就做了一些調(diào)整框全,使內(nèi)存利用率低了察绷,但是卻提高了效率。
這里又帶來一個(gè)問題津辩,cpu一次讀取數(shù)據(jù)的長(zhǎng)度是如何確定的拆撼,這個(gè)叫對(duì)齊系數(shù)。
go中有一個(gè)函數(shù)喘沿,可以計(jì)算出對(duì)齊系數(shù)闸度,一段代碼看一下
package main
import (
"fmt"
"unsafe"
)
type One struct {
a bool
b int8
}
type Two struct {
a bool
c int32
b int8
}
type Three struct {
a bool
b int8
c int64
}
func main() {
fmt.Println(unsafe.Alignof(One{}))
fmt.Println(unsafe.Alignof(Two{}))
fmt.Println(unsafe.Alignof(Three{}))
}
運(yùn)行結(jié)果如下
go run main.go
1
4
8
可以得到,cpu一次讀取的數(shù)據(jù)長(zhǎng)度和這結(jié)構(gòu)體中的最大的數(shù)據(jù)有關(guān)蚜印。
One 中最大的數(shù)據(jù)長(zhǎng)度是1個(gè)字節(jié), Two 中最大的數(shù)據(jù)長(zhǎng)度是4個(gè)字節(jié), Three 中最大的數(shù)據(jù)長(zhǎng)度是8個(gè)字節(jié)
如果我換成 32位操作系統(tǒng) 呢
$env:GOARCH="386"
go run main.go
1
4
4
此時(shí)的對(duì)齊系數(shù)變成了4莺禁,因?yàn)?2位的系統(tǒng)一次能處理的數(shù)據(jù)長(zhǎng)度就是 4個(gè)字節(jié),說明對(duì)齊系數(shù)還和操作系統(tǒng)有關(guān)窄赋。
所以:對(duì)齊系數(shù)和結(jié)構(gòu)體中最大的的數(shù)據(jù)有關(guān)哟冬,同時(shí)和操作系統(tǒng)也有關(guān)楼熄。取這兩個(gè)條件中的小值
內(nèi)存對(duì)齊的利弊
利
- 高效. 數(shù)據(jù)只需要一次就能完成讀寫,效率肯定比多次讀寫要高效
- 數(shù)據(jù)原子性. 還是數(shù)據(jù)一次就能完成讀寫浩峡,保證了原子性
弊
那就是內(nèi)存的利用率變低了孝赫,這個(gè)可以通過優(yōu)化來解決一部分
看一下如果沒有內(nèi)存對(duì)齊的情況吧,如果在32位的系統(tǒng)上红符,變量C要經(jīng)過兩次才能讀到
不同類型的對(duì)齊系數(shù)
在64位操作系統(tǒng)中
類型 | 系數(shù) |
---|---|
bool, byte, uint8, int8 | 1 |
uint16, int16 | 2 |
uint32, int32 | 4 |
float32, complex64 | 4 |
int, uint, int64, uint64,string,uintptr,float64 | 8 |
有個(gè)特殊的類型 struct{}
空結(jié)構(gòu)體青柄,這個(gè)結(jié)構(gòu)體的空間大小是0,但是計(jì)算出的對(duì)齊系數(shù)是1预侯。
而且 struct{}
的位置不同致开,占用的空間也不相同
package main
import (
"fmt"
"unsafe"
)
type One struct {
s struct{}
x int
}
type Two struct {
x int
s struct{}
}
func main() {
fmt.Println(unsafe.Sizeof(One{}))
fmt.Println(unsafe.Sizeof(Two{}))
}
運(yùn)行結(jié)果如下:
go run main.go
8
16
One 和 Two 中,只是 struct{}
的位置不同萎馅,就會(huì)導(dǎo)致內(nèi)存占用不同
之所以這樣是因?yàn)樗粒?dāng)struct{}
作為結(jié)構(gòu)體最后一個(gè)字段時(shí),如果struct{}
不做填充糜芳,就會(huì)導(dǎo)致指向 struct{}
的地址指向了 結(jié)構(gòu)體之外了飒货,所以為了出錯(cuò),就會(huì)填充一個(gè)數(shù)據(jù)位置峭竣,也就是一個(gè) int的大小塘辅。
如何優(yōu)化內(nèi)存布局
首先,go的編譯器不會(huì)做內(nèi)存對(duì)齊的優(yōu)化皆撩,也就是在編譯期間扣墩,編譯器不會(huì)調(diào)整結(jié)構(gòu)體中字段的順序。至于為啥不做扛吞,我還沒想通呻惕,有知道的歡迎評(píng)論區(qū)探討。
所以內(nèi)存對(duì)齊的優(yōu)化需要我們自己做滥比。
我自己總結(jié)的兩點(diǎn)亚脆,歡迎補(bǔ)充
- 盡量把相同類型的變量放到一起
- 把小的數(shù)據(jù)放到前面,大的數(shù)據(jù)放到后面
好了盲泛,常見的go內(nèi)存對(duì)齊的問題 差不多就是這些了濒持。
說實(shí)話,我在工作中不是特別注意內(nèi)存對(duì)齊的問題查乒,只有在想到的時(shí)候才會(huì)做一下弥喉。這個(gè)東西帶來的收益不是特別大郁竟,在內(nèi)存動(dòng)不動(dòng)16G起步的時(shí)代玛迄,這點(diǎn)優(yōu)化可以忽略不計(jì)的,當(dāng)做知識(shí)了解一下就好了棚亩。