pack 与字节对齐

来源:互联网 发布:mac的appstore打不开 编辑:程序博客网 时间:2024/03/29 00:35

计算机组成原理教导我们这样有助于加快计算机的取数速度,否则就得多花指令周期了。为此,编译器默认会对结构体进行处理(实际上其它地方的数据变量也是如此),让宽度为2的基本数据类型(short等)都位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上,以此类推。这样,两个数中间就可能需要加入填充字节,所以整个结构体的sizeof值就增长了。
unsigned char *p1;
unsigned long *p2;
p1=(unsigned char *)0x801000;
p2=(unsigned long *)0x810000;
请问p1+5=  ;
p2+5=  ;
答案:0x801005(相当于加上5位) 0x810014(相当于加上20位);

字节对齐的细节和编译器实现相关,但一般而言,满足三个准则:

1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;

2) 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);

3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。

结构体某个成员相对于结构体首地址的偏移量可以通过宏offsetof()来获得,

程序员通常倾向于认为内存就像一个字节数组.在C及其衍生语言中,char * 用来指代"一块内存",甚至在JAVA中也有byte[]类型来指代物理内存.
然而,你的处理器并不是按字节块来存取内存的.它一般会以双字节,四字节,8字节,16字节甚至32字节为单位来存取内存.我们将上述这些存取单位称为内存存取粒度.
但从地址1读取数据时由于地址1没有和处理器的内存存取边界对齐,处理器就会做一些额外的工作.地址1这样的地址被称作非对齐地址.由于地址1是非对齐的,双字节存取粒度的处理器必须再读一次内存才能获取想要的4个字节,这减缓了操作的速度.
目前的开发普遍比较重视性能,所以对齐的问题,有2种不同的处理方法:
1) 有一种使用空间换时间做法是显式的插入reserved成员:
struct A{
char a;
char reserved1[3]; //使用空间换时间
int b;

}a;
2) 随便怎么写,一切交给编译器自动对齐。
还有一种将逻辑相关的数据放在一起定义
代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。下面举个例子:
unsigned int i = 0x12345678;
unsigned char *p=NULL;
unsigned short *p1=NULL;

p=&i;
*p=0x00;
p1=(unsigned short *)(p+1);
*p1=0x0000;
最后两句代码,从奇数边界去访问unsignedshort型变量,显然不符合对齐的规定。
在x86上,类似的操作只会影响效率,但是在MIPS或者sparc上,可能就是一个error

32 位内存中除了结构体中成员变量之外的基本类型的开始的首地址最低两位都是0

基本类型数据对齐就是数据在内存中的偏移地址必须是一个字的倍数,以提高读取数据时的性能。

为了对齐数据,必须在上个数据结束和下个数据开始处插入一些字节,这就是结构体数据对齐。

不进行对齐的影响,例如int a的地址是0x00fffff3,则其字节分布在0x00fffff30x00fffff6空间内,为了读取这个intcpu必须对 0x00fffff00x00fffff4进行两次内存读取,并处理得出的中间结果。两次内存访问将会浪费大量的时间,因为内存访问的速度远小于CPU处理指令的速度。

什么是对齐?以及为什么要对齐:现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

VC对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。

VC中提供了#pragma pack(n)来设定变量以n字节对齐方式。

n字节对齐就是说变量存放的起始地址的偏移量有两种情况:第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数。

下面举例说明其用法。

#pragma pack(push) //保存对齐状态

#pragma pack(4)//设定为4字节对齐

struct test

{

char m1;

double m4;

int m3;

};

#pragma pack(pop)//恢复对齐状态

以上结构体的大小为16,下面分析其存储情况,首先为m1分配空间,其偏移量为0,满足我们自己设定的对齐方式(4字节对齐),m1大小为1个字节。接着开始为m4分配空间,这时其偏移量为4,需要补足3个字节,这样使偏移量满足为n=4的倍数(因为sizeof(double)大于4,m4占用8个字节。接着为m3分配空间,这时其偏移量为12,满足为4的倍数,m3占用4个字节。这时已经为所有成员变量分配了空间,共分配了16个字节,满足为n的倍数。如果把上面的#pragma pack(4)改为#pragma pack(8),那么我们可以得到结构的大小为24

结构体的内存地址对齐

 

 注意:结构体本身必须是4字节对齐的,而其成员变量则处理规则如下。

 

  以下是MicrosoftGNUx86架构32位系统的结构体成员的默认对齐方式:

 

 char  1字节对齐

 

 short  2字节对齐

 

 int      4字节对齐


 float  4字节对齐


 double       windows8字节对齐,      linux4字节对齐

当某一个成员后边的成员变量要求的地址对齐较大,则应该填入一些字节。且总的结构体大小为最大对齐的倍数,因此最后可能还要填充一些字符。

 因为上述结构体对齐的原因,将结构体成员按照大小递增/递减方式排序,可以减少结构体占用的空间大小。而这样同时使得对整个结构体的存取的效率变高了(占用小,整个的访问次数可以降低)。

数组或者结构体数组只要保证首地址对齐,其中元素不要求对齐,因为对于数组,其中任何元素都可以在12次内存访问中获取,对于结构体数组,因为结构体内部是按照上述方式填充,则也不需要结构体数组的元素都地址对齐。

typedef struct m_struct{

  int i;   // size 4

  short j;   // size 2

  double k;   // size 8

}ms;结构体大小为16字节.

#pragma pack(n)

    将当前字节对齐值设为 n.

 #pragma pack()

    将当前字节对齐值设为默认值(通常是8) .

例如,下面的结构各成员空间分配情况:

struct test

{

    char x1;

    short x2;

    float x3;

    char x4;

};

 

结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2short类型,其起始地址必须2字节对界,因此,编译器在x2x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上(如果没落到其自然边界地址上则照样要填充字节)在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。

 

#pragma pack(8)

 

struct s1{

short a;

long b; //4字节对齐     ----16

};

 

struct s2{

char c;

s1 d;

long long e;      ----48

};

 

#pragma pack()

 

1.sizeof(s2) = ?

2.s2c后面空了几个字节接着是d?

 

结果如下:

 

sizeof(S2)结果为24.

成员对齐有一个重要的条件,即每个成员分别对齐.即每个成员按自己的方式对齐.

也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐.其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐.并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节.

S1,成员a1字节默认按1字节对齐,指定对齐参数为8,这两个值中取1,a1字节对齐;成员b4个字节,默认是按4字节对齐,这时就按4字节对齐,所以sizeof(S1)应该为8;

S2,cS1中的a一样,1字节对齐,d是个结构,它是8个字节,它按什么对齐呢?

对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个,S1的就是4.所以,成员d就是按4字节对齐.成员e8个字节,它是默认按8字节对齐,和指定的一样,所以它对到8字节的边界上,这时,已经使用了12个字节了,所以又添加了4个字节的空,从第16个字节开始放置成员e.这时,长度为24,已经可以被8(成员e8字节对齐)整除.这样,一共使用了24个字节.

                          a    b

S1的内存布局:11**,1111,

                         c    S1.a S1.b     d

S2的内存布局:1***,11**,1111,****11111111

 

这里有三点很重要:

1.每个成员分别按自己的方式对齐,并能最小化长度

2.复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度

3.对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐

 

补充一下,对于数组,比如:

char a[3];这种,它的对齐方式和分别写3char是一样的.也就是说它还是按1个字节对齐.

如果写: typedef char Array3[3];

Array3这种类型的对齐方式还是按1个字节对齐,而不是按它的长度.

不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个.

 

 

原创粉丝点击