Go 轻松了解内存对齐

188 阅读5分钟

前言

什么是内存对齐呢?

举个例子,64位系统每次读取都是8个字节,以下有一张图,存放着int16int32大小的两个变量。

image.png

如果要加入一个int32大小的变量,正常情况下,会紧挨着内存摆放,便会出现非内存对齐。

image.png

这会出现什么问题呢?系统要获取该int32的变量,就需要读取两次内存,先读取到0x08后,还得读取到0x10,影响系统的原子性原则及效率。

于是需要内存对齐的摆放,如下图。

image.png

系统只需一次读取0x080x10即可获取到变量的值。

因此,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")))
}

以下是每个基本类型的大小和对齐系数。

image.png

那这对齐系数有什么用呢?

咱们一步步添加变量来看看。

先添加一个bool变量。

image.png

由于bool变量对齐系数为1,因此bool变量摆放在任何位置,其内存地址都能整除1,换句话说,bool变量放在任何一个地址上都能对齐。

再添加一个int32变量。

image.png

如上图,int32变量的对齐系数为4,要内存对齐的话,只能放在0x000x040x080x0c这些能整除4的地址上,这样才能一次性被系统读取。而这里0x00已被占用,故放置在0x04的位置上。

再添加一个int64变量。

image.png

如上图,int64变量对齐系数为8,只能放在0x000x08的位置。而这里0x00已被占用,故放置在0x08的位置上。

再添加一个int16变量呢?

image.png

同理得出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
}

其地址图如下:

image.png

那就有疑问了,为什么整个结构体的大小是32,对齐系数是8?关键在于每个成员的偏移量。

首先结构体demo的地址是0x00

先是成员a,是个bool,大小为1,对齐系数为1。那求a的偏移量,选大小和对齐系数较小者1,于是偏移量为1的整数倍。因此相对结构体的偏移量为1,故放在0x00的位置。

然后是成员b,是个string,大小为16,对齐系数为8,两数中的较小者为8,则b的偏移量为8的整数倍。因此相对结构体首地址偏移8的整数倍,可放在0x000x080x10等位置上,0x00已被占用,故放在0x08的位置。

同样成员c,是个int16,大小为2,对齐系数为2,两数中的较小者是2,偏移量即为2的整数倍。因此相对结构体的首地址偏移2的整数倍,可放在0x000x020x04等位置上,排除已被占用的,于是放在string的后面0x18的位置。

于是结构体的内部就对齐了,接下来便是结构体外部的对齐了。

结构体外部对齐

结构体外部对齐就需要增加结构体的长度,使整个结构体长度达到该结构体对齐系数的整数倍。

该结构体对齐系数 = min(最大成员长度,系统字长),即从最大成员长度,系统字长中选取较小者作为对齐系数。

同样以demo做示例,其填充长度如下:

image.png

因为系统字长为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字节。

image.png

结语

以上便是Go的内存对齐,只需记住两个关键:对齐系数和偏移量,便能大概掌握内存对齐了。