1. 内存对齐(Data Structure Alignment)是什么
内存对齐,或者说字节对齐,是一个数据类型所能存放的内存地址的属性(Alignment is a property of a memory address)。
这个属性是一个无符号整数,并且这个整数必须是2的N次方(1、2、4、8、……、1024、……)。
当我们说,一个数据类型的内存对齐为8时,意思就是指这个数据类型所定义出来的所有变量,其内存地址都是8的倍数。
当一个基本数据类型(fundamental types)的对齐属性,和这个数据类型的大小相等时,这种对齐方式称作自然对齐(naturally aligned)。
比如,一个4字节大小的int型数据,默认情况下它的字节对齐也是4。
2. 为什么我们需要内存对齐
这是因为,并不是每一个硬件平台都能够随便访问任意位置的内存的。
Many CPUs, such as those based on Alpha, IA-64, MIPS, and SuperH architectures, refuse to read misaligned data. When a program requests that one of these CPUs access data that is not aligned, the CPU enters an exception state and notifies the software that it cannot continue. On ARM, MIPS, and SH device platforms, for example, the operating system default is to give the application an exception notification when a misaligned access is requested.
大意是说,有不少平台的CPU,比如Alpha、IA-64、MIPS还有SuperH架构,若读取的数据是未对齐的(比如一个4字节的int在一个奇数内存地址上),将拒绝访问,或抛出硬件异常。
另外,在维基百科里也记载着如下内容:
Data alignment means putting the data at a memory offset equal to some multiple of the word size, which increases the system's performance due to the way the CPU handles memory.
意思是,考虑到CPU处理内存的方式(32位的x86 CPU,一个时钟周期可以读取4个连续的内存单元,即4字节),使用字节对齐将会提高系统的性能(也就是CPU读取内存数据的效率。比如你一个int放在奇数内存位置上,想把这4个字节读出来,32位CPU就需要两次。但对齐之后一次就可以了)。
3. 内存对齐带来的数据结构大小变化
因为有了内存对齐,因此数据在内存里的存放就不是紧挨着的,而是可能会出现一些空隙(Data Structure Padding,也就是用于填充的空白内容)。因此对基本数据类型来说可能还好说,对于一个内部有多个基本类型的结构体(struct)或类而言,sizeof的结果往往和想象中不大一样。
让我们来看一个例子:
-
struct MyStruct
-
{
-
char a; // 1 byte
-
int b; // 4 bytes
-
short c; // 2 bytes
-
long long d; // 8 bytes
-
char e; // 1 byte
-
};
我们可以看到,MyStruct中有5个成员,如果直接相加的话大小应该是16,但在32位MSVC里它的sizeof结果是32。
之所以结果出现偏差,为了保证这个结构体里的每个成员都应该在它对齐了的内存位置上,而在某些位置插入了Padding。
下面我们尝试考虑内存对齐,来计算一下这个结构体的大小。首先,我们可以假设MyStruct的整体偏移从0x00开始,这样就可以暂时忽略MyStruct本身的对齐。这时,结构体的整体内存分布如下图所示:
我们可以看到,char和int之间;short和long long之间,为了保证成员各自的对齐属性,分别插入了一些Padding。
因此整个结构体会被填充得看起来像这样:
-
struct MyStruct
-
{
-
char a; // 1 byte
-
char pad_0[3]; // Padding 3
-
int b; // 4 bytes
-
short c; // 2 bytes
-
char pad_1[6]; // Padding 6
-
long long d; // 8 bytes
-
char e; // 1 byte
-
char pad_2[7]; // Padding 7
-
};
注意到上面加了Padding的示意结构体里,e的后面还跟了7个字节的填充。这是因为结构体的整体大小必须可被对齐值整除,所以“char e”的后面还会被继续填充7个字节好让结构体的整体大小是8的倍数32。
我们可以在gcc + 32位linux中尝试计算sizeof(MyStruct),得到的结果是24。
这是因为gcc中的对齐规则和MSVC不一样,不同的平台下会使用不同的默认对齐值(The default alignment is fixed for a particular target ABI)。在gcc + 32位linux中,大小超过4字节的基本类型仍然按4字节对齐。因此MyStruct的内存布局这时看起来应该像这个样子:
下面我们来确定这个结构体类型本身的内存对齐是多少。为了保证结构体内的每个成员都能够放在它自然对齐的位置上,对这个结构体本身来说最理想的内存对齐数值应该是结构体里内存对齐数值最大的成员的内存对齐数。
也就是说,对于上面的MyStruct,结构体类型本身的内存对齐应该是8。并且,当我们强制对齐方式小于8时,比如设置MyStruct对齐为2,那么其内部成员的对齐也将被强制不能超过2。
为什么?因为对于一个数据类型来说,其内部成员的位置应该是相对固定的。假如上面这个结构体整体按1或者2字节对齐,而成员却按照各自的方式自然对齐,就有可能出现成员的相对偏移量随内存位置而改变的问题。
比如说,我们可以画一下整个结构体按1字节对齐,并且结构体内的每个成员按自然位置对齐的内存布局:
上面的第一种情况,假设MyStruct的起始地址是0x01(因为结构体本身的偏移按1字节对齐),那么char和int之间将会被填充2个字节的Padding,以保证int的对齐还是4字节。
如果第二次分配MyStruct的内存时起始地址变为0x03,由于int还是4字节对齐,则char和int之间将不会填充Padding(填充了反而不对齐了)。
以此类推,若MyStruct按1字节对齐时不强制所有成员的对齐均不超过1的话,这个结构体里的相对偏移方式一共有4种。
因此对于结构体来说,默认的对齐将等于其中对齐最大的成员的对齐值。并且,当我们限定结构体的内存对齐时,同时也限定了结构体内所有成员的内存对齐不能超过结构体本身的内存对齐。