字节对齐问题

1、解释

       字节(Byte)是计算机信息技术用于计量存储容量和传输容量的一种计量单位,一个字节等于8位二进制数,在UTF-8编码中,一个英文字符等于一个字节。字节按照一定规则在空间上排列就是字节对齐。现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特 定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。

2、作用和原因

        各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那 么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。

例子
       比如在32位cpu下,假设一个整型变量的地址为0×00000004,那它就是自然对齐的。假设上面整型变量的地址不是自然对齐,比如为0×00000002,则CPU如果取它的值的话需要 访问两次内存,第一次取从0×00000002-0×00000003的一个short,第二次取从0×00000004-0×00000005的一个 short然后组合得到所要的数据,如果变量在0×00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为 char,然后组合得到整型数据。而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对 齐的数据会发生错误,举个例: 

  char ch[8]; 
  char *p = &ch[1]; 
  int i = *(int *)p; 

运行时会报segment error,而在x86上就不会出现错误,只是效率下降。 

3、正确处理字节对齐

对于标准数据类型,它的地址只要是它的长度的整数倍就行了,而非标准数据类型按下面的原则对齐: 
   
      数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。
      结构体: 结构体中每个数据类型都要对齐。(每个编译环境下,都会默认指定一个对齐的值,每个结构体都有一个自身对齐的值,这个值是结构体内部所有字段中占字节最长的那个字段的尺寸值)。 
比如有如下一个结构体: 

struct tagA
{
<span style="white-space:pre">    </span>char cNum;
<span style="white-space:pre">    </span>int iAge;
<span style="white-space:pre">    </span>char cCount[5];
}A1;
struct  tagA  A2

      这个结构体int变量占字节最长(数组按基本类型算不能按数组长度算,char cCount[5]还是1)---4个字节,所以这个结构体的自身对齐的值是4,程序在编译分配内存的时候会从默认指定的对齐值和具体该结构体自身对齐值2个值中选出较少的那个值来分配内存,是我们sizeof(A2)会得到长度为16,而不是9。
在VC中默认值为8,在Keil中默认值是1,所以A1在VC中分配内存是选用的是min(8,4),在Keil中选用的是min(1,4)来分配A1这个结构体。姑且称这个长度为L
编译的时候,首先选结构体首地址也不是乱选的,首先在内存中选到能够被结构体内部最长那个字段整除的地址
    (VC中具体到这个例子就是4的倍数的地址,Keil中就是2的倍数的地址),然后开始依次排字段,结构体内部前面的字段排在靠近该结构体的地址的前面,先排cNum,接着iAge,最后排cCount[5]。排的时候又有规则,对于基本数据类型长度少于等于L的字段必须排在能被自身长度整除的地址上不连续的部分用空字节补齐(具体到这里iAge这个字段为4,必须排在能被4整除的地址上,但是首地址也是被4整除,cNum只需要1个字节,这个时候在cNum后面就需要补3个空字节,这些空字节就是需要浪费的内存,但是用这些内存换取些效率还是值得的),对于基本数据类型长度大于L的字段必须排在能被L整除的地址上不连续的部分用空字节补齐(具体到这里,如果在Keil环境下L=1,整型字段iAge占2个字节,所以Keil排在能被1整除的任意位置就可以了),最后排完所有字段后如果首地址到最后字段的末尾的总字节数不是L的整数倍,需要用空字节补齐。


这就是字节对齐的所有规则。
另外需要补充一点,虽然编译环境默认有个对齐值,但是可以通过

#pragma pack(2)
struct a
{
char ch;
int i;
short st;
char ch1[3];
}ta;

#pragma pack 2个预处理命令重新设置,#pragma pack(x)表示下面的部分在分配内存时默认值改为x
#pragma pack 表示撤销#pragma pack(x)操作。

4、图文讲解

