一、什么是字节对齐,为什么要进行字节对齐
在计算机存储空间上,各种类型的数据往往并不是顺序地一个接一个地排列,而是按照一定的规则排列,这就是对齐。
各个硬件平台对存储空间的处理有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取否则会发生错误,而有些平台如果不按照其要求对数据进行存取则可能会降低存取效率。这就是我们需要进行字节对齐的原因。
实际上,字节对齐往往也是一种空间换时间的策略。例如,32位计算机在存取内存时是以4字节为单位的,如果一个int型数据(4字节)的首地址对4的余数是2,那么访问这个数据需要读两次内存,如果在这个数据之前填充2字节的未用内存,那么读取这个数据只需读取一次内存。字节对齐就是在数据结构和代码中填充空白区域,把数据和指令的首地址移动到CPU本身指令长度(以字节为单位)整数倍的地址处以提升程序的性能。
二、编译器是按照什么样的原则进行字节对齐的
下面我们以VC6.0为例进行说明。
编译器会对一些变量的起始地址做对齐处理。在默认情况下,VC6.0规定数据结构中各数据成员存放的起始地址相对于结构的起始地址的偏移量必须为该数据的对奇值(数据类型所占字节数跟设定对奇值两者中的小值为数据的对奇值,设定对奇值在后文叙述)的整数倍(即偏移量%对奇值=0)。下面列出常用类型的对齐方式(VC6.0,32位系统,编译器的设定对齐值为8)。
类型 |
对齐方式(数据存放的起始地址相对于结构的起始地址的偏移量) |
char |
1字节对齐,偏移量必须为1的倍数 |
short |
2字节对齐,偏移量必须为2的倍数 |
int |
4字节对齐,偏移量必须为4的倍数 |
float |
4字节对齐,偏移量必须为4的倍数 |
double |
8字节对齐,偏移量必须为8的倍数 |
数据结构中各数据成员在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节编译器会自动填充。第一个数据成员的起始地址就是数据结构的起始地址。除了数据成员需要对齐存放外,数据结构本身也要根据结构的字节边界数(结构的字节边界数为该结构中占用最大空间的类型所占用的字节数跟设定对奇值两者中的小值)圆整(即数据结构大小需要是结构的字节边界数的整数倍,数据结构的大小%字节边界数=0)。所以在为最后一个数据成员申请空间后,编译器还会根据需要自动填充空缺的字节。
现在我们用上述对齐原则来对下面的结构进行分析。
struct TestStructA
{
double a;
char b;
int c;
};
对结构TestStructA采用sizeof会出现什么结果呢?在阅读上述文字之前也许你会这样认为:
sizeof(TestStructA)=sizeof(double)+sizeof(char)+sizeof(int)=13
但是当在VC6.0中测试上面结构的大小时,我们发现sizeof(TestStructA)为16。实际上,在为上面的结构分配空间的时候,编译器根据数据成员出现的顺序和对齐方式,先为第一个成员a分配空间,其起始地址跟结构的起始地址相同(偏移量为0,0%sizeof(double)=0),该成员变量占用sizeof(double)=8个字节;接下来为第二个成员b分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为8,8%sizeof(char)=0,所以编译器从该地址处存放b满足对齐方式,该数据成员占用sizeof(char)=1个字节;接下来为第三个成员c分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,9%sizeof(int)!=0,为了满足对齐方式,编译器自动填充3个字节(这三个字节没有放什么东西),这时下一个可以分配的地址对于结构的起始地址的偏移量为12,12%sizeof(int)=0,所以c从偏移量为12的地方开始存放,该数据成员占用sizeof(int)=4个字节。这时整个结构的数据成员都已经分配了空间,总的占用的空间大小为:8+1+3+4=16,16%字节边界数(即设定对奇值跟sizeof(double)两者中的小值,这里两值均为8)=0,没有空缺的字节需要填充,所以整个结构的大小为:sizeof(TestStructA)=8+1+3+4=16,其中有3个字节是编译器自动填充的,没有放任何有意义的东西。
再看下面的例子,交换一下TestStructA的数据成员的位置,使它变成下面的TestStructB:
struct TestStructB
{
char b;
double a;
int c;
};
这个结构占用的空间为多大呢?在VC6.0环境下,可以得到sizeof(TestStructB)=24。结合上面提到的分配空间的一些原则,现简单分析说明如下。
先为b分配空间,其起始地址跟结构的起始地址相同,偏移量为0,0%sizeof(char)=0,满足对齐方式,b占用1个字节;下一个可用地址的偏移量为1,1%sizeof(double)!=0,需要补足7个字节才能使偏移量变为8(满足对齐方式),因此编译器自动填充7个字节,a从偏移量为8的地址处开始存放,它占用8个字节;下一个可用的地址的偏移量为16,16%sizeof(int)=0,满足int的对齐方式,c从偏移量为16的地址开始存放,它占用4个字节。现在所有成员变量都分配了空间,空间总的大小为1+7+8+4=20,20%字节边界数!=0,所以需要填充4个字节,以满足结构的大小为字节边界数的整数倍。所以该结构总的大小为:sizeof(TestStructB)为1+7+8+4+4=24。其中共有7+4=11个字节是编译器自动填充的,没有放任何有意义的东西。
三、如何修改设定对齐值
我们有两种方法修改对齐方式。一是在VC集成环境中修改默认对奇值。修改方法是:在[Project]|[Settings]菜单的C/C++选项卡Category之Code Generation选项Struct Member Alignment中修改(环境默认设定为8字节)。这也是我们上文提到的设定对奇值。我们把用这种方法设定的对奇值称为默认对奇值,按默认对奇值对齐的方式称为默认对齐方式。另外一种方法是在编码时动态地修改。VC6.0支持的代码修改方法是使用#pragma pack(n)(注意:是pragma而不是progma)来设定数据以n字节对齐。这时,数据对奇值为n、该数据的类型所占用的字节数及默认对奇值三者当中的最小值;结构的字节边界数为n、成员数据中占用空间最大的数据所占用的字节数以及默认对奇值三者当中的最小值。
下面我们对第二种方法举例说明。
#pragma pack(4) //设定为4字节对齐
struct TestStructB
{
char b;
double a;
int c;
};
#pragma pack() //取消指定对齐,恢复默认对齐
以上结构的大小为16。首先为b分配空间,其偏移量为0,0%1=0,满足对齐方式,b占用1个字节。接着开始为a分配空间,这时其偏移量为1,因为sizeof(double)大于n,所以a的对奇值为n=4,需要补足3个字节才能满足偏移量%n =0,a占用8个字节。接着为c分配空间,这时其偏移量为12,满足偏移量%4=0,c占用4个字节。这时已经为所有数据成员分配了空间,共分配了16个字节,是结构字节边界数n的整数倍。如果把上面的#pragma pack(4)改为#pragma pack(16),那么我们可以得到结构的大小为24。