首页 > 代码库 > 内存对齐详解

内存对齐详解

内存对齐,memory alignment.为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。
内存对齐一般讲就是cpu access memory的效率(提高运行速度)和准确性(在一些条件下,如果没有对齐会导致数据不同步现象).依赖cpu,平台和编译器的不同.一些cpu要求较高(这句话说的不准确,但是确实依赖cpu的不同),而有些平台已经优化内存对齐问题,不同编译器的对齐模数不同.总的来说内存对齐属于编译器的问题.

一般情况下不需要理会内存对齐问题,内存对齐是编译器的事情.但碰到一些问题上还是需要理解这个概念.毕竟c/c++值直接操作内存的语言.需要理解程序在内存中的分布和运行原理.

总之一句话就是:不要让代码依赖内存对齐.


1.原因:为什么需要内存对齐.
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。


2.内存对齐的规则和范例
讲述内存对齐之前先看下各种类型的大小,和编译器以及字长有关具体在此不多叙述.
具体帖子:http://blog.csdn.net/lyl0625/article/details/7350045
成员的内存分配规律:从结构体的首地址开始向后依次为每个成员寻找第一个满足条件的首地址x,该条件是x % N = 0,并且整个结构的长度必须为各个成员所使用的对齐参数中最大的那个值的最小整数倍,不够就补空字节。
结构体中所有成员的对齐参数N的最大值称为结构体的对齐参数。

1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值(或默认值)和这个数据成员类型长度中,比较小的那个进行。在上一个对齐后的地方开始寻找能被当前对齐数值整除的地址.
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐.主要体现在,最后一个元素对齐后,后面是否填补空字节,如果填补,填补多少.对齐将按照#pragma pack指定的数值(或默认值)和结构(或联合)最大数据成员类型长度中,比较小的那个进行。
3、结合1、2颗推断:当#pragma pack的n值等于或超过所有数据成员类型长度的时候,这个n值的大小将不产生任何效果。
两点注意:数组,嵌套结构体.
数组:
对齐值为:min(数组元素类型,指定对齐长度).但数组中的元素是连续存放,存放时还是按照数组实际的长度.
如char t[9],对齐长度为1,实际占用连续的9byte.然后根据下一个元素的对齐长度决定在下一个元素之前填补多少byte.
嵌套的结构体:
假设
struct A
{
......
struct B b;
......
};
对于B结构体在A中的对齐长度为:min(B结构体的对齐长度,指定的对齐长度).
B结构体的对齐长度为:上述2中结构整体对齐规则中的对齐长度.

例子:
VC++6.0中n 默认是8个字节,可以修改这个设定的对齐参数
也可以采用指令:#pragma   pack(xx)控制.

1.基础例子

 1 #include <iostream>
 2 #include <cstdio>
 3 using namespace std;
 4 #pragma   pack(n)
 5 struct A
 6 {
 7 char   c; //1byte
 8 double d; //8byte
 9 short s; //2byte
10 int i; //4byte
11 };
12 int main(int argc, char* argv[])
13 {
14 A strua;
15 printf("%len:d\n",sizeof(A));
16 printf("%d,%d,%d,%d",&strua.c,&strua.d,&strua.s,&strua.i);
17 return 0;
18 }
View Code

1)n设置为8byte时
结果:len:24,
1245032,1245040,1245048,1245052
内存中成员分布如下:
strua.c分配在一个起始于8的整数倍的地址1245032(为什么是这样读者先自己思考,读完就会明白),接下来要在strua.c之后分配strua.d,由于double为8字节,取N=min(8,8),8字节来对齐,所以从strua.c向后找第一个能被8整除的地址,所以取1245032+8得1245040, strua.s 为2byte小于参数n,所以N=min(2,8),即N=2,取2字节长度对齐,所以要从strua.d后面寻找第一个能被2整除的地址来存储strua.s,由于strua.d后面的地址为1245048可以被2整除,所以strua.s紧接着分配,现在来分配strua.i,int为4byte,小于指定对齐参数8byte,所以N=min(4,8)取N=4byte对齐,strua.s后面第一个能被4整除地址为1245048+4,所以在1245048+4的位置分配了strua.i,中间补空,同时由于所有成员的N值的最大值为8,所以整个结构长度为8byte的最小整数倍,即取24byte其余均补0.
于是该结构体的对齐参数就是8byte。
2)当对齐参数n设置为16byte时,结果同上,不再分析
3)当对齐参数设置为4byte时
上例结果为:Len:20
1245036,1245040,1245048,1245052
内存中成员分布如下:
Strua.c起始于一个4的整数倍的地址,接下来要在strua.c之后分配strua.d,由于strua.d长度为8byte,大于对齐参数4byte,所以N=min(8,4)取最小的4字节,所以向后找第一个能被4整除的地址来作为strua.d首地址,故取1245036+4,接着要在strua.d后分配strua.s,strua.s长度为2byte小于4byte,取N=min(2,4)2byte对齐,由于strua.d后的地址为1245048可以被2
整除,所以直接在strua.d后面分配,strua.i的长度为4byte,所以取N=min(4,4)4byte对齐,所以从strua.s向后找第一个能被4整除的位置即1245048+4来分配和strua.i,同时N的最大值为4byte,所以整个结构的长度为4byte的最小整数倍20byte
4)当对齐参数设置为2byte时
上例结果为:Len:16
1245040,1245042,1245050,1245052
Strua.c分配后,向后找一第一个能被2整除的位置来存放strua.d,依次类推
5)1byte对齐时:
上例结果为:Len:15
1245040,1245041,1245049,1245051
此时,N=min(sizeof(成员),1),取N=1,由于1可以整除任何整数,所以各个成员依次分配,没有间空.
6)当结构体成员为数组时,并不是将整个数组当成一个成员来对待,而是将数组的每个元素当一个成员来分配,其他分配规则不变,如将上例的结构体改为:
struct A
{
char c; //1byte
double d; //8byte
short s; //2byte
char  szBuf[5];
};
对齐参数设置为8byte,则,运行结果如下:
Len:24
1245032,1245040,1245048,1245050
Strua 的s分配后,接下来分配Strua 的数组szBuf[5],这里要单独分配它的每个元素,由于是char类型,所以N=min(1,8),取N=1,所以数组szBuf[5]的元素依次分配没有间隙。

