新闻中心

字节那些事儿

作者: 时间:2016-07-29 来源:网络 收藏

  7、 如何控制对齐

本文引用地址://m.amcfsurvey.com/article/201607/294782.htm

  控制程序的对齐行为是一个与编译器相关的工作。以下编译指示( directive )被许多编译器认可:

  #pragma pack(n)

  #pragma pack()

  任何处于这两个编译指示语句之间的数据结构,将采用 n的数据对齐方式。 n 是一个可以指定的数字,取值范围请参阅所使用编译器的文档,通常都会取值为 2 的幂。现代编译器在对程序进行编译时,处于效率方面的考虑,会对数据结构的内存布局使用一个默认的字节对齐值,这个值一般都可以在命令行上显式指定。如果要在一个头文件 / 源文件中对特定的部分指定对齐属性,则需要上述的编译指示。结束指示的写法在某些编译器或者平台下需要写成:

  #pragma pack(pop)

  我们用一个例子来看一下这两个指示的实际效用,看它究竟是如何影响数据的内存排列的。假定我们有如下的数据结构定义:

  struct S1

  {

  int i;

  char c;

  short s;

  };

  struct S2

  {

  char c;

  int i;

  short s;

  };

  这两个结构的成员看起来是一样的,只不过换了一下顺序而已。我们使用 sizeof() 操作符来测量各自占用多少字节(除非特别指出,均在 32 位平台上,并认为 int 占用 4 字节, char 占用 1 字节, short 占用 2 字节)。答案似乎不可思议, sizeof(S1) 的结果是 8 ,而 sizeof(S2) 却是 12 。差异是怎么来的呢?原因就在于编译器缺省的字节对齐设定在发生作用。

  这里需要引入以下概念和规则:

  概念及规则一,原生数据类型自身对齐值。原生数据类型即是 C/C++ 直接支持的数据类型,也可以称为内建(built in )数据类型。它们的自身对齐值分别为: char 为 1 , short int 为 2 , int 、 float 、 double 等为 4 ,不受符号位(即正负)的影响。

  概念及规则二,用户数据类型自身对齐值。用户数据类型即由程序员定义的类、结构、联合等,也叫抽象数据类型( ADT )。它们的自身对齐值等同于为其成员的对齐值中的最大值。

  概念及规则三,用户指定对齐值。程序员在编译器命令行上的指定值,或者在 pragma pack 编译指示中指定的值,对最终数据的影响取就近原则(显然 pragma pack 指示会覆盖命令行的指定)。

  概念及规则四,有效对齐值。取数据类型的自身对齐值与用户指定对齐值中的较小值。此值一旦决出,则会影响到数据在内存中的布局。一个有效对齐值为 n ,表示以下事实:相关数据在内存中存放时,其起始地址的值必须可以被 n 整除 。

  根据以上四条,可以很圆满地解释 S1 和 S2 的大小不同这一现状。由于没有使用 pragma pack 指示,那么编译器(在我的测试环境下)会采用缺省的对齐值 4 。假设 S1 或者 S2 的实例将从地址 0x0000 处开始。

  在 S1 中,第一个成员 i 的自身对齐值为 4 ,指定对齐值(尽管是缺省的)也是 4 ,同时 0x0000 这一地址符合被 4 整除的要求,因此, i 将占据 0x0000 到 0x0003 的四个字节,下一个可用地址值为 0x0004 ;接下来的成员c 的数据类型为 char ,自身对齐值为 1 ,指定对齐值为 4 ,取较小者仍然是 1 , 0x0004 符合被 1 整除的要求,因此 c 将占据 0x0004 处的一个字节,下一个可用地址值为 0x0005 ;最后的一个成员 s 数据类型为 short ,自身对齐值为 2 ,指定对齐值为 4 ,有效对齐值取 2 ,但是地址 0x0005 不能符合被 2 整除的要求,因此编译器作相应调整,向后移动到最近的满足要求的地址处,即 0x0006 , s 将占用 0x0006 和 0x0007 处的两个字节,由此导致S1 的大小为 8 。

  在地址 0x0005 处的一个字节,习惯上称之为填充数据( padding )。

  同理可以轻易推出 S2 结构的大小确实是 12 。是这样吗?不是的。实际动手的结果应该是 10 。那么 12 应该作何解释?

  我们来设想一个场景,程序员用 new 或者 malloc 分配一个 S2 的数组。不用多,假定有两个元素,而地址0x0000 处正好有空闲的内存可以满足这一内存分配请求。我们都知道,在 C/C++ 语言中,数组的元素是紧邻排放的。也就是说,后一个元素的起始地址应该正好等于前一个元素的起始地址,并加上元素的大小。我们来检视一下S2 的情况,它的元素大小为 10 ,它的有效对齐值是 4 (请参阅概念及规则二),这表示任何一个 S2 结构的起始地址都应该位于 4 的整数倍处。现实的情况是,第一个元素的起始地址是 0x0000 ,第二个元素的起始地址变成了0x000A ,而后者的数值不能满足被 4 整除的要求。正是为了解决这一情况,编译器为 S2 结构在结尾处也增加了两个字节的填充,从而满足各个条件的限定。

  pragma pack 指示非常有效,使用也比较普遍,但是对于平台,它有一些力所不及的地方,我们再来看一个例子。仍然用 S2 ,这一次,我们强制把它的字节对齐设定为 1 ,并同时定义了 S2 的一个全局变量 s2 。也即:

  #pragma pack(1)

  struct S2

  {

  char c;

  int i;

  short s;

  } s2;

  #pragma pop()

  然后,在某处具有如下的数据访问:

  int i = s2.i;

  这条看上去稀松平常的语句很可能不能如所希望的那样执行。因为对于 i 的访问其前提应该是 i 的起始地址是 4的倍数(注意,这个不是对齐规则的约束结果,而是CPU 的数据访问规则的约束结果),但强行指定的 1 字节对齐则导致 i 的起始地址是一个奇数。

  RVCT 编译器为此做了特别的努力,引入了 __packed 关键字。此关键字应用到用户定义数据结构上会导致该结构的内存布局取得与 pragma pack(1) 等同的效果,但是,更进一步地,编译器会把对该结构中成员的访问作适当的处理,发现不对齐的访问则会翻译为调用适当的保证数据正确性的函数。此关键字也可以应用到指针上,以保证经由指针对目标对象的访问也采用保守方式。可以预料到的是,此关键字的使用会降低代码执行的效率,所以需要慎用,一个很典型的使用场景是移植其他平台的代码时。以下是一些使用了此关键字的定义示例:

  typedef __packed struct

  {

  char x; // 所有成员都会被 __packed 修饰

  int y;

  } X; // 5 字节的结构,自身对齐值 = 1

  int f(X* p)

  {

  return p->y; // 执行一个非对齐的读取操作

  }

  typedef struct

  {

  short x;

  char y;

  __packed int z; // 仅 __pack 本成员,此用法仅适用于整型

  char a;

  } Y; // 8 字节结构,自身对齐值 = 2(请思考原因)

  int g(Y* p)

  {

  return p->z + p->x; // 仅对 z 执行非对齐读取操作

  }

  需要注意的是, GCCE 编译器没有实现类似的努力,它有一个和对齐有关的关键字: __attribute__ (packed)),该关键字的功效与 pragma pack(1) 类似。

  8、 思考 / 练习题

  a) 位( bit )在字节中的排列,应该也有类似字节序那样的问题,为什么没有?

  b) 自己写几个结构,根据规则推断其大小,然后写代码验证

  c) 请查阅 RVCT 的相关文档,学习 __align 关键字的含义和用法

  d) 了解微软公司针对 Windows Mobile 平台的编译器是否也具有帮助程序员自动解决对其访问的机制

  9、 参考资料

  a) 《编程卓越之道》,第一卷

  b) 《 RealView Compilation Tools - Compiler and Libraries Guide 》

  c)Information Center

  d) http://blog.csdn.net/xhfwr/archive/2006/07/23/963793.aspx


上一页 1 2 下一页

关键词:字节ARM

评论


相关推荐

技术专区

关闭