概述
许多计算机系统对基本数据类型的合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2,4,8)的倍数。这种对齐简化了形成处理器和内存系统之间接口的硬件设计。比如一个处理器每次从内存中读取八个字节,如果保证每个double类型数据的地址对齐成8的倍数,那么就可以用一个内存操作完成读写值,否则要进行两次读写操作,因为对象可能放在两个8字节内存块中
实例
对于包含结构体的代码,操作系统可能要在其字段的分配中加入空隙来保证每个类型的对齐,比如:
1 | struct S1{ |
如果按照4+1+4=9字节分配,则每个字段分配为i=0-3,c=4,j=5-8 。由于j的偏移量为5,不满足4字节对齐要求。故在实际的分配中,c的后面会加入三个字节的空隙,即c分配4-7。总的分配空间为12个字节。
1 | struct S2{ |
这样会分配9个字节么?其实也不会,考虑如下声明;struct S2 s[4];
如果分配9个字节,第一个元素的每个字段符合数据对齐,每个元素的地址为xd,xd+9,xd+18,xd+27,显然也不行。故编译器仍然会在c的后面填充3个字节,即每个元素占12个字节,这样一来,每个元素的地址为xd,xd+12,xd+24,xd+36,只要xd对齐,其他元素也一定会对齐也就是说,每个元素浪费3个字节的空间,虽然浪费了空间,但是在读取中的便利,这点浪费还是值得的。
下面看两道CSAPP上面的题目
1 | A : struct P1{int i;char c;int j;char d;}; |
A:每个字段都被分配四个字节,共16字节。对齐:起始4的倍数
B:i偏0占4个字节,c偏4占2个字节,d偏6占两个字节,j偏8占8个字节。共16字节,对齐:起始8的倍数
C:w偏0占2*3=6个字节,c偏6占1+1+2=4个字节,共10个字节。对齐:起始2的倍数
D:w偏0占2*4+8=16个字节,*c偏16占3*8=24个字节,共40个字节。对齐:起始8的倍数
E:a偏0占10+14=24个字节,t偏24占16个字节,共40个字节,对齐:起始8的倍数
知道原理了每个字段大小的计算比较简单。整体的对齐要求就是每个字段的对齐要求的最小公倍数。比如B中int(4)和long(8)的最小公倍数,E中a(2)和t(8)的最小公倍数
1 | struct { |
A: *a偏0占8字节,b偏8占8字节,c偏16占8字节,d偏24占4字节,e偏28占4字节,f偏32占8字节,g偏40占8字节,h偏48占8字节。
B: 共56字节
C: 可以看出,每个浪费空间的字段之所以会发生填充浪费,是为了对齐后面的本身占空间就大的字段,故只需将字段定义顺序改为降序排列即可(答案只有降序),其实升序也可以,因为不管升序还是降序,都至少有一个字段会发生填充浪费。解答如下:
1 | struct { |
解释下注释:
第一列表示降序排列时每个字段的偏移量,
第二列表示降序排列时每个字段所占空间大小,
第三列表示升序排列时每个字段的偏移量,
第四列表示升序排列时每个字段所占空间的大小。
可见不论升序还是降序,都占40字节