本文將介紹Go中的各種類型的尺寸和對(duì)齊保證。 知曉這些保證對(duì)于估計(jì)結(jié)構(gòu)體值的尺寸和正確使用64位整數(shù)原子操作函數(shù)是必要的。
Go是一門屬于C語(yǔ)言家族的編程語(yǔ)言,所以本文談及的很多概念和C語(yǔ)言是相通的。
為了充分利用CPU指令來(lái)達(dá)到最佳程序性能,為一個(gè)特定類型的值開辟的內(nèi)存塊的起始地址必須為某個(gè)整數(shù)N的倍數(shù)。 N被稱為此類型的值地址對(duì)齊保證,或者簡(jiǎn)單地稱為此類型的對(duì)齊保證。 我們也可以說(shuō)此類型的值的地址保證為N字節(jié)對(duì)齊的。
事實(shí)上,每個(gè)類型有兩個(gè)對(duì)齊保證。當(dāng)它被用做結(jié)構(gòu)體類型的字段類型時(shí)的對(duì)齊保證稱為此類型的字段對(duì)齊保證,其它情形的對(duì)齊保證稱為此類型的一般對(duì)齊保證。
對(duì)于一個(gè)類型T
,我們可以調(diào)用unsafe.Alignof(t)
來(lái)獲得它的一般對(duì)齊保證,其中t
為一個(gè)T
類型的非字段值, 也可以調(diào)用unsafe.Alignof(x.t)
來(lái)獲得T
的字段對(duì)齊保證,其中x
為一個(gè)結(jié)構(gòu)體值并且t
為一個(gè)類型為T
的結(jié)構(gòu)體字段值。
unsafe
標(biāo)準(zhǔn)庫(kù)包中的函數(shù)的調(diào)用都是在編譯時(shí)刻估值的。
在運(yùn)行時(shí)刻,對(duì)于類型為T
的一個(gè)值t
,我們可以調(diào)用reflect.TypeOf(t).Align()
來(lái)獲得類型T
的一般對(duì)齊保證, 也可以調(diào)用reflect.TypeOf(t).FieldAlign()
來(lái)獲得T
的字段對(duì)齊保證。
對(duì)于當(dāng)前的官方Go標(biāo)準(zhǔn)編譯器(1.19版本),一個(gè)類型的一般對(duì)齊保證和字段對(duì)齊保證總是相等的。對(duì)于gccgo編譯器,這兩者可能不相等。
Go白皮書僅列出了些許類型對(duì)齊保證要求。
一個(gè)合格的Go編譯器必須保證:
1. 對(duì)于任何類型的變量x
,unsafe.Alignof(x)
的結(jié)果最小為1
。
2. 對(duì)于一個(gè)結(jié)構(gòu)體類型的變量x
,unsafe.Alignof(x)
的結(jié)果為x
的所有字段的對(duì)齊保證unsafe.Alignof(x.f)
中的最大值(但是最小為1
)。
3. 對(duì)于一個(gè)數(shù)組類型的變量x
,unsafe.Alignof(x)
的結(jié)果和此數(shù)組的元素類型的一個(gè)變量的對(duì)齊保證相等。
從這些要求可以看出,Go白皮書并未為任何類型指定了確定的對(duì)齊保證要求,它只是指定了一些最基本的要求。
即使對(duì)于同一個(gè)編譯器,具體類型的對(duì)齊保證在不同的架構(gòu)上也是不相同的。 同一個(gè)編譯器的不同版本做出的具體類型的對(duì)齊保證也有可能是不相同的。 當(dāng)前版本(1.19)的標(biāo)準(zhǔn)編譯器做出的對(duì)齊保證列在了下面:
類型種類 對(duì)齊保證(字節(jié)數(shù))
------ ------
bool, uint8, int8 1
uint16, int16 2
uint32, int32 4
float32, complex64 4
數(shù)組 取決于元素類型
結(jié)構(gòu)體類型 取決于各個(gè)字段類型
其它類型 一個(gè)自然字的尺寸
這里,一個(gè)自然字(native word)的尺寸在32位的架構(gòu)上為4字節(jié),在64位的架構(gòu)上為8字節(jié)。
這意味著,對(duì)于當(dāng)前版本的標(biāo)準(zhǔn)編譯器,其它類型的對(duì)齊保證為4
或者8
,具體取決于程序編譯時(shí)選擇的目標(biāo)架構(gòu)。 此結(jié)論對(duì)另一個(gè)流行Go編譯器gccgo也成立。
一般情況下,在Go編程中,我們不必關(guān)心值地址的對(duì)齊保證。 除非有時(shí)候我們打算優(yōu)化一下內(nèi)存消耗,或者編寫跨平臺(tái)移植性良好的Go代碼。 請(qǐng)閱讀下兩節(jié)以獲得詳情。
Go白皮書只對(duì)以下種類的類型的尺寸進(jìn)行了明確規(guī)定。
類型種類 尺寸(字節(jié)數(shù))
------ ------
uint8, int8 1
uint16, int16 2
uint32, int32, float32 4
uint64, int64 8
float64, complex64 8
complex128 16
uint, int 取決于編譯器實(shí)現(xiàn)。通常在
32位架構(gòu)上為4,在64位
架構(gòu)上為8。
uintptr 取決于編譯器實(shí)現(xiàn)。但必須
能夠存下任一個(gè)內(nèi)存地址。
Go白皮書沒有對(duì)其它種類的類型的尺寸做出明確規(guī)定。 請(qǐng)閱讀值復(fù)制成本一文來(lái)獲取標(biāo)準(zhǔn)編譯器使用的各種其它類型的尺寸。
標(biāo)準(zhǔn)編譯器(和gccgo編譯器)將確保一個(gè)類型的尺寸為此類型的對(duì)齊保證的倍數(shù)。
為了滿足前面提到的各條地址對(duì)齊保證要求規(guī)則,Go編譯器可能會(huì)在結(jié)構(gòu)體的相鄰字段之間填充一些字節(jié)。 這使得一個(gè)結(jié)構(gòu)體類型的尺寸并非等于它的各個(gè)字段類型尺寸的簡(jiǎn)單相加之和。
下面是一個(gè)展示了一些字節(jié)是如何填充到一個(gè)結(jié)構(gòu)體中的例子。 首先,從上面的描述中,我們已得知(對(duì)于標(biāo)準(zhǔn)編譯器來(lái)說(shuō)):
int8
的對(duì)齊保證和尺寸均為1個(gè)字節(jié);
內(nèi)置類型int16
的對(duì)齊保證和尺寸均為2個(gè)字節(jié);
內(nèi)置類型int64
的尺寸為8個(gè)字節(jié),但它的對(duì)齊保證在32位架構(gòu)上為4個(gè)字節(jié),在64位架構(gòu)上為8個(gè)字節(jié)。
T1
和T2
的對(duì)齊保證均為它們的各個(gè)字段的最大對(duì)齊保證。
所以它們的對(duì)齊保證和內(nèi)置類型int64
相同,即在32位架構(gòu)上為4個(gè)字節(jié),在64位架構(gòu)上為8個(gè)字節(jié)。
T1
和T2
尺寸需為它們的對(duì)齊保證的倍數(shù),即在32位架構(gòu)上為4n個(gè)字節(jié),在64位架構(gòu)上為8n個(gè)字節(jié)。
type T1 struct {
a int8
// 在64位架構(gòu)上,為了讓字段b的地址為8字節(jié)對(duì)齊,
// 需在這里填充7個(gè)字節(jié)。在32位架構(gòu)上,為了讓
// 字段b的地址為4字節(jié)對(duì)齊,需在這里填充3個(gè)字節(jié)。
b int64
c int16
// 為了讓類型T1的尺寸為T1的對(duì)齊保證的倍數(shù),
// 在64位架構(gòu)上需在這里填充6個(gè)字節(jié),在32架構(gòu)
// 上需在這里填充2個(gè)字節(jié)。
}
// 類型T1的尺寸在64位架構(gòu)上為24個(gè)字節(jié)(1+7+8+2+6),
// 在32位架構(gòu)上為16個(gè)字節(jié)(1+3+8+2+2)。
type T2 struct {
a int8
// 為了讓字段c的地址為2字節(jié)對(duì)齊,
// 需在這里填充1個(gè)字節(jié)。
c int16
// 在64位架構(gòu)上,為了讓字段b的地址為8字節(jié)對(duì)齊,
// 需在這里填充4個(gè)字節(jié)。在32位架構(gòu)上,不需填充
// 字節(jié)即可保證字段b的地址為4字節(jié)對(duì)齊的。
b int64
}
// 類型T2的尺寸在64位架構(gòu)上位16個(gè)字節(jié)(1+1+2+4+8),
// 在32位架構(gòu)上為12個(gè)字節(jié)(1+1+2+8)。
從這個(gè)例子可以看出,盡管類型T1
和T2
擁有相同的字段集,但是它們的尺寸并不相等。
一個(gè)有趣的事實(shí)是有時(shí)候一個(gè)結(jié)構(gòu)體類型中零尺寸類型的字段可能會(huì)影響到此結(jié)構(gòu)體類型的尺寸。 請(qǐng)閱讀此問(wèn)答獲取詳情。
在此文中,64位字是指類型為內(nèi)置類型int64
或uint64
的值。
原子操作一文提到了一個(gè)事實(shí):一個(gè)64位字的原子操作要求此64位字的地址必須是8字節(jié)對(duì)齊的。 這對(duì)于標(biāo)準(zhǔn)編譯器目前支持的64位架構(gòu)來(lái)說(shuō)并不是一個(gè)問(wèn)題,因?yàn)闃?biāo)準(zhǔn)編譯器保證任何一個(gè)64位字的地址在64位架構(gòu)上都是8字節(jié)對(duì)齊的。
然而,在32位架構(gòu)上,標(biāo)準(zhǔn)編譯器為64位字做出的地址對(duì)齊保證僅為4個(gè)字節(jié)。 對(duì)一個(gè)不是8字節(jié)對(duì)齊的64位字進(jìn)行64位原子操作將在運(yùn)行時(shí)刻產(chǎn)生一個(gè)恐慌。 更糟的是,一些非常老舊的架構(gòu)并不支持64位原子操作需要的基本指令。
sync/atomic
標(biāo)準(zhǔn)庫(kù)包文檔的末尾提到:
On x86-32, the 64-bit functions use instructions unavailable before the Pentium MMX.
On non-Linux ARM, the 64-bit functions use instructions unavailable before the ARMv6k core.
On both ARM and x86-32, 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.
所以,情況并非無(wú)可挽救。
這些途徑被描述為開辟的結(jié)構(gòu)體、數(shù)組和切片值中的第一個(gè)(64位)字可以被認(rèn)為是8字節(jié)對(duì)齊的。 這里的開辟的應(yīng)該如何解讀? 我們可以認(rèn)為一個(gè)開辟的值為一個(gè)聲明的變量、內(nèi)置函數(shù)make
的調(diào)用返回值,或者內(nèi)置函數(shù)new
的調(diào)用返回值所引用的值。 如果一個(gè)切片是從一個(gè)開辟的數(shù)組派生出來(lái)的并且此切片和此數(shù)組共享第一個(gè)元素,則我們也可以將此切片看作是一個(gè)開辟的值。
此對(duì)哪些64位字可以在32位架構(gòu)上被安全地原子訪問(wèn)的描述是有些保守的。 有很多此描述并未包括的64位字在32位架構(gòu)上也是可以被安全地原子訪問(wèn)的。 比如,如果一個(gè)元素類型為64位字的數(shù)組或者切片的第一個(gè)元素可以被安全地進(jìn)行64位原子訪問(wèn),則此數(shù)組或切片中的所有元素都可以被安全地進(jìn)行64位原子訪問(wèn)。 只是因?yàn)楹茈y用三言兩語(yǔ)將所有在32位架構(gòu)上可以被安全地原子訪問(wèn)的64位字都羅列出來(lái),所以官方文檔采取了一種保守的描述。
下面是一個(gè)展示了哪些64位字在32位架構(gòu)上可以和哪些不可以被安全地原子訪問(wèn)的例子。
type (
T1 struct {
v uint64
}
T2 struct {
_ int16
x T1
y *T1
}
T3 struct {
_ int16
x [6]int64
y *[6]int64
}
)
var a int64 // a可以安全地被原子訪問(wèn)
var b T1 // b.v可以安全地被原子訪問(wèn)
var c [6]int64 // c[0]可以安全地被原子訪問(wèn)
var d T2 // d.x.v不能被安全地被原子訪問(wèn)
var e T3 // e.x[0]不能被安全地被原子訪問(wèn)
func f() {
var f int64 // f可以安全地被原子訪問(wèn)
var g = []int64{5: 0} // g[0]可以安全地被原子訪問(wèn)
var h = e.x[:] // h[0]可以安全地被原子訪問(wèn)
// 這里,d.y.v和e.y[0]都可以安全地被原子訪問(wèn),
// 因?yàn)?d.y和*e.y都是開辟出來(lái)的。
d.y = new(T1)
e.y = &[6]int64{}
_, _, _ = f, g, h
}
// 事實(shí)上,c、g和e.y.v的所有以元素都可以被安全地原子訪問(wèn)。
// 只不過(guò)官方文檔沒有明確地做出保證。
如果一個(gè)結(jié)構(gòu)體類型的某個(gè)64位字的字段(通常為第一個(gè)字段)在代碼中需要被原子訪問(wèn),為了保證此字段值在各種架構(gòu)上都可以被原子訪問(wèn),我們應(yīng)該總是使用此結(jié)構(gòu)體的開辟值。 當(dāng)此結(jié)構(gòu)體類型被用做另一個(gè)結(jié)構(gòu)體類型的一個(gè)字段的類型時(shí),此字段應(yīng)該(盡量)被安排為另一個(gè)結(jié)構(gòu)體類型的第一個(gè)字段,并且總是使用另一個(gè)結(jié)構(gòu)體類型的開辟值。
如果一個(gè)結(jié)構(gòu)體含有需要一個(gè)被原子訪問(wèn)的字段,并且我們希望此結(jié)構(gòu)體可以自由地用做其它結(jié)構(gòu)體的任何字段(可能非第一個(gè)字段)的類型,則我們可以用一個(gè)[15]byte
值來(lái)模擬此64位值,并在運(yùn)行時(shí)刻動(dòng)態(tài)地決定此64位值的地址。 比如:
package mylib
import (
"unsafe"
"sync/atomic"
)
type Counter struct {
x [15]byte // 模擬:x uint64
}
func (c *Counter) xAddr() *uint64 {
// 此返回結(jié)果總是8字節(jié)對(duì)齊的。
return (*uint64)(unsafe.Pointer(
(uintptr(unsafe.Pointer(&c.x)) + 7)/8*8))
}
func (c *Counter) Add(delta uint64) {
p := c.xAddr()
atomic.AddUint64(p, delta)
}
func (c *Counter) Value() uint64 {
return atomic.LoadUint64(c.xAddr())
}
通過(guò)采用此方法,Counter
類型可以自由地用做其它結(jié)構(gòu)體的任何字段的類型,而無(wú)需擔(dān)心此類型中維護(hù)的64位字段值可能不是8字節(jié)對(duì)齊的。 此方法的缺點(diǎn)是,對(duì)于每個(gè)Counter
類型的值,都有7個(gè)字節(jié)浪費(fèi)了。而且此方法使用了非類型安全指針。
Go 1.19引入了一種更為優(yōu)雅的方法來(lái)保證一些值的地址對(duì)齊保證為8字節(jié)。 Go 1.19在sync/atomic
標(biāo)準(zhǔn)庫(kù)包中加入了幾個(gè)原子類型。 這些類型包括atomic.Int64
和atomic.Uint64
。 這兩個(gè)類型的值在內(nèi)存中總是8字節(jié)對(duì)齊的,即使在32位架構(gòu)上也是如此。 我們可以利用這個(gè)事實(shí)來(lái)確保一些64位字在32位架構(gòu)上總是8字節(jié)對(duì)齊的。 比如,無(wú)論在32位架構(gòu)還是64位架構(gòu)上,下面的代碼所示的T
類型的x
字段在任何情形下總是8字節(jié)對(duì)齊的。
type T struct {
_ [0]atomic.Int64
x int64
}
更多建議: