内存对齐
本次主要讨论三个问题:
- 什么是内存对齐
- 内存对齐的好处
- 如何对齐
内存对齐
内存对齐是一种提高内存访问速度的策略。编译器为程序中的每个数据单元安排在适当的位置上。计算机系统对基本类型数据在内存中存放的位置有限制,它们要求这些数据的首地址的值是某个数M(通常是4或8,Windows中默认对齐数为8,Linux中默认对齐数为4);为了提高程序的性能,数据结构,特别是栈,应尽可能在自然边界上对齐,经过对齐后,cpu的内存访问速度大大提升。
内存的自然对齐,每一种数据类型都必须放在地址中的整数倍上。例如:地址4可以放char(1)类型,可以放int(4)型,可以放short(2)型,但是不能存放double(8)型,仅仅因为4不是8的整数倍。地址3能存放char型,但是其他int,short,double都不能存放。有一个特殊地址,就是0,它可以是任何类型的整数倍,所以可以存放任何数据。根据这个规则,那么在分配一大块包含很多变量的内存的时候,会产生很多碎片。
内存对齐的好处
为了提高效率,计算机从内存中取数据是按照一个固定长度的来取的。以32位机为例,它每次取32个位,也就是4个字节(每字节8个位,计算机基础知识)。每个总线周期都是从偶地址开始读取32位的内存数据,如果数据存放地址不是从偶数开始,则可能出现需要两个总线周期才能读取到想要的数据。
以int型数据为例,如果它在内存中存放的位置按4字节对齐,也就是说1个int的数据全部落在计算机一次取数的区间内,那么只需要取一次就可以了。如果不对齐,很不巧,这个int数据刚好跨越了取数的边界,这样就需要取两次才能把这个int的数据全部取到,这样效率也就降低了。
另外一个是平台原因(移植原因), 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
另外,不同的编译器可能会对内存的分布进行优化,但这属于编译器的问题,暂不做过多讨论。
如何对齐
以struct为例:
struct A{
char a;
int b;
short c;
}
在32位机器上char 占1个字节,int 占4个字节,short占2个字节,一共占用7个字节,但实际上并非如此。测试输出的结果是A: 12, 比计算的7多了5个字节。这个就是因为编译器在编译的时候进行了内存对齐导致的。
内存对齐主要遵循下面三个原则:
结构体变量的起始地址能够被其最宽的成员大小整除
结构体每个成员相对于起始地址的偏移能够被其自身大小整除,如果不能则在前一个成员后面补充字节
结构体总体大小能够被最宽的成员的大小整除,如不能则在后面补充字节
在内存中,编译器按照成员列表顺序分别为每个结构体变量成员分配内存,当存储过程中需要满足边界对齐的 要求时,编译器会在成员之间留下额外的内存空间。如char=1,short=2,int=4,double=8。所谓自对齐,指的是该成员的起始位置的内存地址必须是它自身长度的整数倍。如int只能以0,4,8这类的地址开始。
编译器在编译的时候是可以指定对齐大小的,实际使用的有效对齐其实是取指定大小和自身大小的最小值,一般默认的对齐大小是4。结构体的有效对齐值规定如下:
1)当未明确指定时,以结构体中最长的成员的长度为其有效值
2)当用#pragma pack(n)指定时,以n和结构体中最长的成员的长度中较小者为其值
3)当用__attribute__ ((__packed__))指定长度时,强制按照此值为结构体的有效对齐值
如果默认的对齐大小是4,结构体a的其实地址为0x0000,能够被最宽的数据成员大小(这里是int, 大小为4,有效对齐大小也是4)整除,故char a的从0x0000开始存放占用一个字节即0x0000~0x0001,然后是int b,其大小为4,需要满足第二个原则,从0x0004开始,所以在char a后填充三个字节,因此a对齐后占用的空间是0x0000~0x0003,b占用的空间是0x0004~0x0007, 然后是short c其大小是2,故从0x0008开始占用两个字节,即0x0008~0x000A。 此时整个结构体占用的空间是0x0000~0x000A, 占用11个字节,11%4 != 0, 不满足第三个原则,所以需要在后面补充一个字节,即最后内存对齐后占用的空间是0x0000~0x000B,一共12个字节。
当前的使用
内存对齐本身对程序员来说是透明的,即程序员该取变量就取变量,该存就存,编译程序时编译器会把变量按本身的平台进行对齐。但如果你要写一个内存池(如boost的ordered_pool),或者使用了reinterpret_cast这种对内存直接进行操作的函数,这方面还是要注意一下的,即使CPU支持,效率也会受到影响。
swift 的 json转换工具库 HandyJSON做处理时为了绕过反射赋值,直接写入内存,就用到了内存对齐。类实例的属性并不是直接按照各自占位大小依次往下排列的,和C/C++一样,Swift中实例内存布局也考虑了内存对齐。对齐的规则为,每个字段的起始地址必须为alignment值的整数倍。以此来计算下一个字段起始地址。