1、充分考虑四字节对齐,可以节省存储空间

         typedef struct tagAAA{
               char name[10];
               long sno;
               char sex;
               float score[4]; 
         }AAA;
         typedef struct tagBBB{
               char name[10];
               char sex;
               long sno;
               float score[4];
        }BBB;

       在VC下,调试,可以很容易看出来,AAA占的存储空间为36,BBB占的存储空间为32。原因很简单,在四字节对齐的情况下,按四个字节为单位分配存储空间,如果不足,会自动补充,本次分配不足以存放下面的变量时,会重新分配空间。
        AAA:
              |name[0]|name[1]|name[2]|name[3]|
              ------------------------------------
              |name[4]|name[5]|name[6]|name[7]|
              ------------------------------------
              |name[8]|name[9]|             |                   |
                        ----------由于剩下的两个字节不足以存放sno(long占四个字节),所以重新分配
              ------------------------------------
              |                        sno                                       |
                          ----------long变量占四个字节,32bits
              ------------------------------------
              |sex        |    自动填充                                |
                          ----------剩余三个字节的空间,不足以重放一个float变量,因此重新分配
              ------------------------------------
              |                  score[0]                                     |
              ------------------------------------
              |                  ..........                                        |
              ------------------------------------
              |                  score[3]                                     |
              ------------------------------------
               由此可以轻易的计算出,AAA占36个字节,同理,很容易计算出BBB占32个字节空间。
            
         2、字节对其的情况下,可以更高效的访问
               假设一个结构体的数据如下存储:
              -----------------------------------------------------
              |        12        |       34       |        56        |        78         |   -----------(A)
              -----------------------------------------------------  
              ----------------------------------------------------- 
              |        XX        |       YY       |        12       |         34        |   -----------(B)
              -----------------------------------------------------  
              |        56        |        78       |       XX       |         YY        |
              在A情况下,一次性读取数据成功,但是,在B情况下,需要读取数据两次,由此,可看出效率的差异。
          一般情况下,字节对齐遵从系统字节数与要求的对齐字节数相比,最小原则,即:假设要求按八字节对齐,但是系统为32位系统,则按照4字节对齐。在四字节对齐时,局部会按照2字节对齐,如:
             struct tagAAA
             {
                     char a;
                     short b;
                     char c;
             }AAA;
该结构体占据的空间为8字节而不是4字节,原因就是:
                    -----------------------------------
                    |    a     |          |             b              |
                    -----------------------------------
                    |   c      |                                      |
而不是:
                    ------------------------------------ 
                    |    a     |          b          |        c         |
                    ------------------------------------
其原因就是局部会以2字节对齐。
 
***********************************************

5、什么时候需要设置对齐 

      在设计不同CPU下的通信协议时,或者编写硬件驱动程序时寄存器的结构这两个地方都需要按一字节对齐。即使看起来本来就自然对齐的也要使其对齐,以免不同的编译器生成的代码不一样. 

6、针对字节对齐,我们在编程中如何考虑

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

struct A
{
  char a;
  char reserved[3];    //使用空间换时间
  int b;
};

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

7、字节对齐可能带来的隐患

代码中关于对齐的隐患,很多是隐式的。比如在强制类型转换的时候。例如:

  unsigned int i = 0x12345678;
  unsigned char *p = NULL;
  unsigned short *p1 = NULL;
  p=&i;
  *p=0x00;
  p1=(unsigned short*)(p+1);
  *p1=0x0000;

  最后两行代码,从奇数边界去访问unsigned short型变量,显然不符合对齐的规定。在X86上,类似的操作只会影响效率,但是在MIPS或者sparc上,可能就是一个error,因为它们要求字节必须对齐。

8、如何查找与字节对齐方面的问题

如果出现对齐或者赋值问题首先查看
  1.编译器设置的对齐值
  2.看这种体系本身是否支持非对齐访问
  3.如果支持看设置了对齐与否,如果没有则看访问时需要加某些特殊的修饰来标志其特殊访问操作。

原文地址:https://www.cnblogs.com/jiangzhaowei/p/9240398.html