关于数据对齐

一. 首先看看默认情况下是如何对齐

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结构的内存分布如下:

alignment-1

图中 X 表示数据所用的字节, – 表示空字节.
于是输出的值是: 0, 4, 12, 14. size=16

2. Linux x64-64和Windows中的情况:

alignment-2

输出结果: 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字节对齐
alignment-3
sizeof(S2) 值为20

2. Linux x86-64或Windows中, double需对齐到8字节的倍数:
alignment-4
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, 所以末尾不用填充字节

alignment-5

需要注意的是, 如同前面描述的一样, 默认对齐参数在不同平台中是不一样的
所以在不同平台使用#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