C++ 内存对齐
1 | |
运行上面的代码,会输出
1 | |
也就是说,类A的大小并不是它所有成员变量的大小之和,这是因为结构体在存放成员变量时,会进行内存对齐。
为什么要进行内存对齐
现在的大部分处理器都不是以字节为单位进行内存存取,而是以一定的内存存取粒度进行存取。例如,如果某个CPU的内存存取粒度为8字节,那么它就会以8字节为单位对内存进程存取。而且处理器对内存的访问分为
- 对齐访问
即起始地址需要是访问宽度的整数倍。假设我们有一块大小为64kb的内存,其地址范围为0x0000~0xFFFF,且处理器的存取粒度为8字节,那么它只能直接存取以8的倍数为起始地址的大小为8的内存块,即0x0000~0x0007,0x0008~0x000F……0xFFF8~0xFFFF这些内存块,而无法直接读取类似0x0002~0x0009这样的内存块。 - 非对齐访问
有的架构(如 x86)直接支持,但可能被拆成两次对齐访问;有的架构(如早期的 ARM、MIPS)则会抛出异常或需要软件辅助来完成,但无论如何,都会导致性能的下降。
现在假如我们不对结构体进行内存对齐,那么struct A的布局会是
| 成员变量 | 地址范围 |
|---|---|
a |
0x0000~0x0001 |
b |
0x0002~0x0009 |
c |
0x000A~0x000D |
内存存取粒度为8的处理器在读取b和c的值时就是非对齐访问,会导致访问效率的下降。要还原b的值都需读取两次内存,要还原c的值还需要判断读取的8个字节中哪4个字节属于c。
因此在存储数据时对其进行内存对齐,能够节省大量的时间开销,提高内存的访问效率。
内存对齐的规则
- 第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始。
- 以后每个成员相对于结构体首地址的
offset都是该成员对齐数大小的整数倍(如果有#pragma pack(n)的话,则是成员对齐数和n中更小的那个值的整数倍),如有需要编译器会在成员之间加上填充字节。 - 结构体的总大小为 最大对齐数的整数倍(每个成员变量都有自己的对齐数,通过
alignof()获取),如有需要编译器会在最末一个成员之后加上填充字节。 - 如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。
我们修改一开始例子的中成员变量的顺序
1 | |
结构体A的大小就变成了8+2+4+(2)共16字节。
常用操作
alignof()
- 用于获取某个类型对象的对齐数,它决定了该类型的对象在内存中必须以多少字节为单位对齐。例如类型
T的对齐数是k,那么任何T类型的对象,其起始地址(基址)都必须是k的整数倍。 - 自定义类型的对齐数等于其成员变量对其数最大的成员变量的对齐数,除非通过
alignas(n)指定了自定义对象的对齐数 - 对齐数的大小为2的幂次
alignof()的参数可以是某个类型的对象,也可以是类或变量的名称sizeof和alignof的结果不一定一样1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27struct alignas(32) A {
int64_t a;
int16_t b;
float c;
};
struct B{
int a;
int b;
char c;
};
int main() {
A a;
int arr[4] = {1, 2, 3, 4};
std::cout << "sizeof(int16_t):" << sizeof(int16_t) << std::endl; // 2
std::cout << "alignof(int16_t):" << alignof(int16_t) << std::endl; // 2
std::cout << "sizeof(A):" << sizeof(a) << std::endl; // 32
std::cout << "alignof(A):" << alignof(a) << std::endl; // 32
std::cout << "sizeof(B):" << sizeof(B) << std::endl; // 12
std::cout << "alignof(B):" << alignof(B) << std::endl; // 4
std::cout << "sizeof(arr):" << sizeof(arr) << std::endl; // 16
std::cout << "alignof(arr):" << alignof(arr) << std::endl; // 4
return 0;
}
alingas()
在C++11中引入,用于设置变量的内存对齐方式。
1 | |
- 指定的对齐值不得小于该类型的天然对齐要求。否则编译器会报错。
1
alignas(2) double x; // 错误:double 的 alignof(double) 通常是 8,不能用 2 覆盖 - C++17中引入了无参数的
alignas,可以用于恢复天然对齐1
2struct alignas(16) S1 { /* ... */ };
struct alignas() S2 { /* 恢复为天然对齐 */ };
#pragma pack()
- 每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令
#pragma pack(n),n=1,2,4,8,16来改变这一系数。 #pragma pack(n)规定的是上界,只影响对齐s数元大于n的成员。- 通过预编译命令
#pragma pack()可以取消自定义字节对齐方式。
_attribute__()
用于给类型、函数或变量添加编译器属性,通常写成两层圆括号
1 | |
我们这里只讨论其与内存对齐有关的操作
__attribute__((aligned(n)))
用于强制指定变量或类型至少按照n字节对齐,n必须为2的幂__attribute__((aligned))
让编译器根据目标机制采用最大最有益的方式对齐,一般是16字节__attribute__((packed))
取消默认对齐规则,将结构体成员“紧凑”地打包在一起。
1 | |
参考
C++ 内存对齐
https://guts.homes/2025/06/20/cpp-mem-alignment/