首页 > 技术文章 > 字节对齐1

zjoch 2016-10-27 01:54 原文

一、快速理解
1、什么是字节对齐?
    在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构中的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
    为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除。

2、字节对齐有什么作用?
    字节对齐的作用不仅是便于CPU的快速访问,同时合理的利用字节对齐可以有效地节省存储空间。
    对于32位机来说,4字节对齐能够使CPU访问速度提高,比如说一个long类型的变量,如果跨越了4字节边界存储,那么CPU要读取两次,这样效率就低了。但是在32位机中使用1字节或者2字节对齐,反而会使变量访问速度降低。所以这要考虑处理器类型,另外还得考虑编译器的类型。在VC中默认是4字节对齐的,GNU gcc也是默认4字节对齐。

3、更改C编译器的缺省字节对齐方式
    在缺省的情况下,C编译器为每一个变量或是数据单元按其自然对齐条件分配空间。一般地,可以通过下面的方法改变缺省的对齐条件:
    (1)使用伪指令#pragma pack(n),C编译器将按照n个字节对齐;
    (2)使用伪指令#pragma pack(),取消自定义字节对齐方式;

二、字节对齐对程序的影响
    先让我们看看几个例子吧(32位,x86环境,gcc编译器):
    设结构体如下定义:

  1. struct A
  2. {
  3.     int a;
  4.     char b;
  5.     short c;
  6. };
  7. struct B
  8. {
  9.     char b;
  10.     int a;
  11.     short c;
  12. };

    现在已知32位机器上各种数据类型的长度如下:
    char: 1(有符号无符号同)
    short:2(有符号无符号同)
    int:4(有符号无符号同)
    long:4(有符号无符号同)
    float:4    double:8
    那么上面的两个结构体大小如何呢?
    结果是:
    sizeof(struct A) = 8
    sizeof(struct B) = 12
    结构体A中包含了4个字节长度的int一个,1字节长度的char一个核2字节长度的short型数据一个,B也一样;按理说,A、B大小应该是相同的,都是7个字节。
    之所以出现上面的结果是因为编译器要对数据成员在空间上进行对齐。上面是按照编译器的默认设置进行对齐的结果,那么我们是不是可以改变编译器的这种默认设置呢?当然可以,例如:

  1. #pragma pack(2) //指定按2字节对齐
  2. struct C
  3. {
  4.     char b;
  5.     int a;
  6.     short c;
  7. };
  8. #pragma pack()    //取消指定对齐,恢复缺省对齐
  9. sizeof(struct C) = 8
  10. 修改对齐方式为1:
  11. #pragma pack(1) //指定按1字节对齐
  12. struct D
  13. {
  14.     char b;
  15.     int a;
  16.     short c;
  17. };
  18. #pragma pack()    //取消指定对齐,恢复缺省对齐
  19. sizeof(struct D) = 7

    后面在介绍#pragma pack()的作用。

