分享

内存对齐与ANSI C中struct内存布局

 scholes_goal 2012-01-08

内存对齐与ANSI C中struct内存布局

许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为48)的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)

   当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T(严格),而称TS(宽松)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。某些处理器在数据不满足对齐要求的情况下可能会出错,但是IntelIA32架构的处理器则不管数据是否对齐都能正确工作。不过Intel奉劝大家,如果想提升性能,那么所有的程序数据都应该尽可能地对齐。
   Win32平台下的微软C编译器(cl.exe for 80x86)在默认情况下采用如下的对齐规则任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。比如对于double类型(8字节),就要求该类型数据的地址总是8的倍数,而char类型数据(1字节)则可以从任何一个地址开始。Linux下的GCC奉行的是另外一套规则(在资料中查得,并未验证,如错误请指正):任何2字节大小(包括单字节吗?)的数据类型(比如short)的对齐模数是2,而其它所有超过2字节的数据类型(比如Longdouble)都以4为对齐模数。   
   现在回到我们关心的struct上来。ANSI C规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小之和。填充区就是为了使结构体字段满足内存对齐要求而额外分配给结构体的空间。那么结构体本身也有对齐要求,ANSI C标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽松,可以更严格(但此非强制要求,VC7.1就仅仅是让它们一样严格)。我们来看一个例子(以下所有试验的环境是Intel Celeron 2.4G + WIN2000 PRO + vc7.1,内存对齐编译选项是"默认",即不指定/Zp/pack选项):
  typedef struct ms1  {     char a;     int b;  } MS1;
MS1中有最强对齐要求的是b字段(int),所以根据编译器的对齐规则以及ANSIC标准,该结构体的内存布局图如下:
    
        这个方案在ab之间多分配了3个填充(padding)字节,这样当整个struct对象首地址满足4字节的对齐要求时,b字段也一定能满足int型的4字节对齐规定。那么sizeof(MS1)显然就应该是8,而b字段相对于结构体首地址的偏移就是4。非常好理解,对吗?现在我们把MS1中的字段交换一下顺序:
  typedef struct ms2  {     int a;     char b;  } MS2;
    或许你认为MS2MS1的情况要简单,它的布局应该就是
   因为MS2对象同样要满足4字节对齐规定,而此时a的地址与结构体的首地址相等,所以它一定也是4字节对齐。可是却不全面。让我们来考虑一下定义一个MS2类型的数组会出现什么问题。C标准保证,任何类型(包括自定义结构类型)的数组所占空间的大小一定等于一个单独的该类型数据的大小乘以数组元素的个数。换句话说,数组各元素之间不会有空隙。按照上面的方案,一个MS2数组array的布局就是:
   当数组首地址是4字节对齐时,array[1].a也是4字节对齐,可是array[2].a呢?array[3].a ....呢?可见这种方案在定义结构体数组时无法让数组中所有元素的字段都满足对齐规定,必须修改成如下形式:
 
    现在无论是定义一个单独的MS2变量还是MS2数组,均能保证所有元素的所有字段都满足对齐规定。那么sizeof(MS2)仍然是8,而a的偏移为0b的偏移是4尝试分析一个稍微复杂点的类型。  typedef struct ms3  {     char a;     short b;     double c;  } MS3;    我想你一定能得出如下正确的布局图:  
sizeof(short)等于2b字段应从偶数地址开始,所以a的后面填充一个字节,而sizeof(double)等于8c字段要从8倍数地址开始,前面的ab字段加上填充字节已经有4 bytes,所以b后面再填充4个字节就可以保证c字段的对齐要求了。sizeof(MS3)等于16b的偏移是2c的偏移是8。接着看看结构体中字段还是结构类型的情况:
  typedef struct ms4  {     char a;     MS3 b;  } MS4;    MS3中内存要求最严格的字段是c,那么MS3类型数据的对齐模数就与double的一致(8)a字段后面应填充7个字节,因此MS4的布局应该是:
    显然,sizeof(MS4)等于24b的偏移等于8
    在实际开发中,我们可以通过指定/Zp编译选项或者在代码中用#pragma pack指令来更改编译器的对齐规则。比如指定/Zpn(VC7.1n可以是124816)就是告诉编译器最大对齐模数是n。或者定义结构时
#pragma pack(push, n)
typedef struct ms3  {     char a;     short b;     double c;  } MS3;
#pragma pack(pop);
在这种情况下,所有小于等于n字节的基本数据类型的对齐规则与默认的一样,但是大于n个字节的数据类型的对齐模数被限制为n。如果n = 1,那么结构体的大小就是各个字段的大小之和,在Moses中定义结构体的地方随处可见,这样做可以减少结构体所占用的内存空间。
VC7.1的默认对齐选项就相当于/Zp8。仔细看看MSDN对这个选项的描述,会发现它郑重告诫了程序员不要在MIPSAlpha平台上用/Zp1/Zp2选项,也不要在16位平台上指定/Zp4/Zp8(想想为什么?)    结构体的内存布局依赖于CPU、操作系统、编译器及编译时的对齐选项,而你的程序可能需要运行在多种平台上,你的源代码可能要被不同的人用不同的编译器编译(试想你为别人提供一个开放源码的库),那么除非绝对必需,否则你的程序永远也不要依赖这些诡异的内存布局。顺便说一下,如果一个程序中的两个模块是用不同的对齐选项分别编译的,那么它很可能会产生一些非常微妙的错误。如果你的程序确实有很难理解的行为,不防仔细检查一下各个模块的编译选项。 
--------------------------------------------------------------------------------- 
问题:下面的试验,请问如何解释?
#pragma pack(push, 2)
struct s
{
     char a;
};
#pragma pack (pop)
 
