内存对齐的规则以及作用 经典总结

来源:互联网 发布:淘宝上买港版iphone7p 编辑:程序博客网 时间:2024/04/29 06:19

首先由一个程序引入话题:

 1 //环境:vc6 + windows sp2
 2 //程序1
 3 #include <iostream>
 4 
 5 using namespace std;
 6 
 7 struct st1 
 8 {
 9     char a ;
10     int  b ;
11     short c ;
12 };
13 
14 struct st2
15 {
16     short c ;
17     char  a ;
18     int   b ;
19 };
20 
21 int main()
22 {
23     cout<<"sizeof(st1) is "<<sizeof(st1)<<endl;
24     cout<<"sizeof(st2) is "<<sizeof(st2)<<endl;
25     return 0 ;
26 }
27 

程序的输出结果为:

 sizeof(st1) is 12

        sizeof(st2) is 8 

 

问题出来了,这两个一样的结构体,为什么sizeof的时候大小不一样呢?

本文的主要目的就是解释明白这一问题。

 

内存对齐,正是因为内存对齐的影响,导致结果不同。

对于大多数的程序员来说,内存对齐基本上是透明的,这是编译器该干的活,编译器为程序中的每个数据单元安排在合适的位置上,从而导致了相同的变量,不同声明顺序的结构体大小的不同。

 那么编译器为什么要进行内存对齐呢?程序1中结构体按常理来理解sizeof(st1)和sizeof(st2)结果都应该是7,4(int) + 2(short) + 1(char) = 7 。经过内存对齐后,结构体的空间反而增大了。

在解释内存对齐的作用前,先来看下内存对齐的规则

