内存对齐为何被需要
架构规定了数据类型大小的同时,也规定了对这些类型的变量合法访问的对齐要求。 也就是说,变量不能随便的放在内存的任意位置,起始地址必须满足特定的对齐要求, 对不满足要求的变量强行访问就叫做非对齐访问, 非对齐访问通常会触发异常。
一般数据类型的对齐要求
对于一般的数据类型,比如 int, long, char 这些,要求其变量地址对齐到自身大小, 比如 ARM64 中,int 变量的地址必须对齐到 4 字节,long 变量地址必须对齐到 8 字节等等。
那么对于*(int *)0x1001 = 1234;
, 这类的内存访问就叫非对齐的内存访问。
即 (变量 addr % 变量 size) != 0, 就称为非对齐内存访问。
结构体的对齐要求
上面说的还都是一般的数据类型,对于结构体这种复杂的类型,对齐的要求也复杂些。
- 首先是结构体成员,每个成员都必须满足其自身的对齐要求
- 然后是结构体变量自身的起始地址的对齐要求是其所有成员的最大对齐要求。
然而两个要求均满足有时候根本不可能,比如一个结构体声明为:
struct foo {
char mem1;
int mem2;
short mem3;
};
不可能同时做到 foo 变量和其成员 mem2 同时满足对齐到 4 字节,所以编译器会依据 上面的两条要求在成员之间添加 padding。
除了变量中间添加 padding 外,在末尾也会添加,使得结构体数组容易满足对齐需求。
最后 foo 变量在内存中的样子可能是:
struct foo {
char mem1;
char _pad1[3]; // 保证结构体和成员均对齐正确
int mem2;
short mem3;
char _pad2[2]; // 保证【结构体数组】对齐正确
};
若结构体的成员还是一个结构体,嵌套操作就可以了,编译器可以 handle。
对于结构体的定义来说,若不想添加 padding,可以使用
__attribute((packed))
来声明。 常用于一些数据包的声明,除非你清楚自己为什么要这么做,要不别用。
如何做到内存对齐
上面一节说明了各个类型的变量对于内存对齐的需求,只要是各个类型变量的地址满足要求了, 对所有变量的访问也就 OK 了。那么如何保证每个变量地址都满足需求呢?
对于静态分配的变量,即在编译链接时期就能确定地址,由编译器完成这项工作。编译器保证 分配给这些变量的地址是满足对齐要求的,这个完全不用担心。
对于运行时动态分配的变量,例如malloc()
接口返回的,其实 malloc 本身不知道要申请
空间的对齐规则,因为它只接受一个 size 作为参数。 所以一般来说,为了保证满足所有的
对齐要求,malloc()
返回的地址总是满足最大的对齐请求,即指针的大小 8 字节。
malloc()
的实际效果与运行库的实现有关,并不是规定死的。不过我还没有见过不是 按照最大对齐要求分配的实现方法:)
AArch64 对非对齐访问的支持
非对齐访问的结果是架构定义的, 不同的架构可能造成的结果不同:
- 架构可能支持非对齐访问,成功读取数据
- 架构不支持非对齐访问,产生异常
AArch64 架构支持 16、32、64 和 128 位的非对齐访问,但是有几个前提条件:
- 关闭系统的对齐检查:
SCTLR_ELx.A
bit 来控制 - exclusive load/store 和 load-acuqire/store-release 两类指令必须是对齐访问的 。这就表示构建信号量和其他锁机制时必须是对齐访问的
- 非对齐访问仅“普通内存”可用,"Device memory“必须是对齐访问的
AArch64 非对齐访问的原理是分解为多次的访存,所以不能保证原子性,且性能是较差的。
虽然 AArch64 支持非对齐访问, 但编译器默认还是会生成满足对齐要求的代码。