数据对齐

概述

许多计算机系统对基本数据类型的合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2,4,8)的倍数。这种对齐简化了形成处理器和内存系统之间接口的硬件设计。比如一个处理器每次从内存中读取八个字节,如果保证每个double类型数据的地址对齐成8的倍数,那么就可以用一个内存操作完成读写值,否则要进行两次读写操作,因为对象可能放在两个8字节内存块中

实例

对于包含结构体的代码,操作系统可能要在其字段的分配中加入空隙来保证每个类型的对齐,比如:

1
2
3
4
5
struct S1{
int i;
char c;
int j;
}

如果按照4+1+4=9字节分配,则每个字段分配为i=0-3,c=4,j=5-8 。由于j的偏移量为5,不满足4字节对齐要求。故在实际的分配中,c的后面会加入三个字节的空隙,即c分配4-7。总的分配空间为12个字节。

1
2
3
4
5
struct S2{
int i;
int j;
char c;
}

这样会分配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
2
3
4
5
6
A : struct P1{int i;char c;int j;char d;};
B: struct P2{int i;char c;char d;long j;};
C: struct P3{short w[3];char c[3];};
D : struct P4{short w[5];char *c[3]};
E : struct P5{struct P3 a[2];struct P2 t};
求各个字段的偏移量和结构的总的大小,以及对齐要求

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
2
3
4
5
6
7
8
9
10
11
12
13
14
struct {
char *a;
short b;
double c;
char d;
float e;
char f;
long g;
int h;
}rec;

A.求各个字段的偏移量
B.这个结构的总大小
C.重新编排结构中的字段,以最小化浪费空间,给出其大小

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
2
3
4
5
6
7
8
9
10
struct {
char * a; //0,8,32,8
double c; //8,8,24,8
long g; //16,8,16,8
int h; //24,4,8,8
float e; //28,4,4,4
short b; //32,2,2,2
char d; //34,1,1,1
char f; //35,5,0,1
}

解释下注释:
第一列表示降序排列时每个字段的偏移量,
第二列表示降序排列时每个字段所占空间大小,
第三列表示升序排列时每个字段的偏移量,
第四列表示升序排列时每个字段所占空间的大小。
可见不论升序还是降序,都占40字节