内存对界

文中部分参考自:

点击打开链接http://www.th7.cn/Program/cp/201209/92293.shtml

点击打开链接http://blog.csdn.net/u010064842/article/details/8865632

点击打开链接http://blog.csdn.net/21aspnet/article/details/6729724

点击打开链接http://blog.csdn.net/jamesf1982/article/details/4375719


内存是计算机比较紧俏的资源,对程序的优化也包括对内存的优化工作,通过内存对界的知识,可以对程序占用的内存进行适当的优化。

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

举个例子

#include <iostream>
using namespace std;

typedef struct _Elem{  
	char x1;  
	short x2;  
	double x3;  
	char x4;  
	short x5;  
}Elem; 

int main(int argc, char **argv)
{
	Elem e;
	cout<<sizeof(e)<<endl;
	system("pause");
	return 0;
}

按照平常的理解,char类型占1个字节,short类型占两个字节,double类型占8个字节,那么sizeof(e)是不是应该占14个字节呢?答案是否定的,真实的情况是,占用24个字节。这是因为内存对界的存在。

我们把真实的占用字节的情况打印出来。


从图上可以看出:

x1从0x002AF750-0x002AF751,占2个字节。

x2从0x002AF752-0x002AF757,占6个字节。

x3从0x002AF758-0x002AF75F,占8个字节。

x4从0x002AF760-0x002AF761,占2个字节。

x5从0x002AF762到0x002AF767占6个字节。

加起来一共是24个字节。用一张表来表示如下:(借用了链接1中的图,忽略左边的地址部分)


那它是怎么安排地址的呢?

x1为结构体的第一个成员,其地址和整个结构的地址相同,因而其偏移地址为0。

x2为short类型,其大小为2,因而其自然对界也为2,所以其偏移地址必须为2的整数倍,所以编译器在x2和x1之间添充了1个空字节。这样,x2的偏移地址为2。

x3为double类型,其大小为8,因而其自然对界也为8,所以其偏移地址必须是8的整数倍,所以编译器在x3和x2之间添充了4个空字节。这样,x3的偏移地址为8。

同理,可以算出x4的偏移地址为16,紧跟着x3存储(因为x3是char类型,大小是1,自然对界的偏移地址是1的整数倍)。x5的偏移地址为18。

最后,由于x3要求8字节对界,是该结构所有成员中要求的最大对界单元,因而整个结构struct1的自然对界条件为8字节(注意不是该结构的总大小),所以该结构的大小必须满足8的整数倍,这就要求在x5之后还要再添充4个字节,使整个结构体的大小为24字节。


如此看来,如果我们改变结构体中变量声明的顺序,那么结构体占用的内存大小也会随之改变。

typedef struct _Elem{ 
	double x3; 
	short x2;
	short x5;
	char x1;  
	char x4;  
}Elem;

先不用sizeof(),手动计算出应该占用内存:

偏移地址0-7被x3占用,8-9被x2占用,10-11被x5占用,12被x1占用,13被x4占用,最后加上两个字节的整个结构体对界,所以总共占用内存16个字节。用sizeof()验证果然如此。


通过上面的分析可得到如下结论

1.对于基本数据类型(如int、long、float等。注意,还包括指针),其自然对界条件为该类型所占用存储空间的大小,指针用4个字节的空间;

2.对于复合数据类型(如数组、结构、联合等),其自然对界条件为该数据类型的所有成员中要求的最大对界单元。

 

