内存对齐与ANSI C中struct型数据的内存布局

   许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数 k( 通常它为 4 8) 的倍数,这就是所谓的内存对齐,而这个 k 则被称为该数据类型的对齐模数 (alignment modulus)
   当一种类型 S 的对齐模数与另一种类型 T 的对齐模数的比值是大于 1 的整数,我们就称类型 S 的对齐要求比 T ( 严格 ) ,而称 T S ( 宽松 ) 。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个 8 倍数的地址开始,一次读出或写入 8 个字节的数据,假如软件能保证 double 类型的数据都从 8 倍数地址开始,那么读或写一个 double 类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的 8 字节内存块上。某些处理器在数据不满足对齐要求的情况下可能会出错,但是 Intel IA32 架构的处理器则不管数据是否对齐都能正确工作。不过 Intel 奉劝大家,如果想提升性能,那么所有的程序数据都应该尽可能地对齐。
   Win32 平台下的微软 C 编译器 (cl.exe for 80x86) 在默认情况下采用如下的对齐规则 : 任何基本数据类型 T 的对齐模数就是 T 的大小,即 sizeof(T) 。比如对于 double 类型 (8 字节 ) ,就要求该类型数据的地址总是 8 的倍数,而 char 类型数据 (1 字节 ) 则可以从任何一个地址开始。 Linux 下的 GCC 奉行的是另外一套规则 ( 在资料中查得,并未验证,如错误请指正 ): 任何 2 字节大小 ( 包括单字节吗 ?) 的数据类型 ( 比如 short) 的对齐模数是 2 ,而其它所有超过 2 字节的数据类型 ( 比如 Long double) 都以 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 标准,该结构体的内存布局图如下:
    
         这个方案在 a b 之间多分配了 3 个填充 (padding) 字节,这样当整个 struct 对象首地址满足 4 字节的对齐要求时, b 字段也一定能满足 int 型的 4 字节对齐规定。那么 sizeof(MS1) 显然就应该是 8 ,而 b 字段相对于结构体首地址的偏移就是 4 。非常好理解,对吗?现在我们把 MS1 中的字段交换一下顺序 :
  typedef struct ms2  {     int a;     char b;  } MS2;
    或许你认为 MS2 MS1 的情况要简单,它的布局应该就是
   因为 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 的偏移为 0 b 的偏移是 4 尝试分析一个稍微复杂点的类型。   typedef struct ms3  {     char a;     short b;     double c;  } MS3;    我想你一定能得出如下正确的布局图 :  
sizeof(short) 等于 2 b 字段应从偶数地址开始,所以 a 的后面填充一个字节,而 sizeof(double) 等于 8 c 字段要从 8 倍数地址开始,前面的 a b 字段加上填充字节已经有 4 bytes ,所以 b 后面再填充 4 个字节就可以保证 c 字段的对齐要求了。 sizeof(MS3) 等于 16 b 的偏移是 2 c 的偏移是 8 。接着看看结构体中字段还是结构类型的情况 :
  typedef struct ms4  {     char a;     MS3 b;  } MS4;    MS3 中内存要求最严格的字段是 c ,那么 MS3 类型数据的对齐模数就与 double 的一致 ( 8) a 字段后面应填充 7 个字节,因此 MS4 的布局应该是 :
    显然, sizeof(MS4) 等于 24 b 的偏移等于 8
    在实际开发中,我们可以通过指定 /Zp 编译选项或者在代码中用 #pragma pack 指令来更改编译器的对齐规则。比如指定 /Zpn(VC7.1 n 可以是 1 2 4 8 16) 就是告诉编译器最大对齐模数是 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 对这个选项的描述,会发现它郑重告诫了程序员不要在 MIPS Alpha 平台上用 /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;
即便在不同的 /Zpn pack 方式下,编译生成的内存布局总是相同的;
 
再比如:
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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值