1. 内存对齐简介#
在讨论内存对齐之前,我们先来看一下计算机底层是怎么对数据进行存取的,这涉及到计算机组成原理的知识。
这主要涉及计算机中的三大总线:地址总线、数据总线和控制总线。地址总线决定数据的地址,数据总线里面传输的是数据,控制总线传输的是命令和状态信号。数据总线的宽度直接决定了一个CPU周期可以同时传送多少位。CPU的设计是高度协同的,寄存器和数据总线的宽度一般相同(Intel早期的8086的寄存器宽度为16位,总线宽度为8位),我们平时所说的xx位的计算机实际上指的是CPU寄存器的宽度,例如:
- 32位计算机:CPU寄存器和数据总线的宽度都为32位
- 64位计算机:CPU寄存器和数据总线的宽度都为64位
现代计算机中,内存空间的最小单位是字节,因为CPU的地址总线以字节为最小寻址粒度。但是,大部分的处理器并不是按字节来存取内存的,一般会以2字节,4字节,8字节来存取内存,我们称之为内存存取粒度。这个存取的宽度就取决于我们上面介绍的总线宽度,例如32位计算机每次传输的是32位(4字节),而64位计算机每次传输的是64位(8字节)。
以32位计算机为例,假设我们要在0x0001
这个地址中写入一个字符'A'
(1字节)。CPU会将0X0000
放在地址总线,让内存控制器知道目标在0x0000
到0x0003
那个内存块,然后会将字符'A'
放在数据总线的对应位置,即4字节块中的第二个字节,CPU会使能第二天字节使能线,这个控制指令放在控制总线中,这样CPU可以在对应的位置写入而不影响内存块中的其他字节。
再举个例子,假设在一个32位的计算机中,要读取一个地址为0x1001
的int
变量,那么这个4字节的int
变量占用的内存是0x1001
- 0x1005
。由于32位计算机的内存存取粒度是4,因此该处理器只能从地址为4的倍数的内存开始读取数据。第一次会读取0x1000
- 0x1003
,剔除不需要的字节(0x1000
),然后第二次读取0x1004
- 0x1007
,剔除不要的数据(0x1005
、0x1006
、0x1007
),最后将数据合并完成读取。
那内存对齐是什么呢?其实内存对齐是一种优化技术,旨在提高CPU访问内存的速度,从以上的例子可以看出,虽然int
的大小等于内存存取粒度,但是由于存放的位置,CPU需要进行两次读取。而内存对齐则是对数据再内存中存放的位置的限制,通常会要求数据的首地址的值是某个数k的倍数。例如如果规定int
的首地址只能是4的倍数,那么对int的读取就可以一次性完成。
2. 内存对齐规则#
一些核心概念如下:
- 自身对齐值:这是数据类型本身固有的对齐要求,通常等于其 sizeof 的大小。
char
: 1 字节short
: 2 字节int
: 4 字节float
: 4 字节double
: 8 字节long
: 4 字节(32为系统)或 8 字节(64位系统)long long
: 8 字节- 指针: 4 字节(32位系统)或 8 字节(64位系统)
- 指定对齐值:这是程序员通过编译器指令(如 C/C++ 中的
#pragma pack(n)
) 手动设定的对齐值。n 的值通常是1, 2, 4, 8等2的幂。 - 有效对齐值 (Effective Alignment):这是编译器最终为某个数据成员采用的对齐值。它的计算规则是: 有效对齐值 = Min(自身对齐值, 指定对齐值)。
所有的数据结构,无论是struct
还是union
,在布局时都遵循以下三条规则:
- 结构体中每个成员相对于结构体起始地址的偏移量,必须是其有效对齐值的整数倍。如果不是,编译器会在前一个成员后面填充若干字节以满足要求。
- 结构体(或联合体)的总大小,必须是其所有成员中最大的有效对齐值的整数倍。如果不是,编译器会在最后一个成员后面填充若干字节以满足要求。
- 如果一个结构体包含了另一个嵌套结构体,那么这个嵌套结构体的起始偏移量,必须是其内部所有成员中最大的有效对齐值的整数倍。
3. 内存对齐举例#
在64位系统中,一个结构体定义如下:
struct MyStruct {
char a; // 自身对齐1
int b; // 自身对齐4
double c; // 自身对齐8
short d; // 自身对齐2
};
cpp- 第一个成员
a
,有效对齐值为1,当前偏移量为0,是1的倍数,因此当前大小为1字节。 - 第二个成员
b
,有效对齐值4,当前偏移量为1,不是4的倍数,前面要填充3字节,因此当前大小为8字节。 - 第三个成员
c
,有效对齐值为8,当前偏移量为8,是8的倍数,因此当前大小为16字节。 - 第四个成员
d
,有效对齐值为2,当前偏移量为10,是2的倍数,因此当前大小为18字节。 - 在结构体中,最大有效对齐值为8,18不是8的倍数,需要填充到24,后面填充6个字节。
因此这个结构体在内存中的布局如下:
下面举一个指定对齐值的例子:
#pragma pack(push, 2) // 设置指定对齐值为2
struct MyStructPacked {
char a; // 自身对齐1->有效对齐min(1,2)=1
int b; // 自身对齐4->有效对齐min(4,2)=2
double c; // 自身对齐8->有效对齐min(8,2)=2
short d; // 自身对齐2->有效对齐min(2,2)=2
};
#pragma pack(pop) // 恢复默认
cpp- 第一个成员
a
,有效对齐值为1,当前偏移量为0,是1的倍数,因此当前大小为1字节。 - 第二个成员
b
,有效对齐值为2,当前偏移量为1,不是2的倍数,前面要填充1字节,因此当前大小为6字节。 - 第三个成员
c
,有效对齐值为2,当前偏移量为6,是2的倍数,因此当前大小为14字节。 - 第四个成员
d
,有效对齐值为2,当前偏移量为14,是2的倍数,因此当前大小为16字节。 - 在结构体中,最大有效对齐值为2,16是8的倍数,不需要再填充,因此总大小为16字节。
因此这个结构体在内存中的布局如下: