关于内存对齐的面试题
郑重声明:本文是笔者根据个人理解所写,错误难免,欢迎拍砖!
可以任意转载、修改,转载时是否标明出处,随君而定!
请说出如下2种方式,哪种更好,为什么?
方式一:
void foo(int a, float b, char* ch, double d, float f);
方式二:
struct A { int a; float b; char ch[5]; double d; float f; }; void foo(A* pa);
咋一看,不知道这题想要考什么,无从下手。其实该题是检查考生对于内存对齐的理解。下面我们先看看关于内存的一些知识。
什么是字节对齐,为什么要对齐?
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
对齐的作用和原因:各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些架构的CPU在访问一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数,32位gcc 4.7上默认为8,32位VS2010上默认为8)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
规则:
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员
自身长度中,比较小的那个进行。
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
3、结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
笔者总结:当用sizeof求结构的大小时,第一个数据大小始终为该数据的sizeof大小(即偏移位始终为0),与对齐系数无关。下一个数据的仍为其sizeof的大小,但偏移位为min(数据的sizeof大小,对齐系数)。最后对结构本身进行对齐,即结构的sizeof大小为min(结构中最大数据成员长度,对齐系数)的整数倍。注意该规则不适用于有设置位域的结构。
实例1(32位GCC 4.7):
#include <iostream> #include <cstddef> using namespace std; //#pragma pack(push, 4) struct A { int a; float b; char c; double d; int *pa; char *pc; }; //#pragma pack(pop) int main() { cout << offsetof(A, a) << endl; cout << offsetof(A, b) << endl; cout << offsetof(A, c) << endl; cout << offsetof(A, d) << endl; cout << offsetof(A, pa) << endl; cout << offsetof(A, pc) << endl; cout << sizeof(A) << endl; }
输出结果:
0 4 8 16 24 28 32
分析结果:这里默认对齐系数为8,读者可以根据上面的规则推算一下,sizeof(A) = 4(a) + 4(b) + 1(c) + 7(偏移位) + 8(d) + 4(pa) + 4(pc) = 32。注意,这里别忘了结构本身的对齐,上面各个数据成员偏移后的大小刚好是对齐系数8的整数倍,就不需要再进行偏移了。如果将上面的注释去掉,即将默认对齐系数改为4,想想sizeof(A)的大小?sizeof(A) = 4(a) + 4(b) + 1(c) + 4(偏移位) + 8(d) + 4(pa) + 4(pc) = 28。
这里的offsetof是查看数据成员在结构中的偏移位,定义在cstddef文件中。当你#pragma pack(push,n)改变默认对象系数时,n只能取1、2、4、8、16,将你想要进行内存对齐的结构放入#pragma pack(push, n) ... #pragma pack(pop)之间,这样可以防止将其他的结构也进行内存对齐。
现在回到上面的面试题,方式一调用不存在内存对齐的问题,传入的数据大小就是其本来的数据大小,而方式二就不一样了,由于内存对齐的原因,将导致传入的结构大小为32(32位GCC 4.7)个字节,sizeof(A) = 4(a) + 4(b) + 5(ch) + 3(偏移) + 8(d) + 4 + 4(结构本身对齐偏移) = 32。显然方式一的效果比较高。