[c&cpp][memory] 内存对齐分配策略(含位域模式)

1:内存对齐定义:
    现在使用的计算机中内存空间都是按照字节划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际上计算机系统对于基本数据类型在内存中的存放位置都有限制,要求这些数据存储首地址是某个数K的倍数,这样各种基本数据类型在内存冲就是按照一定的规则排列的,而不是一个紧挨着一个排放,这就是内存对齐。

对齐模数:
    内存对齐中指定的对齐数值K成为对齐模数(Alignment Modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。

2:内存对齐的好处:
    内存对齐作为一种强制的要求,第一简化了处理器与内存之间传输系统的设计,第二可以提升读取数据的速度。各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
    Intel的IA32架构的处理器则不管数据是否对齐都能正确工作。但是如果想提升性能,应该注意内存对齐方式。
ANSI C标准中并没有规定相邻声明的变量在内存中一定要相邻。为了程序的高效性,内存对齐问题由编译器自行灵活处理,这样导致相邻的变量之间可能会有一些填充字节。对于基本数据类型(int char等),他们占用的内存空间在一个确定硬件系统下有确定的值。ANSI C规定一种结构类型的大小是它所有字段的大小以及字段之间或字段尾部的填充区大小之和。

3:内存对齐策略:
微软C编译器(cl.exe for 80×86)的对齐策略:
第一: 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。
第二: 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
第三: 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。
备注:结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

填充字节就是为了使结构体字段满足内存对齐要求而额外分配给结构体的空间。对于结构体本身也存在着对齐要求,ANSI C标准规定结构体类型的对齐要求不能比它所有字段中要求最严格的那个宽松,但是可以更严格(但此非强制要求,VC7.1就仅仅是让它们一样严格)。C标准保证,任何类型(包括自定义结构类型)的数组所占空间的大小一定等于一个单独的该类型数据的大小乘以数组元素的个数。换句话说,数组各元素之间不会有空隙。

总结规则如下:
0: 结构体变量的首地址能够被其最宽基本类型成员的大小所整除
1: VC6和VC71默认的内存对齐方式是 #pragam pack(8)
2: 结构体中每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数中较小的一个对齐.
3:   结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍.
4:   结构体本身也存在着对齐要求规则,不能比它所有字段中要求最严格的那个宽松.
5: 结构体的总大小为结构体最宽基本类型成员大小的整数倍,且应尽量节省内存。
6: 在GCC中,对齐模数的准则是:对齐模数最大只能是 4,也就是说,即使结构体中有double类型,对齐模数还是4,所以对齐模数只能是1,2,4。
      而且在上述的规则中,第3条里,offset必须是成员大小的整数倍:
       (1): 如果这个成员大小小于等于4则按照上述准则是可行的,
       (2): 如果成员的大小大于4,则结构体每个成员相对于结构体首地址的偏移量只能按照是4的整数倍来进行判断是否添加填充。

例子1(VC8):
typedef struct ms1 {
  char a;
  int b;
} MS1;

typedef struct ms2 {
  int a;
  char b;
} MS2;
MS1中有最强对齐要求的是b字段(int类型),字段a相对于首地址偏移量为0(1的倍数),直接存放,此时如果直接存放字段b,则字段b相对于结构体变量首地址的偏移量为1(不是4的倍数),填充3字节,b由偏移地址为4开始存放。也就是遵循了第2条与第3条规则,而对于结构体变量本身,根据规则4,对齐参数至少应该为4。根据规则5,sizeof(MS1) = 8; 同样MS2分析得到的结果也是如此。

例子2VC8:
typedef struct ms3 {
  char a;
  short b;
  double c;
} MS3;

typedef struct ms4 {
  char a;
  MS3 b;
} MS4;
MS3中内存要求最严格的字段是c(8字节),MS3的对齐参数也是8字节; 那么MS4类型数据的对齐模数就与MS3中的double一致(为8),a字段后面应填充7个字节.sizeof(MS3) = 16; sizeof(MS4) = 24;
注意规则5中是说,结构体的总大小为结构体最宽基本类型成员大小的整数倍。注意是基本类型,这里的MS3不是基本类型。
对齐模数的选择只能是根据基本数据类型,所以对于结构体中嵌套结构体,只能考虑其拆分的基本数据类型。

例子3(GCC):
struct T {
  char ch;
  double d ;
};
在GCC下,sizeof(T)应该等于12个字节。VC8下为16字节。
ch为1字节,没有问题的,之后d的大小大于4,对于d的对齐模数只能是4,相对于结构体变量的首地址偏移量也只能是4,而不能使8的整数倍,由偏移量4开始存放,结构体共占12字节。
这里并没有执行第5条规则。

位域情况
C99规定int、unsigned   int和bool可以作为位域类型。但编译器几乎都对此作了扩展,允许其它类型类型的存在。
如果结构体中含有位域(bit-field),总结规则如下
1) 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
2) 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
3) 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式(不同位域字段存放在不同的位域类型字节中),Dev-C++和GCC都采取压缩方式;
4)如果位域字段之间穿插着非位域字段,则不进行压缩;
5) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,且应尽量节省内存。
备注:当两字段类型不一样的时候,对于不压缩方式,例如:

struct N {
  char c:2;
  int i:4;
};
依然要满足不含位域结构体内存对齐准则第3条,i成员相对于结构体首地址的偏移应该是4的整数倍,所以c成员后要填充3个字节,然后再开辟4个字节的空间作为int型,其中4位用来存放i,所以上面结构体在VC中所占空间为8个字节;
而对于采用压缩方式的编译器来说,遵循不含位域结构体内存对齐准则第3条,不同的是,如果填充的3个字节能容纳后面成员的位,则压缩到填充字节中,不能容纳,则要单独开辟空间,所以上面结构体N在GCC或者Dev- C++中所占空间应该是4个字节。

例子4:
typedef struct {
  char c:2;
  double i;
  int c2:4;
}N3;
按照含位域规则4,在GCC下占据的空间为16字节,在VC下占据的空间是24个字节。

结论:
--------
定义结构体的时候,成员最好能从大到小来定义,那样能相对的省空间。例如如下定义:
struct A {
  double d;
  int i;
  char c;
};
那么,无论是windows下的vc系列编译器,还是linux下的gcc,都是16字节。

例子5:
typedef union student{
  char name[10];
  long sno;
  char sex;
  float score [4];
} STU;
STU aa[5];
cout<<sizeof(aa)<<endl;
union是可变的以其成员中最大的成员作为该union的大小16*5=5=80

例子6:
typedef struct student{
  char name[10];
  long sno;
  char sex;
  float score [4];
} STU;
STU aa[5];
cout<<sizeof(aa)<<endl;
STU占空间为:10字节(char)+空2字节+4字节(long)+1字节(char)+空3字节+16字节(float)=36字节,36*5=180字节

例子7(VC8.0):
typedef struct bitstruct {
  int b1:5;
  int b2:2;
  int b3:3;
}bitstruct;

int _tmain(int argc, _TCHAR* argv[]) {
  bitstruct b;
  memcpy(&b,"EM",sizeof(b));
  cout<<sizeof(b)<<endl;
  cout<<b.b1<<endl<<b.b2<<endl<<b.b3;
  return 0;
}
对于bitstruct是含有位域的结构体,sizeof(int)为4字节,按照规则1、2,首先b1占起始的5个字节, 根据含位域规则1, b2紧跟存放,b3也是紧跟存放的。
根据规则5,得到sizeof(bitstruct) = 4。
现在主流的CPU,intel系列的是采用的little endian的格式存放数据,motorola系列的CPU采用的是big endian.
以主流的little endian分析:
在进行内存分配的时候,首先分配bitstruct的第一个成员类型int(4字节),这四个字节的存放按照低字节存储在低地址中的原则。
int共4个字节:
第4个字节 - 第3个字节 - 第2个字节 - 第1个字节,

在内存中的存放方式如下所示。
而后为b1分配5位,这里优先分配的应该是低5位,也就是第一个字节的低5位。
继而分配b2的2个字节,也就是第1个字节中紧接着的2位。
最后分配b3的3位,按照规则1、2,b3还是紧接着存放的,b3的最低位是第一个字节的最高位,高两位为第2个字节的低两位。
内存分配图如下所示:

QQ截图未命名
字符E二进制为0100 0101,字符M的二进制为0100 1101,在内存中存放如下所示:

QQ截图未命名2
memcpy为按位拷贝的,所以两片内存区可以直接对应上,得到
b1的二进制形式为:00101 ,高位为0,正数,5
b2的二进制形式为:10 ,高位为1,负数,取反加1,添加符号,-2
b3的二进制形式为:b3的最低一位是0,高位为01,拼接后为010,正数,2

