C++内存对齐机制

来源:互联网 发布:unity3d室内灯光开关 编辑:程序博客网 时间:2024/05/20 09:09
1.           什么是内存对齐

内存对齐的问题主要存在于理解struct等复合结构在内存中的存储结构。

在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,但不一定是相邻存储,第一个成员的地址和整个结构的地址相同。

由此引出内存对齐的概念:许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个对其参数k(通常它为4或8)的倍数,这就是所谓的内存对齐。

引入内存对齐的原因一方面在于硬件取指的方便,例如在32位总线系统上,如果一个int变量(4字节)放在一个4的倍数开始的内存地址中,则CPU可以一次将其数值读出,否则的话就要分两次才能读出。另一个重要的原因在于移植性的要求,也就是说不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。引入内存对齐的目的主要是为了可移植性以及最大限度提升硬件性能。

2.           内存对齐规则

内存对齐与平台,编译器都有一定的关系,比较流行的关于内存对齐的规则是这样来描述的:

1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。

    2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

    3、结合1、2两项推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。

但是,这种流行的说法并不完善。比如在Linux环境中,gcc对于预编译指令#pragma pack (N)有一定的限制,不允许N超过gcc的默认值;此外,可以使用__attribute((aligned(n)))来强制定义某个数据成员或者整个结构的对齐规则,但是这种定义同样受到系统默认值以及#pragma pack (N)预编译指令的限制。另外,如果结构成员仍是结构,则上述的第二条就不正确了。最后,上述的说法没有对结构的内存对齐规则以及由此产生的对占用存储空间大小的影响作出说明。

参照流行的关于内存对齐规则的描述,以及在Linux系统中实际的程序测试结果,总结了如下四条内存对齐规则。在规则描述中使用了内存对齐参数的概念(有的文献中称其为对齐模数),其意为,如果某结构成员的内存对齐参数为n,则该结构成员一定在该参数整倍数的地址单元处开始。

1.                        结构体成员在内存中的起始位置为该成员内存对齐参数的整数倍(前一字段的末尾和该字段之间可能需要补空位);

