什么是内存对齐
我们时常会以为一个变量在内存中可以任意存放(假设内存空间足够, 不考虑占用情况)。但实际上并不是这样。
简单来说,特定类型变量通常存放在特定的内存地址上。各种类型数据按照一定的规则在空间上排列,而不是顺序(紧挨着)的一个接一个的排放,这就是对齐。
内存对齐是编译器的管辖范围。表现为:编译器为程序中的每个“数据单元”安排在适当的位置上。
为什么需要
在回答这个问题之前,我们先简单回顾下为啥会有内存这东西?
CPU是计算机的控制与运算核心,它的运算速度非常快,但CPU运算的前提是需要有东西给它算。东西从哪来?最开始是硬盘,不过硬盘IO的速度相对CPU来说太慢了,于是就有了“中间层”-内存。(后面的"寄存器"那些也是同理)
CPU会从内存中读数据出来进行各种操作,不同硬件平台每次读取数据的大小是不一样的(单位:字节)。比如A CPU每次以2字节为单位读取数据,B CPU每次以4字节为单位读取数据。接下来就到了内存对齐的时候,先画个图:
这里我们申请了一个short
类型的变量,假设它的内存起始地址为0x00001
,同时CPU每次会从内存中以2字节为单位读取数据。可以看到,CPU读取数据时,仅需一次就可以读取到完成的short
类型的变量(因为该类型占2个字节)。
不过有一种情况,假设该变量的内存起始地址发生了一点变化,不以0x00001
作为起始地址:
此时,short
类型的变量的内存起始地址为0x00002
,CPU第一次读取数据时,只能读取到0x00002
这个地址上的数据,还需要再读取一次才能读取到完整的变量。这样就导致了CPU读取数据的次数增加,同时还需要算上剔除多余的数据(0x00001&0x00004
)以及合并两个字节的时间,从而降低了内存访问的效率。
此时,我们就可以看到内存对齐的重要性了。分配内存时,编译器会根据变量的类型,自动调整变量的内存地址,使其符合CPU读取数据的要求(它的地址只要是它的长度的整数倍就行了)。
也就是说,有了内存对齐,例子中的short
类型变量就不会出现上述情况,CPU会尽量的以最少的次数读取到完整的数据。当然例子举得不是特别好,应该以0x00000
为起始地址,这里只是为了说明问题。
结构体的内存对齐
这里先用golang
来举例:
|
|
同样的结构体定义,只是换了成员顺序,结果分配的内存大小却不一样。继续画图说明:
结构体的内存布局时根据成员的顺序来决定的,这里Person1第一个成员为int32
类型,占4个字节(0x0~0x3)。之后第二个成员为int64
类型,占8个字节(0x8~0x15)。大家注意到0x4~0x7空出来了,因为上文说到的:
它的地址只要是它的长度的整数倍就行了
BirthUnix
成员的起始地址必须是8的整数倍,也就是要从0x8开始。就导致Age
字段之后的空间空出了4个字节。
第三个字段Ip
是int32
类型,占4个字节(0x16~0x19)。之后又空出了4个字节(0x20~0x23)是因为结构体内存对齐有个规则:结构体的总大小为 最大对齐数的整数倍(每个成员变量都有自己的对齐数),如有需要编译器会在最末一个成员之后加上填充字节。。
所以Person1的内存大小为24个字节。通过图片我们得出优化结论,如果把Ip
字段放到BirthUnix
字段之前,那么Ip
字段的起始地址就是0x4,BirthUnix
字段的起始地址还是0x8,这样就不会浪费4个字节的空间了。相当于把图中绿色的部分挪到黄色后面:
这样Person2的内存大小就是16个字节了。
平时在写代码时,可以适当地利用结构体内存对齐的特性,来减少内存的浪费。
这里补充下结构体内存对齐的规则:
- 第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始
- 以后每个成员相对于结构体首地址的 offset 都是该成员大小的整数倍,如有需要编译器会在成员之间加上填充字节 (上文提到了)
- 结构体的总大小为 最大对齐数的整数倍(每个成员变量都有自己的对齐数),如有需要编译器会在最末一个成员之后加上填充字节 (上文提到了)
- 如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