前言
什么是内存对齐呢?
举个例子,64位系统每次读取都是8个字节,以下有一张图,存放着int16和int32大小的两个变量。
如果要加入一个int32大小的变量,正常情况下,会紧挨着内存摆放,便会出现非内存对齐。
这会出现什么问题呢?系统要获取该int32的变量,就需要读取两次内存,先读取到0x08后,还得读取到0x10,影响系统的原子性原则及效率。
于是需要内存对齐的摆放,如下图。
系统只需一次读取0x08到0x10即可获取到变量的值。
因此,Go就得设计一下该如何内存对齐了。
Go的内存对齐
对齐系数
为了方便内存管理,Go提供了对齐系数。
unsafe.Alignof()
内存系数的含义是:变量的内存地址必须整除对齐系数。
基本数据类型
写个函数打印下各个基本数据类型的大小和对齐系数。
func main() {
fmt.Printf("bool size:%d align:%d\n", unsafe.Sizeof(true), unsafe.Alignof(true))
fmt.Printf("byte size:%d align:%d\n", unsafe.Sizeof(byte(0)), unsafe.Alignof(true))
fmt.Printf("int8 size:%d align:%d\n", unsafe.Sizeof(int8(0)), unsafe.Alignof(int8(0)))
fmt.Printf("int16 size:%d align:%d\n", unsafe.Sizeof(int16(0)), unsafe.Alignof(int16(0)))
fmt.Printf("int32 size:%d align:%d\n", unsafe.Sizeof(int32(0)), unsafe.Alignof(int32(0)))
fmt.Printf("int64 size:%d align:%d\n", unsafe.Sizeof(int64(0)), unsafe.Alignof(int64(0)))
fmt.Printf("string size:%d align:%d\n", unsafe.Sizeof(string("0")), unsafe.Alignof(string("0")))
}
以下是每个基本类型的大小和对齐系数。
那这对齐系数有什么用呢?
咱们一步步添加变量来看看。
先添加一个bool变量。
由于bool变量对齐系数为1,因此bool变量摆放在任何位置,其内存地址都能整除1,换句话说,bool变量放在任何一个地址上都能对齐。
再添加一个int32变量。
如上图,int32变量的对齐系数为4,要内存对齐的话,只能放在0x00,0x04,0x08,0x0c这些能整除4的地址上,这样才能一次性被系统读取。而这里0x00已被占用,故放置在0x04的位置上。
再添加一个int64变量。
如上图,int64变量对齐系数为8,只能放在0x00和0x08的位置。而这里0x00已被占用,故放置在0x08的位置上。
再添加一个int16变量呢?
同理得出int16变量的可对齐的地址,然后找个空闲的位置放进去。
以上是基本数据类型的内存对齐,只需找到对齐系数的整数倍地址即可。接下来讨论下结构体的内存对齐。
结构体
结构体对齐就要考虑两个部分了,分别为结构体内部对齐和结构体外部对齐。
结构体内部对齐:考虑每个成员大小和成员对齐系数。
结构体外部对齐:考虑自身对齐系数和系统字长。
结构体内部对齐
- 每个成员有一个相对结构体的偏移量。
- 偏移量是自身大小和对齐系数中较小值的倍数。
举个例子,定义个结构体demo,它的大小为32,对齐系数为8。
// demo size:32 align:8
type demo struct {
a bool // size:1 align:1
b string // size:16 align:8
c int16 // size:2 align:2
}
其地址图如下:
那就有疑问了,为什么整个结构体的大小是32,对齐系数是8?关键在于每个成员的偏移量。
首先结构体demo的地址是0x00。
先是成员a,是个bool,大小为1,对齐系数为1。那求a的偏移量,选大小和对齐系数较小者1,于是偏移量为1的整数倍。因此相对结构体的偏移量为1,故放在0x00的位置。
然后是成员b,是个string,大小为16,对齐系数为8,两数中的较小者为8,则b的偏移量为8的整数倍。因此相对结构体首地址偏移8的整数倍,可放在0x00,0x08,0x10等位置上,0x00已被占用,故放在0x08的位置。
同样成员c,是个int16,大小为2,对齐系数为2,两数中的较小者是2,偏移量即为2的整数倍。因此相对结构体的首地址偏移2的整数倍,可放在0x00,0x02,0x04等位置上,排除已被占用的,于是放在string的后面0x18的位置。
于是结构体的内部就对齐了,接下来便是结构体外部的对齐了。
结构体外部对齐
结构体外部对齐就需要增加结构体的长度,使整个结构体长度达到该结构体对齐系数的整数倍。
该结构体对齐系数 = min(最大成员长度,系统字长),即从最大成员长度,系统字长中选取较小者作为对齐系数。
同样以demo做示例,其填充长度如下:
因为系统字长为64bit,即8个字节,而demo的最大成员长度为16字节,选取较小者8作为demo的对齐系数,则整个结构体大小需要达到8的整数倍。因此需要往int16后面填充6个0,使整个结构体长度变为32,为8的整数倍。
因此最后demo结构体的大小为32字节,对齐系数为8。
节约结构体空间
根据内存对齐的原理,咱们可以重新构造demo结构体来节约内存,只需将成员按对齐系数从小到大排序即可。
// 改写的demo size:24 align:8
type demo struct {
a bool // size:1 align:1
c int16 // size:2 align:2
b string // size:16 align:8
}
内存图如下,由于demo大小为24,已经是自身对齐系数8的整数倍了,就不需要填充0了。于是demo的大小从32字节变为24字节。
结语
以上便是Go的内存对齐,只需记住两个关键:对齐系数和偏移量,便能大概掌握内存对齐了。