2.                        结构体成员的变量对齐参数可由__attribute((aligned(n)))强制定义,强制定义失败的情况下,为全局内存对齐参数(由#define pack (N)指定或使用默认值)和该成员所占空间大小之间较小的一个。如果该成员也是结构体,请参照3;

3.                        整个结构的内存对齐参数同样可由__attribute((aligned(n)))强制定义,强制定义失败的情况下,取该结构中所有成员内存对齐参数中最大的;

4.                        结构占有存储空间的大小必须为该结构内存对齐参数的整数倍(可能会在最后补相应数目的空位);

5.                        以上规则是结合流行的关于内存对齐的规则描述基于Linux系统和gcc编译环境总结的,没有在Windows环境进行过验证,但是两者之间不会有大的差异。

3.           设置内存对齐参数规则

设置内存对齐参数包含两方面的意思:通过预编译指令#pragma pack (N)进行全局内存对齐参数设置;通过__attribute((aligned(n)))强制定义某个结构成员的内存对齐参数。

根据网上查找材料分析,并结合在Linux环境下程序实际测试的结果,总结设置内存对齐参数的规则如下:

1.                        可通过#pragma pack (N)设置全局内存对齐参数。但在Linux,gcc编译环境下,N的值超过默认值4之后将不能生效,gcc将忽略该预编译指令(Windows中的限制尚不了解)。也就是说,在Linux,gcc编译环境下只能将这个数值降低,不能提高。另外,N必须是2的非负整数次幂。

2.                        可通过__attribute((aligned(n)))强制定义结构成员或者整个结构的内存对齐参数,n也必须是2的非负整数次幂。但是,这里有两个限制:一是不能通过这种用法降低某个结构成员和整个结构的内存对齐参数;二是在设置全局内存对齐参数的预编译指令生效时,不能通过这种用法使某个结构成员或者整个结构的内存对齐参数超过全局内存对齐参数。

3.                        上述两项规则没有在Windows平台进行过验证。

这里对第二条规则做一点说明。首先是在任何情况下,试图通过使用__attribute((aligned(n)))降低某个结构成员或者整个结构的内存对齐参数的做法都是无效的,降低结构成员或整个结构内存对齐参数的唯一办法是使用预编译指令#pragma pack (N)。另外,__attribute((aligned(n)))可以提高结构成员或者结构的内存对齐参数,但是最大不能超过通过预编译指令定义的全局内存对齐参数;但是如果没有使用该预编译指令或者该预编译指令无效(如Linux,gcc编译环境下,N超过4),则没有这个限制。这就意味着,此时尽管默认全局内存对齐参数为4,但是可通过__attribute((aligned(n)))强制定义某个结构成员(比如某个double变量)的内存对其参数为8。

4.           示例

Linux,gcc编译环境下,有如下定义的结构体:

#pragma pack(2)

struct Pool

{

    char c;

    char s;

    double i __attribute((aligned (8)));

    char d,e,f;

};

struct s2

{

    char c;

    struct Pool s1 __attribute((aligned(1)));

    int d;  

};

由于通过预编译指令设置了全局内存对齐参数为2,所以struct Pool中double i设置内存对齐参数为8无效,整个Pool结构内存对齐参数为2,结构所占存储空间大小为14;struct s2中成员s1设置内存对齐参数为1也无效,整个s2结构所占存储空间为20。

还是上面的数据结构,如果将#pragma pack (2)屏蔽,那么struct Pool中double i设置内存对齐参数为8生效,整个Pool结构内存对齐参数为8,结构所占存储空间大小为24;struct s2中成员s1设置内存对齐参数为1仍然无效,整个s2结构所占存储空间为40。

5.           参考文章

主要包括但不仅限于:

1.        

http://blog.csdn.net/wenddy112/articles/300583.aspx

2.        

http://blog.csdn.net/sadgod/archive/2007/08/09/1733926.aspx

3.        

http://topic.csdn.net/u/20080904/10/ffc6b178-8261-4d8d-b74c-aed1e497c875.html

4.        

http://bbs.chinaunix.net/viewthread.php?tid=636323

5.        

http://blog.chinaunix.net/u1/39518/showart_504959.html




=============================================================================================================================


一.内存对齐的初步讲解

内存对齐可以用一句话来概括:

“数据项只能存储在地址是数据项大小的整数倍的内存位置上”

例如int类型占用4个字节,地址只能在0,4,8等位置上。

例1:

#include <stdio.h>
struct xx{
        char b;
        int a;
        int c;
        char d;
};

int main()
{
        struct xx bb;
        printf("&a = %p/n", &bb.a);
        printf("&b = %p/n", &bb.b);
        printf("&c = %p/n", &bb.c);
        printf("&d = %p/n", &bb.d);
        printf("sizeof(xx) = %d/n", sizeof(struct xx));

        return 0;
}

执行结果如下:

&a = ffbff5ec
&b = ffbff5e8
&c = ffbff5f0
&d = ffbff5f4
sizeof(xx) = 16

会发现b与a之间空出了3个字节,也就是说在b之后的0xffbff5e9,0xffbff5ea,0xffbff5eb空了出来,a直接存储在了0xffbff5ec, 因为a的大小是4,只能存储在4个整数倍的位置上。打印xx的大小会发现,是16,有些人可能要问,b之后空出了3个字节,那也应该是13啊?其余的3个 呢?这个往后阅读本文会理解的更深入一点,这里简单说一下就是d后边的3个字节,也会浪费掉,也就是说,这3个字节也被这个结构体占用了.

可以简单的修改结构体的结构,来降低内存的使用,例如可以将结构体定义为:
struct xx{
        char b; 
        char d;
        int a;          
        int c;                  
};

这样打印这个结构体的大小就是12,省了很多空间,可以看出,在定义结构体的时候,一定要考虑要内存对齐的影响,这样能使我们的程序占用更小的内存。

二.操作系统的默认对齐系数

每 个操作系统都有自己的默认内存对齐系数,如果是新版本的操作系统,默认对齐系数一般都是8,因为操作系统定义的最大类型存储单元就是8个字节,例如 long long(为什么一定要这样,在第三节会讲解),不存在超过8个字节的类型(例如int是4,char是1,long在32位编译时是4,64位编译时是 8)。当操作系统的默认对齐系数与第一节所讲的内存对齐的理论产生冲突时,以操作系统的对齐系数为基准。

例如:

假设操作系统的默认对齐系数是4,那么对与long long这个类型的变量就不满足第一节所说的,也就是说long long这种结构,可以存储在被4整除的位置上,也可以存储在被8整除的位置上。

可以通过#pragma pack()语句修改操作系统的默认对齐系数,编写程序的时候不建议修改默认对齐系数,在第三节会讲解原因

例2:

#include <stdio.h>
#pragma pack(4)
struct xx{
        char b;
        long long a;
        int c;
        char d;
};
#pragma pack()

int main()
{
        struct xx bb;
        printf("&a = %p/n", &bb.a);
        printf("&b = %p/n", &bb.b);
        printf("&c = %p/n", &bb.c);
        printf("&d = %p/n", &bb.d);
        printf("sizeof(xx) = %d/n", sizeof(struct xx));

        return 0;
}
打印结果为:

&a = ffbff5e4
&b = ffbff5e0
&c = ffbff5ec
&d = ffbff5f0
sizeof(xx) = 20

发现占用8个字节的a,存储在了不能被8整除的位置上,存储在了被4整除的位置上,采取了操作系统的默认对齐系数。

三.内存对齐产生的原因


内存对齐是操作系统为了快速访问内存而采取的一种策略,简单来说,就是为了放置变量的二次访问。操作系统在访问内存 时,每次读取一定的长度(这个长度就是操作系统的默认对齐系数,或者是默认对齐系数的整数倍)。如果没有内存对齐时,为了读取一个变量是,会产生总线的二 次访问。

例如假设没有内存对齐,结构体xx的变量位置会出现如下情况:

struct xx{
        char b;         //0xffbff5e8
        int a;            //0xffbff5e9       
        int c;             //0xffbff5ed      
        char d;         //0xffbff5f1
};

操作系统先读取0xffbff5e8-0xffbff5ef的内存,然后在读取0xffbff5f0-0xffbff5f8的内存,为了获得值c,就需要将两组内存合并,进行整合,这样严重降低了内存的访问效率。(这就涉及到了老生常谈的问题,空间和效率哪个更重要?这里不做讨论)。

这样大家就能理解为什么结构体的第一个变量,不管类型如何,都是能被8整除的吧(因为访问内存是从8的整数倍开始的,为了增加读取的效率)!

 

 

 

内存对齐的问题主要存在于理解struct等复合结构在内存中的分布。

首先要明白内存对齐的概念。
许多实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。

这个k在不同的cpu平台下,不同的编译器下表现也有所不同。比如32位字长的计算机与16位字长的计算机。这个离我们有些远了。我们的开发主要涉及两大平台,windows和linux(unix),涉及的编译器也主要是microsoft编译器(如cl),和gcc。

内存对齐的目的是使各个基本数据类型的首地址为对应k的倍数,这是理解内存对齐方式的终极法宝。另外还要区分编译器的分别。明白了这两点基本上就能搞定所有内存对齐方面的问题。

不同编译器中的k:
1、对于microsoft的编译器,每种基本类型的大小即为这个k。大体上char类型为8,int为32,long为32,double为64。
2、对于linux下的gcc编译器,规定大小小于等于2的,k值为其大小,大于等于4的为4。

明白了以上的说明对struct等复合结构的内存分布就应该很清楚了。

下面看一下最简单的一个类型:struct中成员都为基本数据类型,例如:
struct test1
{
char a;
short b;
int c;
long d;
double e;
};

在windows平台,microsoft编译器下:

假设从0地址开始,首先a的k值为1,它的首地址可以使任意位置,所以a占用第一个字节,即地址0;然后b的k值为2,他的首地址必须是2的倍数,不能是1,所以地址1那个字节被填充,b首地址为地址2,占用地址2,3;然后到c,c的k值为4,他的首地址为4的倍数,所以首地址为4,占用地址4,5,6,7;再然后到d,d的k值也为4,所以他的首地址为8,占用地址8,9,10,11。最后到e,他的k值为8,首地址为8的倍数,所以地址12,13,14,15被填充,他的首地址应为16,占用地址16-23。显然其大小为24。

这就是 test1在内存中的分布情况。我们建立一个test1类型的变量,a、b、c、d、e分别赋值2、4、8、16、32。然后从低地址依次打印出内存中每个字节对应的16进制数为:
2 0 4 0 8 0 0 0 10 0 0 0 0 0 0 0 0 0 0 0 0 0 40 40

验证:
显然推断是正确的。

在linux平台,gcc编译器下:
假设从0地址开始,首先a的k值为1,它的首地址可以使任意位置,所以a占用第一个字节,即地址0;然后b的k值为2,他的首地址必须是2的倍数,不能是1,所以地址1那个字节被填充,b首地址为地址2,占用地址2,3;然后到c,c的k值为4,他的首地址为4的倍数,所以首地址为4,占用地址4,5,6,7;再然后到d,d的k值也为4,所以他的首地址为8,占用地址8,9,10,11。最后到e,从这里开始与microsoft的编译器开始有所差异,他的k值为不是8,仍然是4,所以其首地址是12,占用地址12-19。显然其大小为20。

验证:
我们建立一个test1类型的变量,a、b、c、d、e分别赋值2、4、8、16、32。然后从低地址依次打印出内存中每个字节对应的16进制数为:
2 0 4 0 8 0 0 0 10 0 0 0 0 0 0 0 0 0 40 40

显然推断也是正确的。

接下来,看一看几类特殊的情况,为了避免麻烦,不再描述内存分布,只计算结构大小。

第一种:嵌套的结构
struct test2
{
char f;
struct test1 g;
};

在windows平台,microsoft编译器下:

这种情况下如果把test2的第二个成员拆开来,研究内存分布,那么可以知道,test2的成员f占用地址0,g.a占用地址1,以后的内存分布不变,仍然满足所有基本数据成员的首地址都为其对应k的倍数这一原则,那么test2的大小就还是24了。但是实际上test2的大小为32,这是因为:不能因为test2的结构而改变test1的内存分布情况,所以为了使test1种各个成员仍然满足对齐的要求,f成员后面需要填充一定数量的字节,不难发现,这个数量应为7个,才能保证test1的对齐。所以test2相对于test1来说增加了8个字节,所以test2的大小为32。

在linux平台,gcc编译器下:

同样,这种情况下如果把test2的第二个成员拆开来,研究内存分布,那么可以知道,test2的成员f占用地址0,g.a占用地址1,以后的内存分布不变,仍然满足所有基本数据成员的首地址都为其对应k的倍数这一原则,那么test2的大小就还是20了。但是实际上test2的大小为24,同样这是因为:不能因为test2的结构而改变test1的内存分布情况,所以为了使test1种各个成员仍然满足对齐的要求,f成员后面需要填充一定数量的字节,不难发现,这个数量应为3个,才能保证test1的对齐。所以test2相对于test1来说增加了4个字节,所以test2的大小为24。

第二种:位段对齐

struct test3
{
unsigned int a:4;
unsigned int b:4;
char c;
};
或者
struct test3
{
unsigned int a:4;
int b:4;
char c;
};

在windows平台,microsoft编译器下:

相邻的多个同类型的数(带符号的与不带符号的,只要基本类型相同,也为相同的数),如果他们占用的位数不超过基本类型的大小,那么他们可作为一个整体来看待。不同类型的数要遵循各自的对齐方式。
如:test3中,a、b可作为一个整体,他们作为一个int型数据来看待,所以test3的大小为8字节。并且a与b的值在内存中从低位开始依次排列,位于4字节区域中的前0-3位和4-7位

如果test4位以下格式
struct test4
{
unsigned int a:30;
unsigned int b:4;
char c;
};
那么test4的大小就为12个字节,并且a与b的值分别分布在第一个4字节的前30位,和第二个4字节的前4位。

如过test5是以下形式
struct test5
{
unsigned int a:4;
unsigned char b:4;
char c;
};

那么由于int和char不同类型,他们分别以各自的方式对齐,所以test5的大小应为8字节,a与b的值分别位于第一个4字节的前4位和第5个字节的前4位。

在linux平台,gcc编译器下:

struct test3
{
unsigned int a:4;
unsigned int b:4;
char c;
};
gcc下,相邻各成员,不管类型是否相同,占的位数之和超过这些成员中第一个的大小的时候,在结构中以k值为1对齐,在结构外k值为其基本类型的值。不超过的情况下在内存中依次排列。
如test3,其大小为4。a,b的值在内存中依次排列分别为第一个四字节中的0-3和4-7位。

如果test4位以下格式
struct test4
{
unsigned int a:20;
unsigned char b:4;
char c;
};
test4的大小为4个字节,并且a与b的值分别分布在第一个4字节的0-19位,和20-23位,c存放在第4个字节中。
如过test5是以下形式
struct test5
{
unsigned int a:10;
unsigned char b:4;
short c;
};

那么test5的大小应为4字节,a,b的值为0-9位和10-13位。c存放在后两个字节中。如果a的大小变成了20
那么test5的大小应为8字节。即

struct test6
{
unsigned int a:20;
unsigned char b:4;
short c;
};

此时,test6的a、b共占用0,1,2共3字节,c的k值为2,其实可以4位首位置,但是在结构外,a要以int的方式对齐。也就是说连续两个test6对象在内存中存放的话,a的首位置要保证为4的倍数,那么c后面必须多填充2位。所以test6的大小为8个字节。

关于位段结构的部分是比较复杂的。暂时我就知道这么多。



原创粉丝点击