C/C++字节对齐简述

来源:互联网 发布:美工不错的网站 编辑:程序博客网 时间:2024/05/19 09:12

原址:http://blog.csdn.net/qiuqiu173/article/details/1968299

导读:
  一, 为什么会有字节对齐问题?
  字节对齐问题之主观所以存在,我想是源于程序员和CPU对内存数据访问的理解稍有不同。在我们看来,程序对内存数据的访问总是按照其大小,一个字节接着一个字节进行的,比如读取一个char型变量,系统就读去一个字节的内容,读取一个int型变量,系统就读取四个字节的内容:
  0|1|2|3|4|5|6|7……
  但是事实并非如此。并非所有CPU都是一个一个字节地来访问内存的(现在可能很少有CPU这么做,效率太低了!)。比如,CPU可以按照1,2,4,6, 8,16,32……的粒度来访问内存数据。这被称之为memory access granularity(内存访问粒度/间隔)[1],例如按照4字节来访问内存:
  0123|4567|8……
  显然,内存访问效率和数据对象的大小以及CPU的内存访问粒度相关。比如我们在32位系统上访问一个4字节int型数据对象,假设这个对象a的起始地址为0×00:
  0×00 0×01 0×02 0×03
  这时候,a的内存位置处于效率最佳情况下:
  访问粒度 CPU访问次数
  1字节 4次
  2字节 2次
  4字节 1次
  我们当然希望每次运行程序,支持4字节访问的CPU都可以一次就读/写这个int型数据。但是事实确未必一定是这样。如果a的起始地址为0×01,则:
  访问粒度 CPU访问次数
  1字节 4次
  2字节 3次
  4字节 2次
  按照2字节访问需要三次:第一次访问0×00~0×01,第二次访问0×02~0×03,第三次访问0×04~0×05,然后把三部分数据拼凑起来。
  同理,按照4字节访问需要两次:第一次访问0×00~0×03,第二次访问0×04~0×07,然后把两部分数据拼凑起来。
  显然,a的地址不符合一定的要求,就会导致效率损失。
  二,X86的字节对齐规则
  Kang Su Gatlin在其文章[2]中指出了X86上对齐的原则。32位X86CPU支持1,2,4,8字节的访问。访问L字节(2的整数倍)数据,其地址A满足A MOD L = 0。
  这样对基本的数据类型就有如下规则:
  char —— 单字节对齐,A MOD 1 = 0;可为任意的奇/偶值
  short —— 双字节对齐,A MOD 2 = 0;偶地址
  int —— 四字节对齐,A MOD 4 = 0;地址最后一位为:4,8,C
  double —— 八字节对齐, A MOD 8 = 0;地址最后一位为:0,8
  三,堆栈的对齐布局
  先用VC6运行一个例子:
  #include
  int main(int argc, char* argv[])
  {
  char c0;
  char c1;
  char c2;
  printf(”%p/n%p/n%p/n”,&c0,&c1,&c2);
  return 0;
  }
  在Debug模式下,得到类似如下的结果(具体地址值可能不同):
  0012FF7C
  0012FF78
  0012FF74
  OK,这个结果的每个地址,都符合1字节对齐,MOD 1 = 0。不过,等等,也符合四字节对齐阿!MOD 4 = 0。堆栈理论上只需要3个字节,这里却用了12个字节。
  因为VC6编译器默认对堆栈采用了4字节对齐原则。所以在不进行任何优化的情况下,一个char对象也需要占据4个字节空间。这样做的好处呢?—— 显然,无论如何,1字节和2字节对象都是符合对齐原则的。
  用RELEASE模式,把编译选项调整到MAXSPEED或者MAXSIZE,可以得到如下的输出:
  0012FF7D
  0012FF7E
  0012FF7F
  这次堆栈中就没有冗余的字节了。访问速度也不会因为字节排列而下降。
  对于32位总线而言,我认为8字节对齐可能没有什么太大意义。因为无论如何,8字节数据对象,例如double,都需要两个时钟周期的。这也就是为什么 VC6在32位平台上总是默认4字节对齐的原因:4字节对齐总是兼容单/双字节对齐方式的。关于32位总线上8字节对齐的性能影响,我么在后面的例子中可以看到。
  我们可以修改上一个例子:
  #include
  int main(int argc, char* argv[])
  {
  char c0;
  char c1;
  double d0;
  char c2;
  printf(”%p/n%p/n%p/n%p/n”,&c0,&d0,&c1,&c2);
  return 0;
  }
  Debug模式下:
  0012FF7C
  0012FF6C
  0012FF78
  0012FF74
  VC6将堆栈变量的位置做了偏移。double被放在了栈顶。这样做的好处是什么暂时不太清楚。
  四,结构体/联合体的布局
  Kang Su Gatlin指出了struct/union的布局规则,对于inter-struction/inter-union,对齐规则很简单,即 struct/union中最大的那个alignment,而这个max(alignment),又取决于编译器的设置(用参数/Zpn或者代码中用 #pragma pack(n)),即自身的对齐要求和编译器设置对齐要求二者中的最小值。
  4.1 简单的情况
  例如:
  #include “stdio.h”
  struct SA
  {
  char g;
  double k;
  };
  int main(int argc, char* argv[])
  {
  SA a;
  printf(”Size=%d/nAddr=%p/n”,sizeof(a),&a);
  return 0;
  }
  输出结果如下:
  Size=16
  Addr=0012FF70
  VC6默认采用8字节对齐,double的对齐要求也是8字节。因此这里输出大小为16,地址按照规则需符合8字节对齐,最后四位为0000。
  我们可以加上pack 限制
  #include “stdio.h”
  #pragma pack(4)
  struct SA
  {
  char g;
  double k;
  };
  int main(int argc, char* argv[])
  {
  SA a;
  printf(”Size=%d/nAddr=%p/n”,sizeof(a),&a);
  return 0;
  }
  则输出为:
  Size=12
  Addr=0012FF74
  将默认的对齐边界设置为4,则成员g仅占用4个字节,因为k在4字节边界上对齐。由于SA的成员最大对齐值为4,所以整个结构体也4字节对齐。所以地址的最后4位可以被4整除。
  4.2 嵌套的结构体成员
  看一个例子:
  #include “stdio.h”
  struct SB
  {
  int c;
  double k;
  short b;
  };
  struct SA
  {
  char g;
  SB b;
  char k;
  };
  int main(int argc, char* argv[])
  {
  SA a;
  printf(”Size=%d/nAddr=%p/n”,sizeof(a),&a);
  return 0;
  }
  输出为:
  Size=40
  Addr=0012FF58
  从这个例子可以看出嵌套结构体的规则其实也很简单。首先找到SA中成员的最大对齐边界,由于b是结构体类型,因此b的对齐边界取决于SB中的成员最大对齐边界:8。因此,SA中的成员8字节对齐。由于SB中的成员也是8字节对齐的,所以大小为8*5=40。而整个变量a的地址也要符合8字节对齐要求。
  再看看用了pack(4)的情况:
  #include “stdio.h”
  #pragma pack(4)
  struct SB
  {
  int c;
  double k;
  short b;
  };
  struct SA
  {
  char g;
  SB b;
  char k;
  };
  int main(int argc, char* argv[])
  {
  SA a;
  printf(”Size=%d/nAddr=%p/n”,sizeof(a),&a);
  return 0;
  }
  按照分析,SB的最大对齐边界:4,所以SA的对齐边界也是4,这个结果应该输出:
  Size=24
  Addr=0012FF68
  注意pack(n)的限制,作用域从起始位置开始。例如:
  #include “stdio.h”
  struct SB
  {
  int c;
  double k;
  short b;
  };
  #pragma pack(4)
  struct SA
  {
  char g;
  SB b;
  char k;
  };
  int main(int argc, char* argv[])
  {
  SA a;
  printf(”Size=%d/nAddr=%p/n”,sizeof(a),&a);
  return 0;
  }
  SB按照8字节对齐,SA按照4字节对齐,其成员b整体上满足4字节对齐即可。因此大小是8*3(SB大小)+8(SA的成员g和k的大小):
  Size=32
  Addr=0012FF60
  再看更复杂的例子:
  #include “stdio.h”
  #pragma pack(4)
  struct SB
  {
  int c;
  double k;
  short b;
  };
  #pragma pack(2)
  struct SA
  {
  char g;
  SB b;
  char k;
  };
  int main(int argc, char* argv[])
  {
  SA a;
  printf(”Size=%d/nAddr=%p/n”,sizeof(a),&a);
  return 0;
  }
  SB的对齐边界为4,因此大小为16,SA的对齐边界为2(最小值),因此大小为16+4=20:
  Size=20
  Addr=0012FF6C
  再来看看成员变量位置的颠倒会给struct带来什么结果?
  #include “stdio.h”
  struct SB
  {
  int c;
  short b;
  double k;
  };
  struct SA
  {
  char g;
  SB b;
  char k;
  };
  int main(int argc, char* argv[])
  {
  SA a;
  printf(”Size=%d/nAddr=%p/n”,sizeof(a),&a);
  return 0;
  }
  运行结果如下:
  Size=32
  Addr=0012FF60
  变量a的大小变成了32,因为SB中的double成员k的声明被放在了最后。而int成员c和short成员b各占4字节,就可以保证k处于8字节对齐状态。因此,SB的大小为16。再加上SA的另外两个成员:g和k分别占用8个字节,所以得到的Size就为32。同样,地址还是符合8字节对齐边界的要求。
  从这个例子也可以看出,通过合理安排结构体成员的声明顺序,可以减少其占用内存的大小。
  五,性能
  我使用了如下的程序来测试自己对齐给性能带来的影响:
  // Test.cpp : Test the performance of data alignment of 1, 2, 4, 8
  #include “stdafx.h”
  #include
  #include
  #include
  #include
  LARGE_INTEGER operator -(LARGE_INTEGER a, LARGE_INTEGER b)
  {
  LARGE_INTEGER reVal;
  if (a.LowPart
  class CCountTest
  {
  public:
  CCountTest()
  {
  dest = new T[128];
  origsource = new byte[10000];
  }
  ~CCountTest()
  {
  delete[] origsource;
  delete[] dest;
  }
  void CountTest(int offset)
  {
  UINT iters = 99999;
  T* source;
  LARGE_INTEGER startCount, endCount, freq;
  byte* pByte = GetAlign(origsource, offset);
  source = (T *) (pByte);
  printf(”dest = %p source = %p/n”, dest, source);
  QueryPerformanceFrequency(&freq);
  QueryPerformanceCounter(&startCount);
  {
  for (UINT x = 0; x CountTest;
  CountTest.CountTest(1);
  CountTest.CountTest(2);
  CountTest.CountTest(4);
  CountTest.CountTest(8);
  return 0;
  }
  这里我们用double类型进行测试,从一个数组中向另一个数组复制数据,测试环境:
  Intel P4 2.8G + 512 DDR + Windows2000
  输出结果如下:
  dest = 00342080 source = 00342489
  elapsed time = 0;111243
  dest = 00342080 source = 0034248A
  elapsed time = 0;111481
  dest = 00342080 source = 0034248C
  elapsed time = 0;74270
  dest = 00342080 source = 00342488
  elapsed time = 0;69388
  可以看到,1字节和2字节对齐是,对double数据的存取远比4或者8字节对齐来的低效!但是4字节和8字节对齐似乎没有对double的存取造成多大的效率差异。实事上,实际运行中最后两个时间值得大小关系并不唯一确定,这里只是一次运行的结果。这可能说明,在32位平台上,double类型数据对象的 8字节对齐,其实并没有比4字节对齐来的效率更高。毕竟32位总线不可能一次传输完8个字节的数据。这也符合前面我们提到的栈字节对齐中为何double 型变量并不一定被系统安排为8字节对齐的情况。
  六,尾巴
  个人觉得,其实大多数情况下,在Windows上开发,我们都不用关心堆栈和结构体的成员声明顺序,以及是否通过填充冗余字节来达到地址对齐。因为编译器通过优化选项可以自动进行处理。即使浪费了一些内存,在大多数应用中,恐怕也无关痛痒。只是不要相当然的用一个常数来替代sizeof函数就好。
  另外就是程序移植到其他的平台上时,可能目标平台并不支持非对齐式的数据访问。编译器也不会自动纠正对齐问题。这样一来,如果想编写移植性好的程序,就要非常小心了。
  参考文章:
  1,Jonathan Rentzsch : Data alignment: Straighten up and fly right (http://www-128.ibm.com/developerworks/library/pa-dalign/ )
  2,Kang Su Gatlin :Windows Data Alignment on IPF, x86, and x64 (http://msdn.microsoft.com/library/en-us/dv_vstechart/html/vcconwindowsdataalignmentonipfx86×86-64.asp?frame=true)
  续:
  MSN的C++ Group里看到一些讨论,关于栈变量放置的问题:
  我们可以修改上一个例子:
  #include
  int main(int argc, char* argv[])
  {
  char c0;
  char c1;
  double d0;
  char c2;
  printf(”%p/n%p/n%p/n%p/n”,&c0,&d0,&c1,&c2);
  return 0;
  }
  Debug模式下:
  0012FF7C
  0012FF6C
  0012FF78
  0012FF74
  VC6将堆栈变量的位置做了偏移。double被放在了栈顶。这样做的好处是什么暂时不太清楚。
  ——–double放在栈顶,因为有些编译器可以将同一类型的变量连续放置(好处仍然不清楚)。如果要强行安排这些变量的放置顺序,可以用一个struct:
  struct A{
  char c0;
  char c1;
  double d0;
  char c2;
  }
  这样就OK了,当然前提是不能让编译器优化struct内部的放置顺序。