2.7 内存布局对齐

稍微转换一下话题_……

假设有下面这样的一个结构体:

  1. typedef struct {
  2. int int1;
  3. double double1;
  4. char char1;
  5. double double2;
  6. } Hoge;

在我的环境中,sizeof(int)的结果为 4,sizeof(double)的结果为 8,随便说一下,根据 C 标准,sizeof(char)的结果必定为 1*。敢问阁下,这个结构体的尺寸是多大?

* 比如,即使 char 为 9 bit 的处理环境(如果真的存在),sizeof(char)值也是 1。标准就是这么定义的。

4 + 8 + 1 + 8 = 21 个字节——几乎在所有的情况下,这个答案都是错误的。在我的处理环境中,答案是 24 个字节。

还是通过程序做个实验吧(参照代码清单 2-8)。

声明一个 Hoge 类型的变量,然后将各成员的地址输出*

* 如果需要获得结构体成员距离初始位置的偏移量,一般使用 stddef.h 中定义的宏 offsetof()。使用这个宏,不需要声明哑变量(dummy )也可以获取偏移量。

代码清单 2-8 alignment.c

  1. 1: #include <stdio.h>
  2. 2:
  3. 3: typedef struct {
  4. 4: int int1;
  5. 5: double double1;
  6. 6: char char1;
  7. 7: double double2;
  8. 8: } Hoge;
  9. 9:
  10. 10: int main(void)
  11. 11: {
  12. 12: Hoge hoge;
  13. 13:
  14. 14: printf("hoge size..%d\n", sizeof(Hoge));
  15. 15:
  16. 16: printf("hoge ..%p\n", &hoge);
  17. 17: printf("int1 ..%p\n", &hoge.int1);
  18. 18: printf("double1..%p\n", &hoge.double1);
  19. 19: printf("char1 ..%p\n", &hoge.char1);
  20. 20: printf("double2..%p\n", &hoge.double2);
  21. 21:
  22. 22: return 0;
  23. 23: }

我的环境中的运行结果如下:

  1. hoge size..24
  2. hoge ..0xbfbfd9d0
  3. int1 ..0xbfbfd9d0
  4. double1..0xbfbfd9d4
  5. char1 ..0xbfbfd9dc
  6. double2..0xbfbfd9e0

观察运行结果可以发现,char1 的后面空出来一块。

这是因为根据硬件(CPU)的特征,对于不同数据类型的可配置地址受到一定限制。或者,即使可以配置,某些 CPU 的效率也会降低。此时,编译器会适当地进行边界调整(布局对齐),在结构体内插入合适的填充物。

2.7 内存布局对齐 - 图1

图 2-16 布局对齐

根据这个实验,在我的环境中,intdouble 被配置在 4 的倍数的地址上。

布局对齐处理有时候也在结构体的末尾进行,这是由于有时候需要构造结构体数组的缘故。针对这样的结构使用 sizeof 运算符,会返回包含末尾对齐字节的结构体长度。将结果和元素个数相乘,就可以获得整个数组的大小。

此外,malloc()会充分考虑到各种类型的长度,返回调整后最优化的地址。局部变量等也会被配置到优化调整后的地址上。

布局对齐操作是根据 CPU 的情况进行的。因此,根据 CPU 的不同,布局对齐填充的方式也不同。在我的环境中,double 可以被配置在 4 的倍数的地址上,但在很多 CPU 上,double 只能被配置在 8 的倍数的地址上。

偶尔,也会有人比较讨厌布局对齐方式对硬件的依赖,通过手工调整边界来提高可移值性。

  1. typedef struct {
  2. int int1;
  3. char pad1[4]; ←通过手工填充
  4. double double1;
  5. char char1;
  6. char pad2[7]; ←这里也是
  7. double double2;
  8. } Hoge;

可是,这么做究竟有什么作用呢?

即使不这么做,编译器也会根据 CPU 的情况帮我们进行适当的边界调整。如果只是引用成员名,就根本没有必要去理会布局对齐方式。

如果需要将结构体照原样(通过 fwrite())输出到文件中,由于 CPU 的不同,在其他机器上想要读取这个结构体的时候,对齐方式的不同可能导致问题。那么,通过手工方式调整边界,说不定某台机器上输出的数据,也能被其他机器读取。可是无论怎样,这只不过是偶尔才可以拿出来说的例子。

在上面的例子中,pad1 的尺寸为 4,pad2 的尺寸为 7。究竟这些数字是怎么冒出来呢?连标准也不能保证 sizeof(int)为 4,sizeof(double)为 8。将这些数字直接写在程序中,还说什么“为了提高可移植性”……

也就是说,手工插入填充物的方法,即使可以让不同机器的数据交换成为可能,也只不过是敷衍逃避。原型开发也许会允许使用这种方式,但是如果在现实中进行数据交换,将结构体按照原样写入到文件的方式本身就是个错误

另外,即使是 sizeof(int)为 4 的处理环境,其内部表现也不一定相同。关于这一点,下一节会进行说明。

要 点

即使手工进行布局对齐,也不能提高可移植性。