C++内存对齐详解

最近看各公司笔试和面试的试题,不少是关于C++内存对齐方面的。这个问题我以前也模模糊糊的了解一些,但总是不甚清楚。这次费了很大劲,终于算是搞明白了。整理出来,和大家分享一下。

这一切要从机器字长和存储字长说起。机器字长是CPU每次处理的二进制的位数;存储字长是内存中一个存储单元的包含二进制位数,或一次内存读写操作的位数,也可以理解成数据线的根数。

下面以机器字长和存储字长都是32位的机器为例,来说明为什么要内存对齐。我们知道内存是以字节(byte)来编址的,32位也就是4个字节。由于内存的读写单位是存储单元,所以CPU对内存进行读写时,发送给它的地址必须是存储单元长度的倍数,即存储字长的倍数。我们这里的存储字长是4 bytes,所以这个地址必须是4n。假设现有一个大小为4 bytes的int类型的值,如不考虑对齐,它在内存中的存储有以下4种情况:


图 1

case 1时,处理器只需要读一个存储单元,就可以直接得到int值; case 2、3、4时,就需要读取两个内存单元,并且要经过一系列处理后才能得到所需的值,这种情况下效率是很低的。而这种糟糕情况,对于int类型来说,发生的概率是75%。若果不对齐,CPU对内存的读写是很低效的。

在讨论对齐规则之前,先把我用来测试的环境说一下。编译器VC6.0,操作系统Win7,机器字长、存储字长32(4bytes)。此环境下基本数据的大小为:sizeof(char)=1;sizeof(short)=2; sizeof(int)=4; sizeof(double)=8。

设存储字长为W bytes,机器字长不小于W。根据前面的分析,我们从内存访问的效率出发,很容易得到一个原则 每个基本数据类型T存取时,访问的存储单元的个数应为ceiling(sizeof(T)/W),而不应超出此值。这就是内存对齐的基本原则。在进一步讨论之前,我们先引入一个概念:对齐大小。对于基本数据类型而言,对齐大小是sizeof的返回值;而对于复合类型,则是其数据成员中最大的对齐大小的值,sizeof的返回值是它的存储大小。比如:
struct T
{
	int a;
	int b;
};

T的对齐大小为sizeof(int)=4,存储大小是2*4=8。

以下是内存对其的规则,

规则一:数据类型的起始地址必须为其对齐大小的整数倍。

规则二:复合类型的的存储大小为其对齐大小的整数倍。

规则三:复合类型包含复合类型的时候,子结构按一个整体对待,存储大小不变。

       在基本数据类型的大小都是2整数幂的前提下,这个规则满足基本原则的要求。幸运的是C++的基本数据类型确实全都是2的整数幂(bool类型的提升为一个字节处理),VC6.0在默认情况下,也确实用的这一规则。这一个规则的好处是,不必关心存储字长W,就可以满足16、32、64乃至128位存储字长下,内存对齐的要求。以下通过例子进行说明。

例1.

#include<iostream>
using std::cout;
using std::endl;
struct A
{
	char a;
	int  b;
	short c;
};
int main()
{
	A testA;
	cout<<sizeof(A)<<endl;
	cout<<(void*)&testA.a<<endl;//将char*转成void*是必要的,不然会当成字符串输出
	cout<<&testA.b<<endl;
	cout<<&testA.c<<endl;
	return 0;
}

程序运行结果:


可以看出类型A占12 bytes,下面三行是各个数据成员的起始地址。每次运行得到的这些地址可能不同,不过无妨,我们只关心它们之间的偏移。若是把这些地址都减去最小的那个,我们得到数据在内存中存放的示意图,如下:


图 2

在0位置写入char a后,这一存储单元还剩3 bytes,接下来是int b。 int的对齐大小为4 bytes,所以他的起始位置必须为4n,a后面3 bytes的内存不符合这个条件,于是跳过(这个空间会由系统填充的)。而紧随其后的4位置满足了4的倍数的条件,便可以将b写入此处。下一个要存储的是shortc,而b后的位置8满足规则一,可以写入c。三个元素都写完了,用掉了10bytes。我们知道A中元素最大的对齐大小为4,因此A的对齐大小便是4,10bytes不满足规则二,于是补上两个凑足12(=3*4)bytes。

以下例子中,为了节省篇幅,突出重点,我只粘贴程序中主要的部分,相信关心内存对齐问题的朋友,可以明白我在说什么。
例2.

struct B
{
	char a;
	short b;
	int  c;
};

B testB;
cout<<sizeof(B)<<endl;
cout<<(void*)&testB.a<<endl;
cout<<&testB.b<<endl;
cout<<&testB.c<<endl;
运行结果:

类型B和例1中的A其实是一样的,只是改变了内部元素的声明顺序,得到的结果便发生了改变,最明显的就是它节省了4 bytes。根据输出信息,我们得到B的数据在内存中存放的示意图:

图 3

根据规则一,把数据元素排列后,用去8 bytes。这个大小正好满足规则二,也就不再需要补充了,所以B的存储大小是8 bytes。

例 3.
struct C
{
	char a;
	double b;
	char c;
	int d;
	char e;
};
C testC;
cout<<sizeof(C)<<endl;
cout<<(void*)&testC.a<<endl;
cout<<&testC.b<<endl;
cout<<(void*)&testC.c<<endl;
cout<<&testC.d<<endl;
cout<<(void*)&testC.e<<endl;
运行结果:

