c/c++内存对齐详解
一、为什么会有内存对齐?
进行内存对齐的作用主要有两个.
( 1 )平台移植 : 不是所有的硬件平台都能够访问任意地址上的数据,
( 2 )性能 : 内存对齐后访问速度提升了 (对于访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。)
为什么内存对齐会提升效率?
CPU把内存当成是一块一块的,块的大小可以是2、4、8、16字节等大小。CPU在读取内存的时候是一块一块读取的。块大小即memory access granularity:内存读取粒度。
假设CPU要读取一个int型4字节大小的数据,看下列2种情况:
(1)数据从0字节开始;
(2)数据从1字节开始;
假设内存读取粒度是4.情况(1)的时候,CPU只需一次便可把4字节读取出来。
但是情况(2)的时候,要复杂一些。这个时候CPU先访问一次内存,读取0-3字节进寄存器,再读取4-7字节进寄存器,然后把0、6、7、8字节的数据删除掉,最后合并1-4字节的数据。可以看出,如果内存没有对齐,所进行的操作要复杂得多。
二、对齐规则?
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
规则:
(1)数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
(2)结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
(3)当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
三、示例代码
#include<stdio.h>
struct test
{
char a;
int b;
char c;
double d;
char e;
};
int main(void)
{
printf("%d\n",sizeof(struct test));
return 1;
}
输出结果为24
我的执行环境是Debian Linux(内核2.6.18),gcc 4.1.2,其实与在windows下使用vc的原理是一样的而且不对默认对齐进行修改的话,结果也是一样的。
四、分析
默认情况下,c/c++一般是设置对齐系数为4,对于上面的例子也是如此。从上面的结果我们可以看出结构体test占用的内存大小为24,而不是1+4+1+8+1=15,那么24是如何得到的呢?
按照二(1)中的规则对各个成员占用的内存进行分析如下:
struct test
{
char a; /*长度1 < 4 按1对齐;起始offset=0, 0%1=0,存放区间[0]*/
int b; /*长度4 = 4 按4对齐;起始offset=1, 4%4=0;存放位置区间[4,7] */
char c; /*长度1 < 4 按1对齐;起始offset=8, 8%1=0,存放区间[8]*/
double d;/*长度8 > 4 按4对齐;起始offset=9, 12%4=0,存放区间[12,19]*/*/
char e; /*长度1 < 4 按1对齐;起始offset=20, 20%1=0,存放区间[20]*/
};
在按照二(2)中的规则对结构体整体占用的内存进行分析:
整体对齐系数为min(对齐系数,max(成员占用内存大小))=min(4,8)=4
经过上面的分析test的成员共占用内存区间[0,20],大小为21个字节,然后进行整体对齐,需要满足整体为整体系数4的倍数,那么最近的大小就是24了,所以结构体test占用的内存空间为24字节。
五、数组,嵌套.
#include <iostream>
#include <cstdio>
using namespace std;
#pragma pack(8)
struct Args
{
char ch;
double d;
short st;
char rs[9];
int i;
} args;
struct Argsa
{
char ch;
Args test;
char jd[10];
int i;
}arga;
int main()
{
// cout <<sizeof(char)<<" "<<sizeof(double)<<" "<<sizeof(short)<<" "<<sizeof(int)<<endl;
//cout<<sizeof(long)<<" "<<sizeof(long long)<<" "<<sizeof(float)<<endl;
cout<<"Args:"<<sizeof(args)<<endl;
cout<<""<<(unsigned long)&args.i-(unsigned long)&args.rs<<endl;
cout<<"Argsa:"<<sizeof(arga)<<endl;
cout<<"Argsa(i-jd):"<<(unsigned long)&arga.i -(unsigned long)&arga.jd<<endl;
cout<<"Argsa(jd-test):"<<(unsigned long)&arga.jd-(unsigned long)&arga.test<<endl;
return 0;
}
输出结果:
Args:32
10
Argsa:56
Argsa(i-jd):12
Argsa(jd-test):32
#include "stdafx.h"
#include "iostream"
using namespace std;
#pragma pack(8)
struct tagOne{
short s;
char cArray[4];
double d;
};
struct tagTwo
{
char c;
tagOne tOne;//tOne的大小为16,在tagTwo中整体以8以齐,所以上面char c补7个字节,
};
int _tmain(int argc, _TCHAR* argv[])
{
tagTwo temp;
printf("%d\n",sizeof temp);
return 0;
}
上面这个例子的输出结果为24
当结构体中出现结构体类型的成员时,不会将嵌套的结构体类型的整体长度参与到对齐值计算中,而是以嵌套定义的结构体所使用对齐值进行对齐,
六、其它
1. 在编写代码时候可以通过#pragma pack(n),n=1,2,4,8,16来灵活控制内存对齐的系数,当需要关闭内存对齐时,可以使用#pragma pack()实现。
2. 注意事项
内存对齐可以大大的提高编译器的处理速度,但不是任何时候都是必需的,有的时候不注意的话,还可能出现意想不到的错误!最典型的情况就是网络通信程序的编码中,一定要在定义结构体或者联合之前使用#pragma pack()把内存对齐关闭,这是因为远程主机通常不知道对方使用的何种对齐方式,通过socket接收的字节流,然后按照字节解析得到对应的结果,如果使用内存对齐,远程主机很哟可能会得到错误的结果!这种情况曾经指导上机时遇到过,而且属于比较隐蔽的错误,debug了好久才发现问题出在这里。
3. 优化结构体
虽然内存对齐可以提高运行效率,但是却浪费了内存,在现代PC上通常不会在乎这点小的空间,但是在一些内存很小的嵌入式设备上,可能就要锱铢必较了。其实我们发现在不影响功能的前提下,可以调整成员的顺序来减少“内存空洞”带来的浪费。如果三.中的结构体代码可以调整为
struct test
{
char a;
char c;
char e;
int b;
double d;
}
这个时候整个结构体占用的内存空间将会从上面的24减少到16。