一、内存对齐,本质上是CPU访问内存的硬件约束
-
CPU并不是以字节为单位来读取内存,而是以字(Word)为单位(4字节或8字节,根据电脑型号不同决定)。
-
内存对齐要求数据的起始地址必须是其自身大小的整数倍。(比如int占4字节,则起始地址必须能被4整除)
-
这不是C++语法限制,而是CPU指令集的物理硬性要求与性能要求。
二、内存对齐的原因
1、硬件限制
-
像ARM、MIPS这类RISC(精简指令集计算机)架构的CPU,如果读取一个int但地址是奇数对齐,会直接触发硬件异常,导致进程崩溃。
-
而x86,x64这种CISC(复杂指令集计算机)架构的CPU,能容忍未对齐访问,但在内部需要将一次读取拆解成两次内存总线事务。
2、性能损耗
-
假设一个int(4字节)存放在地址0x1001未对齐的地址上,CPU需要先读取0x1000~0x1003这4个字节,再读取0x1004~0x1007这4个字节,然后再通过移位和拼接操作拼出正确的int值。
-
而如果放在异对齐的0x1000地址,cpu只需要一条指令即可读取0x1000~0x1003这4个字节的数据
-
对齐访问时原子操作,未对齐访问时组合操作,这就是性能差距的根源所在
3、缓存行隔离
-
现代CPU以64字节缓存行为单位来加载数据,对齐能确保一个数据不会横跨两个缓存行,避免多次内存加载,并防止伪共享带来的并发性能暴跌。
三、内促对齐规则
1、编译器默认对齐规则
char:对齐到 1 字节(任意地址)。
short:对齐到 2 字节(地址 % 2 == 0)。
int:对齐到 4 字节(地址 % 4 == 0)。
double:对齐到 8 字节(地址 % 8 == 0)。
指针(64位):对齐到 8 字节。
2、结构体内存对齐规则
-
结构体的大小必须是最大成员对齐要求的整数倍,并且结构体中每个属性都要求具有最大长度的空间对齐
-
示例:
#include <iostream>
// ========== 结构体 A(内存浪费严重) ==========
struct A {
char c; // 1 字节,偏移 0
// 填充 3 字节(偏移 1~3),为了对齐下面的 int
int i; // 4 字节,偏移 4(能被4整除)
char d; // 1 字节,偏移 8
// 尾部填充 3 字节(偏移 9~11),为了让整个结构体大小是 4 的倍数
};
// sizeof(A) = 12 字节
// 内存布局: [c][ ][ ][ ][i][i][i][i][d][ ][ ][ ]
// ========== 结构体 B(优化内存布局) ==========
struct B {
int i; // 4 字节,偏移 0
char c; // 1 字节,偏移 4
char d; // 1 字节,偏移 5
// 尾部填充 2 字节(偏移 6~7),让整体是 4 的倍数
};
// sizeof(B) = 8 字节
// 内存布局: [i][i][i][i][c][d][ ][ ]
int main() {
std::cout << "A 大小: " << sizeof(A) << std::endl; // 输出 12
std::cout << "B 大小: " << sizeof(B) << std::endl; // 输出 8
// 结论:仅仅调整成员顺序,B 比 A 节省了 33% 的内存!
}
-
结构体对齐操作是先填充在对齐,而且是每个属性都按照最大字节属性的空间来填充。
-
所以对结构体内存优化操作:可以先把占用空间大的成员放在前面,小的放在后面,这样填充和总的结构体大小会变短。
四、内存对齐控制工具
1、alignof与alignas
-
alignof可以查询属性对齐要求的大小,alignas可强制指定对齐的大小
// 查询类型的对齐要求
std::cout << alignof(int) << std::endl; // 输出 4
std::cout << alignof(double) << std::endl; // 输出 8
// 强制指定对齐(比如 SIMD 需要的 16 字节对齐)
struct alignas(16) AlignedStruct {
float data[4];
};
std::cout << sizeof(AlignedStruct) << std::endl; // 16(对齐到16的倍数)
std::cout << alignof(AlignedStruct) << std::endl; // 16
2、#pragma pack 强制压缩结构体
-
取消内存填充,多用于网络协议封包与读写二进制文件头
#pragma pack(push, 1) // 将对齐系数强制设为 1(最紧凑)
struct NetworkPacket {
char type; // 1 字节
int seq; // 本来是4对齐,现在紧挨着 type,偏移 1
short len; // 紧挨着
};
#pragma pack(pop) // 恢复默认对齐
// sizeof(NetworkPacket) = 1 + 4 + 2 = 7 字节(无任何填充)
3、attribute无填充设置
struct __attribute__((packed)) GccPacked {
char c;
int i;
};
// 同样无填充,大小 = 5
五、new与malloc底层内存对齐解析
-
malloc与operate new保证返回的地址满足当前平台最严格的对齐要求,也就是16字节,可兼容long double类型和SIMD,满足一条指令可同时处理多条数据的操作
-
当new int时,返回的地址一定是4的倍数,所以即使我们写char *p = new char[1]时,内存的起始地址也是16字节对齐的。
951



被折叠的 条评论
为什么被折叠?