那为什么要自然对界呢?(摘自http://blog.csdn.net/21aspnet/article/details/6729724

需要字节对齐的根本原因在于CPU访问数据的效率问题。假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误。

 

那么可以更改C编译器的缺省字节对齐方式么?

可以!使用#pragma pack

在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:

使用伪指令#pragma pack (n),C编译器将按照n个字节对齐。

使用伪指令#pragma pack (),取消自定义字节对齐方式。

另外,还有如下的一种方式:

__attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。

__attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

以上的n = 1, 2, 4, 8, 16...第一种方式较为常见。


强调一点:

#pragma pack(4)

typedef struct

{

char buf[3];

word a;

}kk;

#pragma pack()

对齐的原则是min(sizeof(word),4)=2,因此是2字节对齐,而不是我们认为的4字节对齐。也就是说是按pragma pack(n)和变量自己的自然对界中最小的那个大小来对界。

 

这里有三点很重要:
1.
每个成员分别按自己的方式对齐,并能最小化长度
2.
复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度
3.
对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐

补充一下,对于数组,比如:
char a[3];
这种,它的对齐方式和分别写3char是一样的.也就是说它还是按1个字节对齐.
如果写: typedef charArray3[3];
Array3
这种类型的对齐方式还是按1个字节对齐,而不是按它的长度.
不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个.


语法:

#pragma pack( [show] | [push | pop] [,identifier], n )

 

说明:

      (1)pack提供数据声明级别的控制,对定义不起作用;

      (2)调用pack时不指定参数,n将被设成默认值;

      (3)一旦改变数据类型的alignment,直接效果就是占用memory的减少,但是performance会下降;

 

 

 

语法具体分析:

      (1)show:可选参数;显示当前packing aligment的字节数,以warning message的形式被显示;

      (2)push:可选参数;将当前指定的packing alignment数值进行压栈操作,这里的栈是the internalcompiler stack,同时设置当前的packing alignment为n;如果n没有指定,则将当前的packingalignment数值压栈;

      (3)pop:可选参数;从internal compiler stack中删除最顶端的record;如果没有指定n,则当前栈顶record即为新的packingalignment数值;如果指定了n,则n将成为新的packing aligment数值;如果指定了identifier,则internal compiler stack中的record都将被pop直到identifier被找到,然后pop出identitier,同时设置packingalignment数值为当前栈顶的record;如果指定的identifier并不存在于internal compiler stack,则pop操作被忽略;

     (4)identifier:可选参数;当同push一起使用时,赋予当前被压入栈中的record一个名称;当同pop一起使用时,从internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier没有被找到,则忽略pop操作;

     (5)n:可选参数;指定packing的数值,以字节为单位;缺省数值是8,合法的数值分别是1、2、4、8、16。

 

注:默认对齐方式为8


更改c编译器的缺省字节对齐方式:

在缺省情况下,c编译器为每一个变量或数据单元按其自然对界条件分配空间;一般地可以通过下面的两种方法来改变缺省的对界条件:

方法一:

使用#pragma pack(n),指定c编译器按照n个字节对齐;

使用#pragma pack(),取消自定义字节对齐方式。

 

方法二:

__attribute(aligned(n)),让所作用的数据成员对齐在n字节的自然边界上;如果结构中有成员的长度大于n,则按照最大成员的长度来对齐;

__attribute((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。



重要的规则:

       (1)复杂类型中各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个类型的地址相同;

       (2)每个成员分别对齐,即每个成员按自己的方式对齐,并最小化长度;规则就是每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数中较小的一个对齐;

       (3)结构、联合或者类的数据成员,第一个放在偏移为0的地方;以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度两个中比较小的那个进行;也就是说,当#pragma pack指定的值等于或者超过所有数据成员长度的时候,这个指定值的大小将不产生任何效果;

      (4)复杂类型(如结构)整体的对齐是按照结构体中长度最大的数据成员和#pragma pack指定值之间较小的那个值进行;这样在成员是复杂类型时,可以最小化长度;

       (5)结构整体长度的计算必须取所用过的所有对齐参数的整数倍,不够补空字节;也就是取所用过的所有对齐参数中最大的那个值的整数倍,因为对齐参数都是2的n次方;这样在处理数组时可以保证每一项都边界对齐;


例子说明(摘自http://blog.csdn.net/u010064842/article/details/8865632

#pragma pack(4)  
class TestB  
{  
public:  
int aa; //第一个成员,放在[0,3]偏移的位置,  
char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。  
short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。  
char c; //第四个,自身长为1,放在[8]的位置。  
};  
可见,此类实际占用的内存空间是9个字节。根据规则5,结构整体的对齐是min( sizeof( int ), pack_value ) =4,所以sizeof( TestB ) = 12;


#pragma pack(2)  
class TestB  
{  
public:  
int aa; //第一个成员,放在[0,3]偏移的位置,  
char a; //第二个成员,自身长为1,#pragma pack(2),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。  
short b; //第三个成员,自身长2,#pragma pack(2),取2,按2字节对齐,所以放在偏移[6,7]的位置。  
char c; //第四个,自身长为1,放在[8]的位置。  
}; 
可见结果与例子一相同,各个成员的位置没有改变,但是此时结构整体的对齐是min( sizeof( int ), pack_value ) = 2,所以sizeof(TestB ) = 10;


#pragma pack(4)  
class TestC  
{  
public:  
char a; //第一个成员,放在[0]偏移的位置,  
short b; //第二个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[2,3]的位置。  
char c; //第三个,自身长为1,放在[4]的位置。  
};
整个类的实际内存消耗是5个字节,整体按照min( sizeof( short ), 4 ) = 2对齐,所以结果是sizeof(TestC ) = 6;


truct Test  
{  
char x1; //第一个成员,放在[0]位置,  
short x2; //第二个成员,自身长度为2,按2字节对齐,所以放在偏移[2,3]的位置,  
float x3; //第三个成员,自身长度为4,按4字节对齐,所以放在偏移[4,7]的位置,  
char x4; //第四个成员,自身长度为1,按1字节对齐,所以放在偏移[8]的位置,  
};  
所以整个结构体的实际内存消耗是9个字节,但考虑到结构整体的对齐是4个字节,所以整个结构占用的空间是12个字节。  






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值