void TestPack()
{
     s c[2];
     assert(sizeof(s)==1);
     assert(sizeof(c)==2);
}
 
int _tmain(int argc, _TCHAR* argv[])
{
     TestPack();
     return 0;
}
解答:
每一种基本的数据类型都有该数据类型的对齐模数(alignment modulus)。Win32平台下的微软C编译器(cl.exe for 80x86)在默认
情况下: 任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。
一组可能的对齐模数数据如下:
 
数据类型     模数
------------------
char          1
shor          2
int             4
double       8
 
ANSI C规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小之和。
注:填充区就是为了使结构体字段满足内存对齐要求而额外分配给结构体的空间。
 
产生填充区的条件:
当结构体中的成员一种类型S的对齐模数与另一种类型T的对齐模数不一致的时候,才可能产生填充区。
我们通过编译选项设置/zpn 或#pragma pack(push, n) 来设置内存对齐模数时,当结构体中的某中基本数据类型的对齐模数大于n时才会影响填充区的大小,否则将会按照基本数据类型的对齐模数进行对齐。
例子:
 
当n = 1时:
#pragma pack(push,1)
typedef struct ms3{char a; short b; double c; } MS3;
#pragma pack(pop)
 
这时n=1,此结构中基本数据类型short的对齐模数为2,double为8,大于n 所以将会影响这两个变量存储时地址的偏移量,必须是n的整数倍,而char的对齐模数是1,小于等于n,将会按照其自身的对齐模数1进行对齐.
因为n=1,所以这三个变量在内存中是连续的而不存在填充区.内存布局如下:
     ___________________________
     | a |  b  |       c       |
     +-------------------------+
Bytes: 1    2          8
 
sizeof(MS3) = 11 
当n = 2时:
#pragma pack(push,2)
typedef struct ms3{char a; short b; double c; } MS3;
#pragma pack(pop)
 
这时n=2,此结构中基本数据类型double的对齐模数为8,大于n, 所以将会影响这个变量存储时地址的偏移量,必须是n的整数倍,而char 和 short 的对齐模数小于等于n, 将会按照其自身的对齐模数分别是1,2进行对齐.内存布局如下:
     ____________________________
     | a |\|  b  |       c      |
     +---------------------------+
Bytes: 1  1   2          8
 
此时变量c的存储地址偏移是4,是n=2的整数倍,当然偏移为6,8等等时也满足这个条件,但编译器不至于愚蠢到这种地步白白浪费空间,呵。 
sizeof(MS3) = 12 
当n = 4时:与n=2时结果是一样的. 
当n = 8时:
#pragma pack(push,8)
typedef struct ms3{char a; short b; double c; } MS3;
#pragma pack(pop)
 
这时n=8,此结构中char ,short ,double的对齐模数为都,小于等于n,将会按照其自身的对齐模数分别是1,2,8进行对齐.即:short变量存储时地址的偏移量是2的倍数;double变量存储时地址的偏移量是8的倍数.
内存布局如下:
     _______________________________________
     | a |\|  b  |\padding\|       c       |
     +-------------------------------------+
Bytes: 1  1   2       4            8
 
此时变量a的存储地址偏移是0,当然也是char型对齐模数1的整数倍了 
变量b的存储地址偏移要想是short型对齐模数2的整数倍,因为前面a占了1 个byte ,所以至少在a 与b之间再加上1 个byte的padding.才能满足条件。 
变量c的存储地址骗移要想是double型对齐模数8的整数倍,因为前面a 和b 加 1个byte 的padding,共4 bytes所以最少还需要4 bytes的padding才能满足条件。 
sizeof(MS3) = 16 
当n = 16时:与n=8时结果是一样的. 
====================================================
根据上面的分析,如下定义的结构
#pragma pack(push, 2)
struct s
{
    char a;
};
#pragma pack (pop)
 
因为char 的对齐模数是1,小于n=2,所以将按照自身的对齐模数对齐。根本就不会存在填充区,所以sizeof(s) = 1.对于s c[2];   sizeof(c)==2 也是必然的。
再看下面的结构:
#pragma pack(push, n)//n=(1,2,4,8,16)
struct s
{
    double a;
    double b;
    double c;
};
#pragma pack (pop)
 
对于这样的结构无论pack设置的对齐模数为几都不会影响其大小,即无padding. 
double 类型的对齐模数为8
当n<8时,虽然满足前面讲的规则:当结构体中的某中基本数据类型的对齐模数大于n时才会影响填充区的大小。但这个时候无论n等于几(1,2,4),double 变量存储时地址的偏移量都是n的整数倍,所以根本不需要填充区。当n>=8时,自然就按照double 的对齐模数进行对齐了.因为类型都一样所以变量之间在内存中不会存在填充区.
---------------------------------------------------------------------------------------------------------------------
补充一点:
如果在定义结构的时候,仔细调整顺序,适当明确填充方式,则最终内存结果可以与编译选项/Zpn  pack无关。
举个例子:
typedef struct ms1{ char a; char b; int c; short d; } MS1;
在不同的 /Zpn下,sizeof(MS1)的长度可能不同,也就是内存布局不同。
如果改成
typedef struct ms2{ char a; char b; short d; int c; } MS2;
即便在不同的/Zpnpack方式下,编译生成的内存布局总是相同的;
 
再比如:
typedef struct ms3{ char a; char b; int c; } MS3;
可以改写成:
typedef struct ms4{ char a; char b; short padding; int c; } MS4;   显式地写上 padding
 
(通过源代码本身来消除隐患,要比依赖编译选项更加可靠,并易于移植,优质的代码应该做到这一点)
(减少隐含padding的另外一个好处是少占内存,当结构的实例数量很大时,内存的节省量是非常可观的)
(以上的变量/结构命名没有遵循命名规范,只为说明用,不可模仿)

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多