三、编译器是按照什么样的原则进行对齐的
    先看四个重要的基本概念:
    (1)数据类型自身对齐值
    对于char型数据,其自身对齐值为1,对于short型为2,对于int、float、double类型,其自身对齐值为4,单位字节。
    (2)结构体或者类的自身对齐值
    其成员中自身对齐值最大的那个值。
    (3)指定对齐值
    #pragma pack(value)时的指定对齐值value。
    (4)数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
    有了这些值,我们可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。
    有效对齐N,就是表示”对齐在N上“。也就是说该数据的”存放起始地址%N = 0“。而数据结构中的数据变量都是按定义的先后顺序来排列的。第一个数据变量的起始地址就是数据结构的其实地址。结构体的成员变量要对齐排列,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效地址值的整数倍,结合下面例子理解)。这样就不难理解上面几个例子的值了。
    例子分析,以B为例。

  1. struct B
  2. {
  3.     char b;
  4.     int a;
  5.     short c;
  6. };

    假设B从地址空间0x0000开始排放。该例子中没有定义指定对齐值,在笔者环境下,该值默认为4。第一个成员变量b的自身对齐值是1,比指定或者默认指定对齐值4小,所以其有效对齐值为1,所以其存放地址0x0000符合0x0000%1=0。第二个成员变量a,其自身对齐值为4,所以有效对齐值也为4,所以只能存放在起始地址为0x0004到0x0007这四个连续的字节空间中,复核0x0004%4=0,且紧靠第一个变量。第三个变量c,自身对齐值为2,所以有效对齐值也是2,可以存放在0x0008到0x0009这两个字节空间中,符合0x0008%2=0。所以从0x0000到0x0009存放的都是B内容。再看数据结构B的自身对齐值为其变量中最大对齐值(这里是b)所以就是4,所以结构体的有效对齐值也是4。根据结构体圆整的要求,0x0009到0x0000=10字节,(10+2)%4=0。所以0x0000A到0x000B也为结构体B所占用。故B从0x0000到0x000B共有12个字节,sizeof(struct B)=12;其实如果就这一个就来说它已将满足字节对齐了,因为它的起始地址是0,因此肯定是对齐的,之所以在后面补充2个字节,是因为编译器为了实现结构数组的存取效率,试想如果我们定义了一个结构B的数组,那么第一个结构起始地址是0没有问题,但是第二个结构呢?按照数组的定义,数组中所有元素都是紧挨着的,如果我们不把结构的大小补充为4的整数倍,那么下一个结构的起始地址将是0x0000A,这显然不能满足结构的地址对齐了,因此我们要把结构补充成有效对齐大小的整数倍。其实诸如:对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,这些已有类型的自身对齐值也是基于数组考虑的,只是因为这些类型的长度已知了,所以他们的自身对齐值也就已知了。
    同理,分析上面例子C。

  1. #pragma pack (2) /*指定按2字节对齐*/
  2. struct C
  3. {
  4.     char b;
  5.     int a;
  6.     short c;
  7. };
  8. #pragma pack () /*取消指定对齐,恢复缺省对齐*/

    第一个变量b的自身对齐值为1,指定对齐值为2,所以,其有效对齐值为1,假设C从0x0000开始,那么b存放在0x0000,符合0x0000%1=0;第二个变量,自身对齐值为4,指定对齐值为2,所以有效对齐值为2,所以顺序存放在0x0002、0x0003、0x0004、0x0005四个连续字节中,符合0x0002%2=0。第三个变量c的自身对齐值为2,所以有效对齐值为2,顺序存放在0x0006、0x0007中,符合0x0006%2=0。所以从0x0000到0x00007共八字节存放的是C的变量。又C的自身对齐值为4,所以C的有效对齐值为2。又8%2=0,C只占用0x0000到0x0007的八个字节。所以sizeof(struct C)=8。
    
四、如何修改编译器的默认对齐值
    (1)在VC IDE中,可以修改:【Project】->【Setting】,C/C++选项卡Category的Code Generation选项的Struct Member Alignment中修改,默认是8字节;
    (2)在编码时,可以这样动态修改:#pragma pack。注意:是pragma而不是progma。

五、针对字节对齐,我们在编程中如何考虑    
    如果在编程的时候要考虑节约空间的话,那么我们只需要假定结构的首地址是0,然后各个变量按照上面的原则进行排列即可,基本的原则就是把结构中的变量按照类型大小从小到大声明,尽量减少中间的填补空间。还有一种就是为了以空间换取时间的效率,我们显示的进行填补空间进行对齐。比如:有一种使用空间换时间做法是显式的插入reserved成员:

  1. struct A
  2. {
  3.     char a;
  4.     char reserved[3];//使用空间换时间
  5.     int b;
  6. };

    reversed成员对我们程序没有什么意义,它只是起到填补空间以达到字节对齐的目的,当然即使不加这个成员,通常编译器也会给我们自动填补对齐,我们自己加上它只是起到显式的提醒作用。

六、字节对齐可能带来的隐患
    代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。例如:
    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,因为它们要求必须字节对齐。

七、如何查找与字节对齐方面的问题
    如果出现对齐或者赋值问题,首先查看:
    (1)编译器的big/little端设置;
    (2)看这种体系本身是否支持对齐访问;
    (3)如果支持,看设置了对齐与否;如果没有,则看访问时需要加某些特殊的修饰来标志其特殊访问操作;

八、引用
    blog.csdn.net/xuegao007/article/details/1708349

推荐阅读