现代计算机体系中 CPU 按照双字、字、字节访问存储内存,并通过总线进行传输,若未经过一定规则的数据对齐,CPU 的访址操作与总线的传输操作将会异常的复杂,所以现代编译器中都会对内存进行自动的对齐。
主要作用
平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。比如,有些架构的 CPU 在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐。
性能原因
经过内存对齐后,CPU 的内存访问速度大大提升。比如,有些平台每次读都是从偶地址开始,如果一个 int 型(假设为 32 位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这 32 位,而如果存放在奇地址开始的地方,就需要 2 个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该 32 位数据。显然,这在读取效率上下降很多。
假设 CPU 要读取一个 int 型 4 字节大小的数据到寄存器中,分两种情况讨论(假定内存读取粒度为 4):
从 0 字节开始:CPU 只需读取内存一次即可把这 4 字节的数据完全读取到寄存器中。
从 1 字节开始:此时该 int 型数据不是位于内存读取边界上,这就是一类内存未对齐的数据,需要读 2 次。
对齐规则
默认情况
在默认情况下,编译器规定各成员变量存放的起始地址相对于结构的起始地址的 偏移量
必须为该变量的类型所占用的字节数的倍数。
类型 | 对齐倍数(偏移量相对于类型的字节大小) |
---|---|
char | 1 倍 |
short | 2 倍 |
int | 4 倍 |
float | 4 倍 |
double | 8 倍 |
各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节 编译器
会自动填充。同时,编译器为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节。
例子:
1 | struct mystruct { |
对齐参数
默认的对齐方式为 8 字节。有时,我们自己可以设定变量的对齐方式,#pragma pack(n)
来设定变量以 n 字节对齐。n 字节对齐就是说变量存放的起始地址的偏移量有两种情况:
默认对齐方式:按照变量的类型大小对齐。
如果该变量的类型所占用的字节数小于等于 n,那么偏移量必须满足默认的对齐方式(必须为类型大小的整数倍);
如果该变量的类型所占用的字节数大于 n,那么偏移量必须为 n 的倍数,不满足默认的对齐方式。
此外,结构的总大小也有约束条件,分下面两种情况:
如果所有成员变量类型所占用(所分配的空间)的字节数小于等于 n,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数(即默认对齐方式);
否则,结构的总大小必须为 n 的倍数。
或者,我们也可以这样表述(读者可以自行对比和理解,很重要!):
对于结构的各个成员,第一个成员位于偏移为 0 的位置,以后每个数据成员的偏移量必须是
min(n, 这个数据成员的自身长度)
的倍数;结构(或联合)本身也要进行对齐,对齐将按照
min(n, 结构(或联合)最大数据成员长度)
进行对齐。
总结:类型大小小于等于 n,看类型大小;大于 n,看 n;n 是个阈值。
第一个例子
1 |
|
以上结构的大小为 16,下面分析其存储情况:
首先为 m1 分配空间,其偏移量为 0,满足我们自己设定的对齐方式(4 字节对齐),m1 占用 1个字节。
然后开始为 m4 分配空间,这时其偏移量为 1,由于 double 占用 8 个字节(大于 4),需要补足 3 个字节以满足 4 字节对齐,而 m4 本身占用 8 个字节,即分配了 11 个字节。(默认情况则需要 15 字节,节省了空间)
接着为 m3 分配空间,这时其偏移量为 12,m3 占用 4 个字节(小于等于 4),偏移量仅需满足 int 类型大小的倍数,因此不用填补。
最后,此时已经为所有成员变量分配了空间,共分配了
4 + 8 + 4 = 16
个字节。因为所有各个变量的类型大小并不满足小于等于 n(double 不满足),所以结构总大小必须满足为 n 的倍数,因此不用填补。
总结:调小对齐参数可以节省存储空间,如给 m4 分配空间时,节省了 4 个字节。
第二个例子
1 |
|
S1 中,成员 a 占 1 字节,默认按 8 字节对齐,指定对齐参数为8,偏移量为 0,不用填补字节;成员 b 占 4 个字节,默认是按 8 字节对齐,偏移量为 1,这时就按 8 字节对齐,需要填补 7 个字节,且本身占 8 个字节,所以 sizeof(struct S1)
应该为 16。(假定 long 为 8 字节,即在 64 位机器下)
S2 中,c 和 S1 中的 a 同理。而 d 是个结构体,它占 16 个字节,大于参数 8,按照参数 n 对齐,即 8 字节对齐,补 7 个字节,所以共分配了 23 个字节。接着,成员 e 占 8 个字节,小于等于 8,按照本身类型大小 8 字节对齐,不用填补。最后,S2 中有成员的类型大小大于参数 8,因此结构体大小按照 8 字节对齐。由于此时偏移量为 32,为 8 的倍数,因此无需再填补,所以 sizeof(struct S2)
应该为 32。
注意:有的地方说 S2 中 S1 的对齐应该按照 S1 中最大的对齐参数进行对齐,即 8。