<!--[if !supportLists]-->1、  <!--[endif]-->数据成员各自对齐:对于结构的各个成员,第一个成员位于偏移为0的位置,以后每个数据成员的偏移量必须是min(#pragma pack()指定的数,这个数据成员的自身长度) 的倍数。

<!--[if !supportLists]-->2、  <!--[endif]-->结构(或联合)本身也要进行对齐:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

#pragma pack(n) 表示设置为n字节对齐。 VC6默认8字节对齐


以程序1为例解释对齐的规则 :

St1 :char占一个字节,起始偏移为0 ,int 占4个字节,min(#pragma pack()指定的数,这个数据成员的自身长度) = 4(VC6默认8字节对齐),所以int按4字节对齐,起始偏移必须为4的倍数,所以起始偏移为4,在char后编译器会添加3个字节的额外字节,不存放任意数据。short占2个字节,按2字节对齐,起始偏移为8,正好是2的倍数,无须添加额外字节。到此规则1的数据成员对齐结束,此时的内存状态为:

oxxx|oooo|oo

0123 4567 89 (地址)

(x表示额外添加的字节)

共占10个字节。还要继续进行结构本身的对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行,st1结构中最大数据成员长度为int,占4字节,而默认的#pragma pack 指定的值为8,所以结果本身按照4字节对齐,结构总大小必须为4的倍数,需添加2个额外字节使结构的总大小为12 。此时的内存状态为:

oxxx|oooo|ooxx

0123 4567 89ab  (地址)

到此内存对齐结束。St1占用了12个字节而非7个字节。

 St2 的对齐方法和st1相同,读者可自己完成。

 

内存对齐的主要作用是:

<!--[if !supportLists]-->1、  <!--[endif]-->平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

<!--[if !supportLists]-->2、  <!--[endif]-->性能原因:经过内存对齐后,CPU的内存访问速度大大提升。具体原因稍后解释。

 图一:


这是普通程序员心目中的内存印象,由一个个的字节组成,而CPU并不是这么看待的。

 图二:


CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access granularity(粒度)本人把它翻译为“内存读取粒度” 。【原文::memory access granularity,computer's processor  accesses memory in two-, four-, eight- 16- or even 32-byte chunks,不同CPU 粒度可能不一样

 

假设CPU要读取一个int型4字节大小的数据到寄存器中,分两种情况讨论:

<!--[if !supportLists]-->1、<!--[endif]-->数据从0字节开始

<!--[if !supportLists]-->2、<!--[endif]-->数据从1字节开始

 

再次假设内存读取粒度为4。

 图三:

当该数据是从0字节开始时,很CPU只需读取内存一次即可把这4字节的数据完全读取到寄存器中。

 当该数据是从1字节开始时,问题变的有些复杂,此时该int型数据不是位于内存读取边界上,这就是一类内存未对齐的数据。

 

图四:

 

此时CPU先访问一次内存,读取0—3字节的数据进寄存器,并再次读取4—5字节的数据进寄存器,接着把0字节和6,7,8字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器。对一个内存未对齐的数据进行了这么多额外的操作,大大降低了CPU性能。

 这还属于乐观情况了,上文提到内存对齐的作用之一为平台的移植原因,因为以上操作只有有部分CPU肯干,其他一部分CPU遇到未对齐边界就直接罢工了。


   原文地址:http://www.cppblog.com/snailcong/archive/2009/03/16/76705.html


补充:

1.举例理解:

下面我们通过一系列例子的详细说明来证明这个规则

编译器:GCC 3.4.2VC6.0

平台:Windows XP

 

典型的struct对齐

struct定义:

#pragma pack(n) /* n = 1, 2, 4, 8, 16 */

struct test_t {

 int a;

 char b;

 short c;

 char d;

};

#pragma pack(n)

首先确认在试验平台上的各个类型的size,经验证两个编译器的输出均为:

sizeof(char) = 1

sizeof(short) = 2

sizeof(int) = 4

 

试验过程如下:通过#pragma pack(n)改变“对齐系数”,然后察看sizeof(struct test_t)的值。

 

11字节对齐(#pragma pack(1))

输出结果:sizeof(struct test_t) = 8 [两个编译器输出一致]

分析过程:

1) 成员数据对齐

#pragma pack(1)

struct test_t {

 int a;  /* 长度4 > 1 1对齐;起始offset=0 0%1=0;存放位置区间[0,3] */

 char b;  /* 长度1 = 1 1对齐;起始offset=4 4%1=0;存放位置区间[4] */

 short c; /* 长度2 > 1 1对齐;起始offset=5 5%1=0;存放位置区间[5,6] */

 char d;  /* 长度1 = 1 1对齐;起始offset=7 7%1=0;存放位置区间[7] */

};

#pragma pack()

成员总大小=8

 

2) 整体对齐

整体对齐系数 = min((max(int,short,char), 1) = 1

整体大小(size)=$(成员总大小 $(整体对齐系数圆整 = 8 /* 8%1=0 */ [1]

 

22字节对齐(#pragma pack(2))

输出结果:sizeof(struct test_t) = 10 [两个编译器输出一致]

分析过程:

1) 成员数据对齐

#pragma pack(2)

struct test_t {

 int a;  /* 长度4 > 2 2对齐;起始offset=0 0%2=0;存放位置区间[0,3] */

 char b;  /* 长度1 < 2 1对齐;起始offset=4 4%1=0;存放位置区间[4] */

 short c; /* 长度2 = 2 2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */

 char d;  /* 长度1 < 2 1对齐;起始offset=8 8%1=0;存放位置区间[8] */

};

#pragma pack()

成员总大小=9

2) 整体对齐

整体对齐系数 = min((max(int,short,char), 2) = 2

整体大小(size)=$(成员总大小 $(整体对齐系数圆整 = 10 /* 10%2=0 */

 

34字节对齐(#pragma pack(4))

输出结果:sizeof(struct test_t) = 12 [两个编译器输出一致]

分析过程:

1) 成员数据对齐

#pragma pack(4)

struct test_t {

 int a;  /* 长度4 = 4 4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */

 char b;  /* 长度1 < 4 1对齐;起始offset=4 4%1=0;存放位置区间[4] */

 short c; /* 长度2 < 4 2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */

 char d;  /* 长度1 < 4 1对齐;起始offset=8 8%1=0;存放位置区间[8] */

};

#pragma pack()

成员总大小=9

 

2) 整体对齐

整体对齐系数 = min((max(int,short,char), 4) = 4

整体大小(size)=$(成员总大小 $(整体对齐系数圆整 = 12 /* 12%4=0 */

 

48字节对齐(#pragma pack(8))

输出结果:sizeof(struct test_t) = 12 [两个编译器输出一致]

分析过程:

1) 成员数据对齐

#pragma pack(8)

struct test_t {

 int a;  /* 长度4 < 8 4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */

 char b;  /* 长度1 < 8 1对齐;起始offset=4 4%1=0;存放位置区间[4] */

 short c; /* 长度2 < 8 2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */

 char d;  /* 长度1 < 8 1对齐;起始offset=8 8%1=0;存放位置区间[8] */

};

#pragma pack()

成员总大小=9

2) 整体对齐

整体对齐系数 = min((max(int,short,char), 8) = 4

整体大小(size)=$(成员总大小 $(整体对齐系数圆整 = 12 /* 12%4=0 */

 

516字节对齐(#pragma pack(16))

输出结果:sizeof(struct test_t) = 12 [两个编译器输出一致]

分析过程:

1) 成员数据对齐

#pragma pack(16)

struct test_t {

 int a;  /* 长度4 < 16 4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */

 char b;  /* 长度1 < 16 1对齐;起始offset=4 4%1=0;存放位置区间[4] */

 short c; /* 长度2 < 16 2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */

 char d;  /* 长度1 < 16 1对齐;起始offset=8 8%1=0;存放位置区间[8] */

};

#pragma pack()

成员总大小=9

 

2) 整体对齐

整体对齐系数 = min((max(int,short,char), 16) = 4

整体大小(size)=$(成员总大小 $(整体对齐系数圆整 = 12 /* 12%4=0 */

8字节和16字节对齐试验证明了“规则”的第3点:“当#pragma packn值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果”。


2.什么是对齐,以及为什么要对齐:

 现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。 对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。 对齐的实现 通常,我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择时候目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。 但是,正因为我们一般不需要关心这个问题,所以因为编辑器对数据存放做了对齐,而我们不了解的话,常常会对一些问题感到迷惑。最常见的就是struct数据结构的sizeof结果,出乎意料。为此,我们需要对对齐算法所了解。

  作用:指定结构体、联合以及类成员的packing alignment;
  语法:#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 internal compiler stack,同时设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数值压栈;3,pop:可选参数;从internal compiler stack中删除最顶端的record;如果没有指定n,则当前栈顶record即为新的packing alignment数值;如果指定了n,则n将成为新的packing aligment数值;如果指定了identifier,则internal compiler stack中的record都将被pop直到identifier被找到,然后pop出identitier,同时设置packing alignment数值为当前栈顶的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。

原创粉丝点击