数据在内存中存放的示意图:

图 4

与以上例子一样,先根据规则一将数据安排好,由于此结构中最大元素doubleb的大小为8 bytes,按8的倍数补齐,C的大小即为32 bytes。不过仔细一数,发现实际用到的字节数只有15 bytes,而浪费掉的却有17 bytes之多。现在内存容量都比较大,也许不需要太计较,但作为一个以勤俭节约为传统美德的民族的子民,看到这耀眼的空白,总有点诚惶诚恐的感觉。根据结构A和B的经验,我们调整一下结构。

例4.

struct D
{
	char a;
	char b;
	char c;
	int d;
	double e;	
};

D testD;
cout<<sizeof(D)<<endl;
cout<<(void*)&testD.a<<endl;
cout<<(void*)&testD.b<<endl;
cout<<(void*)&testD.c<<endl;
cout<<&testD.d<<endl;
cout<<&testD.e<<endl;
运行结果:

数据在内存中存放的示意图:

图5

与图4相比,这个是不是看上去好了很多。不错,它确实节省了一半的内存。通过这几个例子我们可以得到一个经验,在设计新复合类型的时候,我们应尽量把相同类型的数据放在一块;不同类型的数据之间按存储大小递增或递减的方式排列,这样就可以保证大多数情况下浪费的空间最少。

下面说说结构中包含结构的情况。

例5.

struct subA
{
	char a;
	short b;
	short c;
};
struct subB 
{
	char a;
	double b;
};

subA testsubA;
subB testsubB;
cout<<"sizeof(subB):"<<sizeof(subA)<<endl;
cout<<"address of testsubA.a:"<<(void*)&testsubA.a<<endl;
cout<<"address of testsubA.b:"<<(void*)&testsubA.b<<endl;
cout<<"address of testsubA.c:"<<(void*)&testsubA.c<<endl;
cout<<"\nsizeof(subA):"<<sizeof(subB)<<endl;
cout<<"address of testsubB.a:"<<(void*)&testsubB.a<<endl;
cout<<"address of testsubB.b:"<<(void*)&testsubB.b<<endl;

运行结果:


由此可到subA和subB的数据在内存中的示意图:

图 6

在此基础上定义

struct E 
{	
	subA a;
	char b;
	int  c;
	subB d;	
};

E testE;
cout<<"sizeof(testE):"<<sizeof(testE)<<endl;
cout<<"address of testE.a.a:"<<(void*)&testE.a.a<<endl;
cout<<"address of testE.a.b:"<<(void*)&testE.a.b<<endl;
cout<<"address of testE.a.c:"<<(void*)&testE.a.c<<endl;

cout<<"\naddress of testE.b:"<<(void*)&testE.b<<endl;
cout<<"address of testE.c:"<<(void*)&testE.c<<endl;

cout<<"\naddress of testsubB.d.a:"<<(void*)&testE.d.a<<endl;
cout<<"address of testsubB.d.b:"<<(void*)&testE.d.b<<endl;
运行结果:

E在内存中的结构示意图为:

图 7

可以看出,给E分配存储空间时,先在0位置安放subA a,占去了6 bytes。接下来安排char b和int c,在安排subB之前共用去了12 bytes。根据规则三,subB按整体对待。由于它的对齐大小是8,起始地址必须是8n,于是跳过4bytes在位置16安放,占去sizeof(subB)=16个字节。至此,E占用了32bytes, E的对齐大小等于subB的对齐大小为8,32=4*8,满足规则二,不需要补齐,这就得到了E的存储大小32bytes。 


到这里,VC6.0的默认对齐方式就算说完了。也许你发现,subB的存储浪费了将近50%的内存,而我们也确实没办法进一步优化它。如果你的程序注重空间而不太要求效率的话,这确实会让你很难受。幸运的是,好的编译器总会提供你更多的选择。预编译命令#pragma pack(n)可以帮你解决这一问题,它允许你设置数据的对齐大小。编译器建议你这个n值取2的整数幂,不然它会给你一个警告,然后不理你的设置。即便按要求设置了n,编译器也不一定会用,它真正使用的值是,你设置的n和类型默认对齐大小中较小的一个,即min{n,类型的默认对齐大小}。是不是看着很绕,没关系,先看个例子。

例6.

#include<iostream>
using std::cout;
using std::endl;
#pragma pack(2)//语句1
struct F
{
short a;
int  b;
char c;
};
int main()
{

F testF;
cout<<sizeof(F)<<endl;
cout<<&testF.a<<endl; 
cout<<&testF.b<<endl;
cout<<(void*)&testF.c<<endl;

	return 0;
}

不加语句1时运行的结果:

加上语句1后运行的结果:


两种情况在内存中的示意图如下:

图 8

设置对齐大小前的就不解释了。设置了对齐大小为n=2后,先在0位置分配了short a。之后分配int b,此时min{n=2,sizeof(int)=4}=2,所以int的对齐大小为2,因为位置2满足了规则一,所以可以在此存储b。然后分配char,min{n=2,sizeof(char)=1}=1,也就是说char的对齐大小没有改变,直接存储就行,最后补成2的倍数,便得sizeof(F)=8.

OK,这就我理解的全部内容。鉴于本人水平有限,且对底层不甚了解,文中难免出现这样那样的错误,还望高手雅正。




  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值