关于内存对齐

什么是内存对齐

我们时常会以为一个变量在内存中可以任意存放(假设内存空间足够, 不考虑占用情况)。但实际上并不是这样。

简单来说,特定类型变量通常存放在特定的内存地址上。各种类型数据按照一定的规则在空间上排列,而不是顺序(紧挨着)的一个接一个的排放,这就是对齐。

内存对齐是编译器的管辖范围。表现为:编译器为程序中的每个“数据单元”安排在适当的位置上。

为什么需要

在回答这个问题之前,我们先简单回顾下为啥会有内存这东西?

CPU是计算机的控制与运算核心,它的运算速度非常快,但CPU运算的前提是需要有东西给它算。东西从哪来?最开始是硬盘,不过硬盘IO的速度相对CPU来说太慢了,于是就有了“中间层”-内存。(后面的"寄存器"那些也是同理)

CPU会从内存中读数据出来进行各种操作,不同硬件平台每次读取数据的大小是不一样的(单位:字节)。比如A CPU每次以2字节为单位读取数据,B CPU每次以4字节为单位读取数据。接下来就到了内存对齐的时候,先画个图:

memory_alignment_01

这里我们申请了一个short类型的变量,假设它的内存起始地址为0x00001,同时CPU每次会从内存中以2字节为单位读取数据。可以看到,CPU读取数据时,仅需一次就可以读取到完成的short类型的变量(因为该类型占2个字节)。

不过有一种情况,假设该变量的内存起始地址发生了一点变化,不以0x00001作为起始地址:

memory_alignment_02

此时,short类型的变量的内存起始地址为0x00002,CPU第一次读取数据时,只能读取到0x00002这个地址上的数据,还需要再读取一次才能读取到完整的变量。这样就导致了CPU读取数据的次数增加,同时还需要算上剔除多余的数据(0x00001&0x00004)以及合并两个字节的时间,从而降低了内存访问的效率。

此时,我们就可以看到内存对齐的重要性了。分配内存时,编译器会根据变量的类型,自动调整变量的内存地址,使其符合CPU读取数据的要求(它的地址只要是它的长度的整数倍就行了)。

也就是说,有了内存对齐,例子中的short类型变量就不会出现上述情况,CPU会尽量的以最少的次数读取到完整的数据。当然例子举得不是特别好,应该以0x00000为起始地址,这里只是为了说明问题。

结构体的内存对齐

这里先用golang来举例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type Person1 struct {
	Age       int32
	BirthUnix int64
	Ip        int32
}

type Person2 struct {
	Age       int32
	Ip        int32
	BirthUnix int64
}

func TestMemoryAlignment(t *testing.T) {
	// person1: 24, person2: 16
	fmt.Printf("person1: %d, person2: %d\n", unsafe.Sizeof(Person1{}), unsafe.Sizeof(Person2{}))
}

同样的结构体定义,只是换了成员顺序,结果分配的内存大小却不一样。继续画图说明:

memory_alignment_03

结构体的内存布局时根据成员的顺序来决定的,这里Person1第一个成员为int32类型,占4个字节(0x0~0x3)。之后第二个成员为int64类型,占8个字节(0x8~0x15)。大家注意到0x4~0x7空出来了,因为上文说到的:

它的地址只要是它的长度的整数倍就行了

BirthUnix成员的起始地址必须是8的整数倍,也就是要从0x8开始。就导致Age字段之后的空间空出了4个字节。

第三个字段Ipint32类型,占4个字节(0x16~0x19)。之后又空出了4个字节(0x20~0x23)是因为结构体内存对齐有个规则:结构体的总大小为 最大对齐数的整数倍(每个成员变量都有自己的对齐数),如有需要编译器会在最末一个成员之后加上填充字节。

所以Person1的内存大小为24个字节。通过图片我们得出优化结论,如果把Ip字段放到BirthUnix字段之前,那么Ip字段的起始地址就是0x4,BirthUnix字段的起始地址还是0x8,这样就不会浪费4个字节的空间了。相当于把图中绿色的部分挪到黄色后面:

memory_alignment_04

这样Person2的内存大小就是16个字节了。

平时在写代码时,可以适当地利用结构体内存对齐的特性,来减少内存的浪费。


这里补充下结构体内存对齐的规则:

  1. 第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始
  2. 以后每个成员相对于结构体首地址的 offset 都是该成员大小的整数倍,如有需要编译器会在成员之间加上填充字节 (上文提到了)
  3. 结构体的总大小为 最大对齐数的整数倍(每个成员变量都有自己的对齐数),如有需要编译器会在最末一个成员之后加上填充字节 (上文提到了)
  4. 如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍
updatedupdated2024-05-222024-05-22