Go語(yǔ)言 關(guān)于Go值的內(nèi)存布局

2023-02-16 17:40 更新

本文將介紹Go中的各種類型的尺寸和對(duì)齊保證。 知曉這些保證對(duì)于估計(jì)結(jié)構(gòu)體值的尺寸和正確使用64位整數(shù)原子操作函數(shù)是必要的。

Go是一門屬于C語(yǔ)言家族的編程語(yǔ)言,所以本文談及的很多概念和C語(yǔ)言是相通的。

Go中的類型對(duì)齊保證(alignment guarantee)

為了充分利用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ì)于任何類型的變量xunsafe.Alignof(x)的結(jié)果最小為1。
2. 對(duì)于一個(gè)結(jié)構(gòu)體類型的變量xunsafe.Alignof(x)的結(jié)果為x的所有字段的對(duì)齊保證unsafe.Alignof(x.f)中的最大值(但是最小為1)。
3. 對(duì)于一個(gè)數(shù)組類型的變量xunsafe.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é)以獲得詳情。

類型的尺寸和結(jié)構(gòu)體字節(jié)填充(structure padding)

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ō)):

  • 內(nèi)置類型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é)。
  • 下例中的類型T1T2的對(duì)齊保證均為它們的各個(gè)字段的最大對(duì)齊保證。 所以它們的對(duì)齊保證和內(nèi)置類型int64相同,即在32位架構(gòu)上為4個(gè)字節(jié),在64位架構(gòu)上為8個(gè)字節(jié)。
  • 類型T1T2尺寸需為它們的對(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è)例子可以看出,盡管類型T1T2擁有相同的字段集,但是它們的尺寸并不相等。

一個(gè)有趣的事實(shí)是有時(shí)候一個(gè)結(jié)構(gòu)體類型中零尺寸類型的字段可能會(huì)影響到此結(jié)構(gòu)體類型的尺寸。 請(qǐng)閱讀此問(wèn)答獲取詳情。

64位字原子操作的地址對(duì)齊保證要求

在此文中,64位字是指類型為內(nèi)置類型int64uint64的值。

原子操作一文提到了一個(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ú)可挽救。

  1. 這些非常老舊的架構(gòu)在今日已經(jīng)相當(dāng)?shù)牟恢髁髁恕?如果一個(gè)程序需要在這些架構(gòu)上對(duì)64位字進(jìn)行原子操作,還有很多其它同步技術(shù)可用。
  2. 對(duì)其它不是很老舊的32位架構(gòu),有一些途徑可以保證在這些架構(gòu)上對(duì)一些64位字的原子操作是安全的。

這些途徑被描述為開辟的結(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.Int64atomic.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
}


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)