数据地址对齐是从C语言映射到机器语言时,C语言隐含做的事情。这一点值得理解。
我们先看看数据:
C声明 | Intel数据类型 | 汇编代码后缀 | x86-64大小(字节) | IA32大小 |
---|---|---|---|---|
char | 字节 | b | 1 | 1 |
short | 字 | w | 2 | 2 |
int | 双字 | l | 4 | 4 |
long int | 四字 | q | 8 | 4 |
long long int | 四字 | q | 8 | 8 |
char * | 四字 | q | 8 | 4 |
float | 单精度 | s | 4 | 4 |
double | 双精度 | d | 8 | 8 |
long double | 扩展精度 | t | 10/16 | 10/12 |
图3-34 x86-64的标准数据类型大小(与IA32比较,长整数和指针需要8个字节,而IA32只需要4个字节)
计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值K(通常是2/4/8)的倍数。这种对齐限制简化了形成处理器和存储器系统之间的接口设计。
无论是否对齐,IA32硬件都能正常的工作,但是Intel建议对齐来提高存储系统的性能。Linux对齐策略是2字节数据类型的地址必须是2的倍数,而较大的数据类型的地址必须是4的倍数。实现多媒体操作的SSE指令要求强制对齐,任何试图以不满足对齐要求的地址来访问存储器都会导致异常,默认的行为是程序终止。Windows的对齐策略更加严格——任何K字节基本对象的地址必须是K的倍数。Linux的惯例是8字节数据在4字节边界上对齐,这可能对i386很好,因为过去存储器十分缺乏,而存储器接口只有4字节宽。对于现代存储器而言,Windows对齐策略更好。在Windows和Linux 上数据类型long double(拓展精度)都有4字节对齐的要求,为此GCC产生的IA32代码分配12个字节(虽然实际的数据类型只需要10个字节)。
x86-64遵循一组更严格的对齐策略。对于任何需要K字节的标量数据类型来说,它的起始地址必须是K的倍数,因此数据类型long和double以及指针,都必须在8字节边界上对齐,此外数据类型long double使用16字节对齐。强加这些对齐条件是为了提高存储器性能——最新的处理器中,存储器接口被设计成读或者写对齐的块,这些块是8或者16字节。
针对更加严格的对齐策略,下面举例说明。
例3.53 对于下列结构声明,确定每个字段的偏移量,结构的整个大小,以及在x86-64下它的对齐要求。
A. struct P1 {int i; char c; long j; char d; };
答案:
偏移量: i:0, c:4, j:8, d:16
整个大小:24
对齐要求:8对齐
B. struct P2 {long i; char c; char d; int j; };
答案:
偏移量:i:0, c:8, d:9, j:12
整个大小:16
对齐要求:8对齐
C. struct P3 {short w[3]; char c[3] };
答案:
偏移量:w:0, c:6
整个大小:10
对齐要求:2
D. struct P4 {short w[3]; char *c[3] };
答案:
偏移量:w:0, c:8
整个大小:32
对齐要求:8
E. struct P3 {struct P1 a[2]; struct P2 *p};
答案:
偏移量:a:0, b:48
整个大小:56
对齐要求:8
总的来说:
x86-64和IA32中的数据结构遵循相同的原则:数组是作为同样大小的块的序列来分配的,这些块中保存着数组元素。结构是作为变长的块的序列来分配的(要确保每个元素满足对齐要求,以及整体可能作为数组元素的对齐要求,因此在元素之间以及元素末尾可能有空隙),块中保存着结构元素。联合是作为一个单独的块来分配的,这个块足够大,能够装下联合中最大的元素。