Dig101: dig more, simplified more and know morehtml
通過前邊幾篇文章,相信你也發現了,struct幾乎無處不在。android
string,slice和map底層都用到了struct。git
今天咱們來重點關注下struct的內存對齊,github
理解它,對更好的運用struct和讀懂一些源碼庫的實現會有很大的幫助。golang
在此以前,咱們先明確幾個術語,便於後續分析。express
是用於表示其天然的數據單位,也叫machine word
。字是電腦用來一次性處理事務的一個固定長度。c#
一個字的位數(即字長)。數組
現代電腦的字長一般爲1六、3二、64位。(通常N位系統的字長是 N/8
字節。)安全
電腦中大多數寄存器的大小是一個字長。CPU和內存之間的數據傳送單位也一般是一個字長。還有而內存中用於指明一個存儲位置的地址也常常是以字長爲單位。session
參見維基百科中 字
簡單來講,操做系統的cpu不是一個字節一個字節訪問內存的,是按2,4,8這樣的字長來訪問的。
因此當處理器從存儲器子系統讀取數據至寄存器,或者,寫寄存器數據到存儲器,傳送的數據長度一般是字長。
如32位系統訪問粒度是4字節(bytes),64位系統的是8字節。
當被訪問的數據長度爲 n
字節且該數據地址爲n
字節對齊,那麼操做系統就能夠一次定位到數據,這樣會更加高效。無需屢次讀取、處理對齊運算等額外操做。
咱們先看下基礎數據結構的大小定義
如Go官方的文檔size and alignment guarantees所示:
type | size in bytes |
---|---|
byte, uint8, int8 | 1 |
uint16, int16 | 2 |
uint32, int32, float32 | 4 |
uint64, int64, float64, complex64 | 8 |
complex128 | 16 |
A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.
struct{}
和[0]T{}
的大小爲0; 不一樣的大小爲0的變量可能指向同一塊地址。
- 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.
對這段描述翻譯到對應類型的對齊就是下表
type | alignment guarantee |
---|---|
bool, byte, uint8, int8 | 1 |
uint16, int16 | 2 |
uint32, int32 | 4 |
float32, complex64 | 4 |
arrays | 由其元素(element)類型決定 |
structs | 由其字段(field)類型決定 |
other types | 一個機器字(machine word)的大小 |
這裏機器字(machine word)對應的大小, 在32位系統上是4bytes,64位系統上是8bytes
下面代碼驗證下:
type T1 struct {
a [2]int8
b int64
c int16
}
type T2 struct {
a [2]int8
c int16
b int64
}
fmt.Printf("arrange fields to reduce size:\n"+
"T1 align: %d, size: %d\n"+
"T2 align: %d, size: %d\n",
unsafe.Alignof(T1{}), unsafe.Sizeof(T1{}),
unsafe.Alignof(T2{}), unsafe.Sizeof(T2{}))
/* output: arrange fields to reduce size: T1 align: 8, size: 24 T2 align: 8, size: 16 */
複製代碼
以64位系統爲例,分析以下:
T1,T2
內字段最大的都是int64
, 大小爲8bytes,對齊按機器字肯定,64位下是8bytes,因此將按8bytes對齊
T1.a
大小2bytes,填充6bytes使對齊(後邊字段已對齊,因此直接填充)
T1.b
大小8bytes,已對齊
T1.c
大小2bytes,填充6bytes使對齊(後邊無字段,因此直接填充)
總大小爲 8+8+8=24
T2
中將c
提早後,a
和c
總大小4bytes,在填充4bytes使對齊
總大小爲 8+8=16
因此,合理重排字段能夠減小填充,使struct字段排列更緊密
零大小字段(zero sized field
)是指struct{}
,
大小爲0,按理做爲字段時不須要對齊,但當在做爲結構體最後一個字段(final field
)時須要對齊的。
爲何?
由於,若是有指針指向這個final zero field
, 返回的地址將在結構體以外(即指向了別的內存),
若是此指針一直存活不釋放對應的內存,就會有內存泄露的問題(該內存不因結構體釋放而釋放)
因此,Go就對這種final zero field
也作了填充,使對齊。
代碼驗證以下:
type T1 struct {
a struct{}
x int64
}
type T2 struct {
x int64
a struct{}
}
a1 := T1{}
a2 := T2{}
fmt.Printf("zero size struct{} in field:\n"+
"T1 (not as final field) size: %d\n"+
"T2 (as final field) size: %d\n",
// 8
unsafe.Sizeof(a1),
// 64位:16;32位:12
unsafe.Sizeof(a2))
複製代碼
從unsafe包規範中,有以下說明:
Computer architectures may require memory addresses to be aligned; that is, for addresses of a variable to be a multiple of a factor, the variable's type's alignment. The function Alignof takes an expression denoting a variable of any type and returns the alignment of the (type of the) variable in bytes. For a variable x:
uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0
大體意思就是,若是類型 t
的對齊保證是 n
,那麼類型 t
的每一個值的地址在運行時必須是 n
的倍數。
這一點在sync.WaitGroup
有很好的應用:
type WaitGroup struct {
noCopy noCopy
state1 [3]uint32
}
// state returns pointers to the state and sema fields stored within wg.state1.
func (wg *WaitGroup) state() (statep *uint64, semap *uint32) {
// 斷定地址是否8位對齊
if uintptr(unsafe.Pointer(&wg.state1))%8 == 0 {
// 前8bytes作uint64指針statep,後4bytes作sema
return (*uint64)(unsafe.Pointer(&wg.state1)), &wg.state1[2]
} else {
// 後8bytes作uint64指針statep,前4bytes作sema
return (*uint64)(unsafe.Pointer(&wg.state1[1])), &wg.state1[0]
}
}
複製代碼
重點是WaitGroup.state1
這個字段,
咱們知道uint64
的對齊是由機器字決定,32位系統是4bytes,64位系統是8bytes
爲保證在32位系統上,也能夠返回一個64位對齊(8bytes aligned
)的指針(*uint64
)
就巧妙的使用了[3]uint32
。
首先在64位系統和32位系統上,uint32
能保證是4bytes對齊
即state1
地址是4N: uintptr(unsafe.Pointer(&wg.state1))%4 == 0
而爲保證8位對齊,咱們只須要判斷state1
地址是否爲8的倍數
並且剩餘的4bytes能夠給sema
字段用,也不浪費內存
但是爲何要在32位系統上也要保證一個64位對齊的uint64
指針呢?
答案是,爲了保證在32位系統上也能原子訪問64位對齊的64位字。咱們下邊來詳細看下。
在atomic-bug中提到:
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 ARM, x86-32, 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.
大體意思是,在32位系統上想要原子操做64位字(如uint64)的話,須要由調用方保證其數據地址是64位對齊的,不然原子訪問會有異常。
爲何呢?
這裏簡單分析以下:
還拿uint64
來講,大小爲8bytes,32位系統上按4bytes對齊,64位系統上按8bytes對齊。
在64位系統上,8bytes恰好和其字長相同,因此能夠一次完成原子的訪問,不被其餘操做影響或打斷。
而32位系統,4byte對齊,字長也爲4bytes,可能出現uint64
的數據分佈在兩個數據塊中,須要兩次操做才能完成訪問。
若是兩次操做中間有可能別其餘操做修改,不能保證原子性。
這樣的訪問方式也是不安全的。
這一點issue-6404中也有提到:
This is because the int64 is not aligned following the bool. It is 32-bit aligned but not 64-bit aligned, because we're on a 32-bit system so it's really just two 32-bit values side by side.
The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.
變量或開闢的結構體、數組和切片值中的第一個64位字能夠被認爲是8字節對齊
這一句中開闢的意思是經過聲明,make,new方式建立的,就是說這樣建立的64位字能夠保證是64位對齊的。
但仍是比較抽象,咱們舉例分析下
32位系統下可原子安全訪問的64位字有:
// GOARCH=386 go run types/struct/struct.go
var c0 int64
fmt.Println("64位字自己:",
atomic.AddInt64(&c0, 1))
複製代碼
c1 := [5]int64{}
fmt.Println("64位字數組、切片:",
atomic.AddInt64(&c1[:][0], 1))
複製代碼
c2 := struct {
val int64 // pos 0
val2 int64 // pos 8
valid bool // pos 16
}{}
fmt.Println("結構體首字段爲對齊的64位字及相鄰的64位字:",
atomic.AddInt64(&c2.val, 1),
atomic.AddInt64(&c2.val2, 1))
複製代碼
type T struct {
val2 int64
_ int16
}
c3 := struct {
val T
valid bool
}{}
fmt.Println("結構體中首字段爲嵌套結構體,且其首元素爲64位字:",
atomic.AddInt64(&c3.val.val2, 1))
複製代碼
c4 := struct {
val int64 // pos 0
valid bool // pos 8
// 或者 _ uint32
// 使32位系統上多填充 4bytes
_ [4]byte // pos 9
val2 int64 // pos 16
}{}
fmt.Println("結構體增長填充使對齊的64位字:",
atomic.AddInt64(&c4.val2, 1))
複製代碼
c5 := struct {
val int64
valid bool
val2 []int64
}{val2: []int64{0}}
fmt.Println("結構體中64位字切片:",
atomic.AddInt64(&c5.val2[0], 1))
複製代碼
The first element in slices of 64-bit elements will be correctly aligned
此處切片至關指針,數據是指向底層堆上開闢的64位字數組,如c1
若是換成數組則會panic,
由於結構體的數組的對齊仍是依賴於結構體內字段
c51 := struct {
val int64
valid bool
val2 [3]int64
}{val2: [3]int64{0}}
// will panic
atomic.AddInt64(&c51.val2[0], 1)
複製代碼
c6 := struct {
val int64
valid bool
val2 *int64
}{val2: new(int64)}
fmt.Println("結構體中64位字指針:",
atomic.AddInt64(c6.val2, 1))
複製代碼
是否是有些複雜,要在32位系統上保證8bytes對齊的64位字, 確實不是很方便
固然也能夠選擇不使用原子訪問(atomic
),用加鎖(mutex
)的方式避免此bug
c := struct{
val int16
val2 int64
}{}
var mu sync.Mutex
mu.Lock()
c.val2 += 1
mu.Unlock()
複製代碼
最後,其實前邊WaitGroup.state1
那樣保證8bytes對齊還有有個有點點沒有分析:
就是爲啥state原子訪問不直接用uint64
,並使用上邊提到的64位字對齊保證?
答案相信你也想到了:若是WaitGroup
嵌套到別的結構體時,若是不放到結構體首位會有問題, 這會使其使用受限。
總結一下:
即 uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0
mutex
)的方式更清晰簡單推薦一個工具包:dominikh/go-tools ,裏邊 structlayout, structlayout-optimize, structlayout-pretty 三個工具比較有意思
本文代碼見 NewbMiao/Dig101-Go
See more: Golang 是否有必要內存對齊?
文章首發公衆號: newbmiao (歡迎關注,獲取及時更新內容)
推薦閱讀:Dig101-Go系列