再談Golang內(nèi)存對(duì)齊
關(guān)于 Golang 內(nèi)存對(duì)齊,之前寫了一篇「淺談Golang內(nèi)存對(duì)齊[1]」,可惜對(duì)一些細(xì)節(jié)問(wèn)題的討論語(yǔ)焉不詳,于是便有了今天這篇「再談Golang內(nèi)存對(duì)齊」。
讓我們回想一下 groupcache[2] 和 sync.WaitGroup[3] 中的做法,為了規(guī)避在 32 位環(huán)境下 atomic 操作 64 位數(shù)的 BUG,它們采取了截然不同的做法:
//?groupcache
type?Group?struct?{
?name?string
?getter?Getter
?peersOnce?sync.Once
?peers?PeerPicker
?cacheBytes?int64
?mainCache?cache
?hotCache?cache
?loadGroup?flightGroup
?_?int32?//?force?Stats?to?be?8-byte?aligned?on?32-bit?platforms
?Stats?Stats
}
//?sync.WaitGroup
type?WaitGroup?struct?{
?noCopy?noCopy
?//?64-bit?value:?high?32?bits?are?counter,?low?32?bits?are?waiter?count.
?//?64-bit?atomic?operations?require?64-bit?alignment,?but?32-bit
?//?compilers?do?not?ensure?it.?So?we?allocate?12?bytes?and?then?use
?//?the?aligned?8?bytes?in?them?as?state,?and?the?other?4?as?storage
?//?for?the?sema.
?state1?[3]uint32
}
func?(wg?*WaitGroup)?state()?(statep?*uint64,?semap?*uint32)?{
?if?uintptr(unsafe.Pointer(&wg.state1))%8?==?0?{
??return?(*uint64)(unsafe.Pointer(&wg.state1)),?&wg.state1[2]
?}?else?{
??return?(*uint64)(unsafe.Pointer(&wg.state1[1])),?&wg.state1[0]
?}
}
問(wèn)題:為什么 groupcache 不用考慮外部地址,只要內(nèi)部對(duì)齊就可以實(shí)現(xiàn) 64 位對(duì)齊?
為了搞清楚這個(gè)問(wèn)題,讓我們回想一下 atomic[4] 文檔最后關(guān)于 64 位對(duì)齊的相關(guān)描述:
On ARM, 386, and 32-bit MIPS, it is the caller’s responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.
其中我們關(guān)心的是最后一句話:變量、結(jié)構(gòu)體、數(shù)組、切片的第一個(gè)字是 64 位對(duì)齊的。為了驗(yàn)證這一點(diǎn),我構(gòu)造了一個(gè)包含 int64 的 struct,看它的地址是否是 8 的倍數(shù):
package?main
import?(
?"fmt"
?"time"
?"unsafe"
)
type?foo?struct?{
?bar?int64
}
//?GOARCH=386?go?run?main.go
func?main()?{
?for?range?time.Tick(time.Second)?{
??f?:=?&foo{}
??p?:=?uintptr(unsafe.Pointer(f))
??fmt.Printf("%p:?%d,?%d\n",?f,?p,?p%8)
?}
}
按照常理來(lái)說(shuō),當(dāng)我們?cè)?32 位環(huán)境(GOARCH=386)下運(yùn)行的時(shí)候,struct 的地址應(yīng)該只能滿足 32 位對(duì)齊,也就是 4 的倍數(shù),不過(guò)測(cè)試發(fā)現(xiàn),當(dāng) struct 里含有 int64 的時(shí)候,struct 的地址竟然滿足 64 位對(duì)齊,也就是是 8 的倍數(shù)。既然外部已經(jīng)是對(duì)齊的了,那么只要內(nèi)部對(duì)齊就可以實(shí)現(xiàn) 64 位對(duì)齊。
問(wèn)題:為什么 sync.WaitGroup 不像 groupcache 那樣實(shí)現(xiàn) 64 位對(duì)齊。
兩者之所以采用了不同的 64 位對(duì)齊實(shí)現(xiàn)方式,是因?yàn)閮烧叩氖褂脠?chǎng)景不同。在實(shí)際使用的時(shí)候,sync.WaitGroup 可能會(huì)被嵌入到別的 struct 中,因?yàn)椴恢狼度氲木唧w位置,所以不可能通過(guò)預(yù)先加入 padding 的方式來(lái)實(shí)現(xiàn) 64 位對(duì)齊,只能在運(yùn)行時(shí)動(dòng)態(tài)計(jì)算。而 groupcache 則不會(huì)被嵌入到別的 struct 中,如果你硬要嵌入,可能會(huì)出問(wèn)題:
package?main
import?(
?"github.com/golang/groupcache"
)
type?foo?struct?{
?bar?int32
?g?groupcache.Group
}
//?GOARCH=386?go?run?main.go
func?main()?{
?f?:=?foo{}
?f.g.Stats.Gets.Add(1)
}
當(dāng)我們?cè)?32 位環(huán)境(GOARCH=386)下運(yùn)行的時(shí)候,會(huì)看到如下 panic 信息:
panic: unaligned 64-bit atomic operation
當(dāng)我們?cè)?32 位環(huán)境,按 4 字節(jié)對(duì)齊,所以 g 的偏移量是 4 而不是 8,如此一來(lái),雖然 groupcache 內(nèi)部通過(guò) _ int32 實(shí)現(xiàn)了相對(duì)的 64 位對(duì)齊,但是因?yàn)橥獠繘](méi)有實(shí)現(xiàn) 64 位對(duì)齊,所以在執(zhí)行 atomic 操作的時(shí)候,還是會(huì) panic(如果 bar 是 int64 就不會(huì) panic)。
問(wèn)題:為什么 sync.WaitGroup 中的 state1 不換成 一個(gè) int64 加一個(gè) int32?
我們知道 sync.WaitGroup 中的 state1 字段是一個(gè)有 3 個(gè)元素的 uint32 數(shù)組,它會(huì)保存兩種數(shù)據(jù),分別是 statep 和 semap,相當(dāng)于一個(gè)是 int64,另一個(gè)是 int32。那為什么它不直接把一個(gè) state1 字段替換成兩個(gè)獨(dú)立的字段,一個(gè) int64 加一個(gè) int32。
當(dāng)然可以換,但是因?yàn)?sync.WaitGroup 可能會(huì)被嵌入到別的 struct 中,并且不知道嵌入的具體位置,所以還是需要在運(yùn)行時(shí)動(dòng)態(tài)計(jì)算是否要 padding,并且這個(gè) padding 的工作要額外空間來(lái)承擔(dān),不能被獨(dú)立的 int32 兼任。和原方案比無(wú)疑浪費(fèi)了空間。
問(wèn)題:為什么 sync.WaitGroup 中的 state1 不換成 一個(gè)[12]byte?
原方案中 state1 的類型是 [3]uint32,取兩個(gè) uint32 做 statep,剩下的一個(gè) uint32 做 semap。為什么不換成 [12]byte,取 8 個(gè) byte 做 statep,剩下的 4 個(gè) byte 做 semap?
想要搞清楚這個(gè)問(wèn)題,我們想要回顧一下 golang 關(guān)于內(nèi)存對(duì)齊保證的描述:
For a variable x of any type: unsafe.Alignof(x) is at least 1. For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1. For a variable x of array type: unsafe.Alignof(x) is the same as the alignment of a variable of the array’s element type.
其中的重點(diǎn)是:對(duì) struct 而言,它的對(duì)齊取決于其中所有字段對(duì)齊的最大值;對(duì)于 array 而言,它的對(duì)齊等于元素類型本身的對(duì)齊。因?yàn)?noCopy 的大小是 0,所以 struct 的對(duì)齊實(shí)際上就取決于 state1 字段的對(duì)齊。
當(dāng) state1 的類型是 [3]uint32 的時(shí)候,那么 struct 的對(duì)齊就是 4。 當(dāng) state1 的類型是 [12]byte 的時(shí)候,那么 struct 的對(duì)齊就是 1。
如果 state1 換成 [12]byte,那么因?yàn)?struct 的對(duì)齊是 1,會(huì)導(dǎo)致 struct 的地址不再是 4 的倍數(shù),結(jié)果 uintptr(unsafe.Pointer(&wg.state1))%8 == 0 的判斷也就沒(méi)有意義了。
參考資料
淺談Golang內(nèi)存對(duì)齊: https://blog.huoding.com/2021/09/29/951
[2]groupcache: https://github.com/golang/groupcache/blob/master/groupcache.go
[3]sync.WaitGroup: https://github.com/golang/go/blob/master/src/sync/waitgroup.go
[4]atomic: https://pkg.go.dev/sync/atomic
推薦閱讀