内存分配情况感觉蛮奇怪的,按如下修改例7,b1应该为5,b2为-2,b3为-6,VC8.0下验证正确。
typedef struct bitstruct {
  int b1:5;
  int b2:2;
  int b3:4;
}bitstruct;

int _tmain(int argc, _TCHAR* argv[]) {
  bitstruct b;
  memcpy(&b,"EM",sizeof(b));
  cout<<sizeof(b)<<endl;
  cout<<b.b1<<endl<<b.b2<<endl<<b.b3;
  return 0;
}

4: 定义数组时的内存布局及内存字节对齐

int b=10;

int a[3]={1,2,3};

int c=11;

image

int b=0x01020304;

char ch='a';

对于一个数0x01020304; 对于一个数0x1122

使用Little Endian方式时,低地址存放低字节,由低地址向高地址存放为:4->3->2->1
而使用Big Endian方式时, 低地址存放高字节,由低地址向高地址存放为:1->2->3->4

而在Little Endian模式中,b的地址所指的就是 : 低地址(存放的是最低的字节)

image

   1:  void __cdecl func_cdcel(int i, char *szTest) {
   2:      cout << "szTest在栈中的地址:" << &szTest << endl;
   3:      cout << "szTest本身的值(指向的地址):" << (void*)szTest << endl<<endl;
   4:      
   5:      cout << "i在堆栈中地址:" << &i << endl;
   6:      cout << "i的地址:" << &i << endl;
   7:   
   8:      int k,k2;
   9:      cout << "局部变量k的地址:" << &k << endl;
  10:      cout << "局部变量k2的地址:" << &k2 << endl;
  11:      cout << "-------------------------------------------------------" << endl;
  12:  }
  13:   
  14:  void __stdcall func_stdcall(int i, char *szTest){
  15:      cout << "szTest在栈中的地址:" << &szTest << endl;
  16:      cout << "szTest本身的值(指向的地址):" << (void*)szTest << endl<<endl;
  17:   
  18:      cout << "i在堆栈中地址:" << &i << endl;
  19:      cout << "i的地址:" << &i << endl;
  20:   
  21:      int k,k2;
  22:      cout << "局部变量k的地址:" << &k << endl;
  23:      cout << "局部变量k2的地址:" << &k2 << endl;
  24:      cout << "-------------------------------------------------------" << endl;
  25:  }
  26:   
  27:  int main(){
  28:      int a[4];
  29:      cout <<"a[0]地址:"<< &a[0] << endl;
  30:      cout <<"a[1]地址:"<< &a[1] << endl;
  31:      cout <<"a[2]地址:"<< &a[2] << endl;
  32:      cout <<"a[3]地址:"<< &a[3] << endl;
  33:   
  34:      int i = 0x22;
  35:      int j = 8;
  36:      char szTest[4] = {'a','b', 'c', 'd'};
  37:      cout <<"i的地址:"<<&i << endl;
  38:      cout <<"szTest的地址:"<<(void*)szTest << endl;
  39:      func_cdcel(i, szTest);
  40:      func_stdcall(i, szTest);
  41:  }

输出为:

a[0]地址:0012FF54
a[1]地址:0012FF58
a[2]地址:0012FF5C
a[3]地址:0012FF60                  <— 可见存储方式如上图所示,a[3]在高地址,先入栈,而数组地址a为a[0]的地址(低地址)
i的地址:0012FF48                    <— 这里进行了内存对齐,i的起始地址必定是i所占内存大小的倍数
szTest的地址:0012FF30  

szTest在栈中的地址:0012FE5C  
szTest本身的值(指向的地址):0012FF30

i在堆栈中地址:0012FE58           <— i在堆栈中的地址低于szTest,也就是说szTest是先入栈的
i的地址:0012FE58
局部变量k的地址:0012FE48
局部变量k2的地址:0012FE3C
-------------------------------------------------------
szTest在栈中的地址:0012FE5C
szTest本身的值(指向的地址):0012FF30

i在堆栈中地址:0012FE58
i的地址:0012FE58
局部变量k的地址:0012FE48
局部变量k2的地址:0012FE3C
-------------------------------------------------------

posted @ 2011-03-24 14:54  zsounder  阅读(3454)  评论(2编辑  收藏  举报