看完上述的例子,基本分配的规律和方法应该已经知道.下面主要说明数组,嵌套结构体,指针时的一些内存对齐问题.
最重要的是自己写程序证明.

2.数组,嵌套.
测试环境:64位 ubuntu;g++ (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3

alignment.cpp代码如下:

#include <iostream>
#include <cstdio>
using namespace std;
#pragma pack(8)
struct Args
{
        char ch; 
        double d;
        short st; 
        char rs[7];
        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<<"Magnum1 ch =="<<(unsigned long)&args.ch<<" d=="<<(unsigned long)&args.d<<" st=="<<(unsigned long)&args.st<<" rs=="<<(unsigned long)&args.rs<<" i=="<<(unsigned long)&args.i<<endl;
cout<<"Args:"<<sizeof(args)<<endl;
cout<<""<<(unsigned long)&args.i-(unsigned long)&args.rs<<endl;

cout<<"Argsa:"<<sizeof(arga)<<endl;
cout<<"Magnum2 Argsa ch=="<<(unsigned long)&arga.ch<<" test=="<<(unsigned long)&arga.test<<" jd=="<<(unsigned long)&arga.jd<<" i=="<<(unsigned long)&arga.i<<endl;
cout<<"Argsa:"<<(unsigned long)&arga.i -(unsigned long)&arga.jd<<endl;
cout<<"Argsa:"<<(unsigned long)&arga.jd-(unsigned long)&arga.test<<endl;
return 0;
}
View Code

输出结果:

Magnum1 ch ==6295936 d==6295944 st==6295952 rs==6295954 i==6295964
Args:32
10
Argsa:56
Magnum2 Argsa ch==6295968 test==6295976 jd==6296008 i==6296020
Argsa:12
Argsa:32

struct Args长度32 struct Argsa长度:56.
改成#pragma pack (16)结果一样.
这个例子证明了三点:
对齐长度长于struct中的类型长度最长的值时,设置的对齐长度等于无用.
数组对齐的长度是按照数组成员类型长度来比对的.
嵌套的结构体中,所包含的结构体的对齐长度是结构体的对齐长度.

 

3.指针.主要是因为32位和64位机寻址上
测试环境同2.(64位系统)

关于指针和数组:

指针就是指针,在32位系统,指针变量永远占4字节,其值为某一个内存的地址。指针可以指向任何地方,但是不是任何地方你都能通过这个指针变量访问到。

数组就是数组,其大小与元素的类型和个数有关;定义数组的时必须指定其元素的类型和个数;数组可以存任何类型的数据,但不能存函数。

指针和数组是两个完全不一样的东西。只是他们都可以“以指针的形式”或以“以下标的形式”进行访问。

 

alignment-pointer.cpp代码如下:

#include <iostream>
#include <cstdio>
#pragma pack(4)
using namespace std;
struct Args
{
        int i;
        double d;
        char *p; 
        char ch; 
        int *pi;
}args;
int main()
{    
        cout<<"args length:"<<sizeof(args)<<endl;
    cout<<"Magnum pointer i =="<<(unsigned long)&args.i<<" d=="<<(unsigned long)&args.d<<" p=="<<(unsigned long)&args.p<<" ch=="<<(unsigned long)&args.ch<<" pi=="<<(unsigned long)&args.pi<<endl;
        cout<<(unsigned long)&args.ch-(unsigned long)&args.p<<endl;
        cout<<(unsigned long)&args.pi-(unsigned long)&args.ch<<endl;
        return 0;
}
View Code

 

输出结果如下:

args length:32
Magnum pointer i ==6295936 d==6295940 p==6295948 ch==6295956 pi==6295960
8
4

这里面为什么ch==6295956,找拉很久后来发现我当前的系统是ubuntu12.04-64bits的,一个指针占用8bytes,而32bits系统不会这样。

所以这里提醒我们考虑内存对齐,要先弄清楚自己当前OS的位数

设置pack为8时:

args length:40
Magnum pointer i ==6295936 d==6295944 p==6295952 ch==6295960 pi==6295968
8
8

 

最后补充一个宏_INTSAIZEOF的作用是求出变量占用内存空间的大小,定义如下:

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )