为什么会有内存对齐以下内容节选自《Intel Architecture 32 Manual》。字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和四字来说,自然边界分别是偶数地址,可以被4整除的地址,和可以被8整除的地址。)无论如何,为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。某些操作双四字的指令需要内存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常(#GP)。双四字的自然边界是能够被16 整除的地址。其他的操作双四字的指令允许未对齐的访问(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。如何避免内存对齐的影响那么,能不能既达到提高性能的目的,又能节约一点空间呢?有一点小技巧可以使用。比如我们可以将上面的结构改成:struct bar{char c1;char c2;short s;int i;};这样一来,每个成员都对齐在其自然边界上,从而避免了编译器自动对齐。在这个例子中,sizeof(bar) == 8。这个技巧有一个重要的作用,尤其是这个结构作为API的一部分提供给第三方开发使用的时候。第三方开发者可能将编译器的默认对齐选项改变,从而造成这个结构在你的发行的DLL中使用某种对齐方式,而在第三方开发者哪里却使用另外一种对齐方式。这将会导致重大问题。比如,foo结构,我们的DLL使用默认对齐选项,对齐为c1 00000000, s 00000002, c2 00000004, i 00000008,同时sizeof(foo) == 12。而第三方将对齐选项关闭,导致c1 00000000, s 00000001, c2 00000003, i 00000004,同时sizeof(foo) == 8。如何使用c/c++中的对齐选项vc6中的编译选项有 /Zp[1|2|4|8|16] ,/Zp1表示以1字节边界对齐,相应的,/Zpn表示以n字节边界对齐。n字节边界对齐的意思是说,一个成员的地址必须安排在成员的尺寸的整数倍地址上或者是n的整数倍地址上,取它们中的最小值。也就是:min ( sizeof ( member ), n)实际上,1字节边界对齐也就表示了结构成员之间没有空洞。/Zpn选项是应用于整个工程的,影响所有的参与编译的结构。要使用这个选项,可以在vc6中打开工程属性页,c/c++页,选择Code Generation分类,在Struct member alignment可以选择。要专门针对某些结构定义使用对齐选项,可以使用#pragma pack编译指令。指令语法如下:#pragma pack( [ show ] | [ push | pop ] [, identifier ] , n )意义和/Zpn选项相同。比如:#pragma pack(1)struct foo_pack{char c1;short s;char c2;int i;};#pragma pack()栈内存对齐我们可以观察到,在vc6中栈的对齐方式不受结构成员对齐选项的影响。(本来就是两码事)。它总是保持对齐,而且对齐在4字节边界上。验证代码#include <stdio.h>struct foo{char c1;short s;char c2;int i;};struct bar{char c1;char c2;short s;int i;};#pragma pack(1)struct foo_pack{char c1;short s;char c2;int i;};#pragma pack()int main(int argc, char* argv[]){char c1;short s;char c2;int i;struct foo a;struct bar b;struct foo_pack p;printf("stack c1 %p, s %p, c2 %p, i %p/n",(unsigned int)(void*)&c1 - (unsigned int)(void*)&i,(unsigned int)(void*)&s - (unsigned int)(void*)&i,(unsigned int)(void*)&c2 - (unsigned int)(void*)&i,(unsigned int)(void*)&i - (unsigned int)(void*)&i);printf("struct foo c1 %p, s %p, c2 %p, i %p/n",(unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,(unsigned int)(void*)&a.s - (unsigned int)(void*)&a,(unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,(unsigned int)(void*)&a.i - (unsigned int)(void*)&a);printf("struct bar c1 %p, c2 %p, s %p, i %p/n",(unsigned int)(void*)&b.c1 - (unsigned int)(void*)&b,(unsigned int)(void*)&b.c2 - (unsigned int)(void*)&b,(unsigned int)(void*)&b.s - (unsigned int)(void*)&b,(unsigned int)(void*)&b.i - (unsigned int)(void*)&b);printf("struct foo_pack c1 %p, s %p, c2 %p, i %p/n",(unsigned int)(void*)&p.c1 - (unsigned int)(void*)&p,(unsigned int)(void*)&p.s - (unsigned int)(void*)&p,(unsigned int)(void*)&p.c2 - (unsigned int)(void*)&p,(unsigned int)(void*)&p.i - (unsigned int)(void*)&p);printf("sizeof foo is %d/n", sizeof(foo));printf("sizeof bar is %d/n", sizeof(bar));printf("sizeof foo_pack is %d/n", sizeof(foo_pack));return 0;}----------------------------------------------------------------------------------------------------------- 在结构中,编译器为结构的每个成员按其自然对界条件分配空间;各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。在缺省情况下,c编译器为每一个变量或是数据单元按其自然对界条件分配空间例如,下面的结构各成员空间分配情况struct test {char x1;short x2;float x3;char x4;};结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求 4字节对界,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。现在你知道怎么回事了吧?更改c编译器的缺省分配策略一般地,可以通过下面的两种方法改变缺省的对界条件:· 使用伪指令#pragma pack ([n])· 在编译时使用命令行参数#pragma pack ([n])伪指令允许你选择编译器为数据分配空间所采取的对界策略:例如,在使用了#pragma pack (1)伪指令后,test结构各成员的空间分配情况就是按照一个字节对齐了#pragma pack(push) //保存对齐状态#pragma pack(1)#pragma pack(pop)
一.什么是字节对齐,为什么要对齐
现代计算机中的内存空间都是按照字节(Byte)划分的,从理论上讲,似乎对任何类型的变量的访问可以从任意地址开始,但实际情况则是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型的数据按照一定的规则在内存空间上排列,而不是顺序的一个接一个地排放,这就是对齐。
二.编译器是按照什么原则进行内存字节对齐的
1. 数据类型的自身对齐值
数据类型的自身对齐值:其在内存中所占的字节数,如在32位系统中,char为1字节,short为2字节,int、float、double、long为4字节。
2. 结构体或类的自身对齐值
结构体或类的自身对齐值:其成员中自身对齐值最大的那个值。
3. 默认对齐值
结构体或类的默认对齐值为其成员中自身对齐值最大的那个值。
4. 指定对齐值
#pragma pack(value)时指定的值value。
5. 结构体和类的有效对齐值
结构体和类的有效对齐值为:没有指定对齐值时为默认对齐值和自身对齐值的最小值;当指定了对齐值后为指定对齐值和自身对齐值的最小值。
三.如何修改编译器的默认对齐值
在编码时,可以这样动态修改:
修改默认对齐值,指定新的内存对齐值:#pragma pack(value)
取消指定的内存对齐值,恢复默认对齐值:#pragma pack()
四.如何进行内存地址对齐
对于一个结构体,不但需要对其每个成员变量进行内存地址对齐,还要对结构体本身进行对齐,具体规则为:在假设结构体起始地址为0x0000的情况下,要求各成员变量的起始地址必须是其相应有效对齐值的整数倍,并要求结构体的大小也为该结构体有效对齐值的整数倍。
例子
例一:
struct A
{
int a;
char b;
short c;
};
则该结构体所占的内存字节数sizeof(struct A) = 8;
具体分析如下:
故该结构体及其成员变量的有效对齐值为编译器默认对齐值和其自身对齐值的最小值:
int a 的自身对齐值为4
char b 的自身对齐值为1
short c 的自身对齐值为2
struct A 的自身对齐值为max(4,1,2) = 4
编译器默认对齐值为各成员变量自身对齐值中的最大值,即为4。
故int a 的有效对齐值为min(4,4) = 4
char b 的有效对齐值为min(1,4) = 1
short c 的有效对齐值为min(2,4) = 2;
struct A 的有效对齐值为min(4,4) = 4
假设结构体A从地址空间0x0000开始排放。由上面的计算可知,第一个成员变量int a的有效对齐值为4,所以其存放的地址为0x0000~0x0003,且其起始地址0x0000符合0x0000%4 = 0;第二个成员变量char b的有效对齐值为1,所以其存放地址为0x00004,且其起始地址0x0004符合0x0004%1 = 0;第三个成员变量short c 的有效对齐值为2,所以其存放地址应该为0x0006~0x0007,而其起始地址0x0006也符合0x0006%2 = 0;
最后还要对整个结构体进行内存地址对齐:即要保证整个结构体的大小应该为其有效对齐值的整数倍,而该结构的有效对齐值为4,现在三个成员变量已经占据了0x0000~0x0007共8个字节的内存空间,而8%4 = 0也符合对齐要求,故整个结构体的大小应该为8.
例二
#pargma pack(8)
struct A
{
short a; //有效对齐值为min(2,8) = 2
long b; //有效对齐值为min(4,8) = 4;
};
struct B
{
char c; //有效对齐值为min(1,8) = 1
struct B s; //有效对齐值为min(4,8) = 4
short e; //有效对齐值为min(2,8) = 2
};
#pargma pack()
分别求出sizeof(struct A)和sizeof(struct B)的值。
类似例一可分析:
sizeof(struct A)= 8,sizeof(struct B)= 16