关于数据对齐
一. 首先看看默认情况下是如何对齐
IA32:
无论数据是否对齐, IA32硬件都能正确工作, 不过, Intel还是建议要求对齐数据以提高存储器系统的性能. Linux沿用的对齐策略是, 2字节数据类型 (例如 short) 的地址必须是2的倍数, 而较大的数据类型 (例如 int, int *, float 和 double) 的地址必须是4的倍数. 注意, 这个要求就意味着一个short类型对象的地址最低位必须等于0. 类似地, 任何 int类型的对象或指针的地址最低两位必须都是0.
强制对齐的情况
对于大多数 IA32 的指令来说, 保持数据对齐能够提高效率, 但是它不会影响程序的行为. 另一方面, 如果数据未对齐, 有些实现多媒体操作的 SSE 指令就无法正确地工作. 这些指令对16字节数据块进行操作, 在 SSE 单元和存储器之间传送数据的指令要求存储器地址必须是16的倍数. 任何试图以不满足对齐要求的地址来访问存储器都会导致异常 (exception), 默认的行为是程序终止.
因此IA32的一个惯例是, 确保每个栈帧的长度都是16字节的整数倍. 编译器就可以在栈帧中以每个块的存储都是16字节对齐的方式来分配存储.
Microsoft Windows 的对齐
Microsoft Windows的对齐的要求更严格 —- 任何K字节基本对象的地址都必须是K的倍数, K = 2, 4 或者8. 特别的, 它要求double或者 long long 类型数据的地址应该是8的倍数. 这种要求提高了存储器的性能, 而代价是浪费了一些空间. Linux 的惯例是 8字节数据在4字节边界上对齐, 这可能对i386很好, 因为过去存储器十分缺乏, 而存储器接口只有4字节宽. 对于现代处理器来说, Microsoft的对齐策略就是更好的选择.在Windows和Linux上, 数据类型long double都有4字节对齐的要求.为此GCC产生的IA32代码分配12个字节(虽然实际的数据类型只需要10个字节).
另外, 编译器在结构的末尾可能需要一些填充, 这样结构数组中的每个元素都会满足它的对齐要求.
以上内容摘自<深入理解计算机系统>(中文版 原书第二版)p.170
x86-64:
x86-64遵循一组更严格的对齐要求. 对于任何需要K字节的标量数据类型来说, 它的起始地址必须是K的倍数. 因此, 数据类型long 和 double 以及指针, 都必须在8字节的边界上对齐. 此外, 数据类型long double使用16字节对齐(分配也是16字节大小), 虽然实际表示只需要10个字节. 强加上这些对齐条件是为了提高存储器系统性能 —- 最新的处理器中, 存储器接口被设计成读或者写对齐的块, 这些块是8或者16字节长.
摘自<深入理解计算机系统>(中文版 原书第二版)p.200
看个例子, 根据以上描述, 以下代码在各系统中分别会输出什么值?
#include <stdio.h> #include <stddef.h> typedef struct { int i; double d; char c; short s; } ST; int main() { printf("%d, %d, %d, %d. ", offsetof(ST,i),offsetof(ST,d), offsetof(ST,c),offsetof(ST,s)); printf("size=%dn",sizeof(ST)); return 0; }
1. 根据书中的描述, Linux i386中ST结构的内存分布如下:
图中 X 表示数据所用的字节, – 表示空字节.
于是输出的值是: 0, 4, 12, 14. size=16
2. Linux x64-64和Windows中的情况:
输出结果: 0, 8, 16, 18. size=24
注意末尾还有4个填充字节, 结构体自身地址按照最大成员的对齐参数对齐, 结构长度必须是最大成员长度的整数倍, 这样才能保证结构数组中的元素也能对齐
*****long double在不同平台上有不同的实现, 甚至同一个系统下不同编译器也有不同实现*****
32位Windows中, gcc会使用10字节的long double, 分配12字节, 按4字节对齐;
vc (32位编译器) 中的long double 却和 double 一样是8字节(如同int 和long int的关系), 分配8字节, 按8字节对齐. (与书中描述不一致)
Windows x86-64中的long double还没考察过, 有用vc-64位编译的朋友可以自己测试下
在Linux i386 中实际使用10字节, 编译器为其分配12字节, 按4字节对齐. (与书中描述一致)
Linux x86-64 中实际使用10字节, 编译器为其分配16字节, 按16字节对齐. (与书中描述一致)
二. 嵌套的结构
考虑如下结构体声明
typedef struct { short s; int i; } S1; /* s i */ /* xx--xxxx */ /* sizeof S1 == 8 */ typedef struct { char c; S1 s1; double d; } S2;
S1在S2中以何种方式对齐?
这个和前面所说的结构数组一个道理, 结构自身将按照最大成员的对齐参数来对齐, 这里是4, 所以S2结构中c后面会填充3个空字节.
1. 首先Linux i386中的情形相对较简单, 大于等于4的标量类型都按照4字节对齐
sizeof(S2) 值为20
2. Linux x86-64或Windows中, double需对齐到8字节的倍数:
sizeof(S2) == 24
三. #pragma pack()
使用#pragma pack(n) , 编译器将使用数据类型默认的对齐参数与指定对齐参数n中较小的值来对齐. 比如:
#pragma pack(2) struct st { char c; int i; }; #pragma pack()
char默认对齐参数是1, 指定的参数是2, 于是c 的边界对齐到1
而int默认对齐参数是4, 指定参数是2, 于是i 的边界对齐到 2
结构的地址也对齐到2, 所以末尾不用填充字节
需要注意的是, 如同前面描述的一样, 默认对齐参数在不同平台中是不一样的
所以在不同平台使用#pragma pack(n)的结果也不一定相同, 比如:
#pragma pack(8) struct st{ char c; double d; }; #pragma pack()
在32位linux中 sizeof(struct st) == 12, 因为double默认对齐单位是4
而Linux(x86-64)和Windows中double默认对齐单位是8, 所以sizeof(struct st) == 16