为什么要数据对齐?
所谓数据对齐是指访问数据的地址要满足一定的条件,能被这个数据的长度所整除。 例如,1字节数据已经是对齐的,2字节的数据的地址要被2整除,4字节的数据地址要 被4整除。
但为什么要数据对齐呢?简单地说,数据对齐是为了读取数据的效率。假如说每一次 读取数据时都是一个字节一个字节读取,那就不需要对齐了,这跟读一个字节没有什 么区别,就是多读几次。但是这样读取数据效率不高。为了提高读取数据的带宽,现 代存储系统都采用许多并行的存储芯片来提高读取效率。以自然字长为4个字节的机器 为例。
memory chip 0 1 2 3 offset 0 0 1 2 3 1 4 5 6 7 2 8 9 10 11 N 4N 4N+1 4N+2 4N+3
如上图所示,地址0-3的4字节数据都存储在4个不同的芯片上。CPU的bits 0-7连上芯 片0,bits 8-15连上芯片1,bits 16-23连上芯片2,bits 24-31连上芯片4。
如果从地址0读取4字节数据,4个存储芯片都从各自的地址0读出数据,而且位置也是 正确的。这4个字节的读取可以并行地进行,因此带宽是一次读取一个字节的4倍。
总结一下为什么需要数据对齐。为了增加CPU读取数据的带宽,内存系统通常都采用并 行结构使得可以并行传输数据。这样的并行结构使得访问对齐的数据速度快,但是若 要使访问不对奇的数据也一样快会使CPU与内存系统的接口变得更复杂,而这是划不来 的。经过权衡之后,最终的结果是:访问对齐的数据速度快,访问不对奇的数据速度 慢(需要2次访问)或干脆禁止访问不对奇数据。
关于访问对齐数据和不对齐数据的速度差异请 看这里。关于为什么使访问不对齐数据需要多次访问请 看这 里。关于哪些CPU禁止访问不对奇数据以及在这些CPU上如何解决访问不对奇 数据的问题请 看这里。
Visual C++的结构体对齐规则
每一个数据对象都有一个对齐限制,通常称为对齐模数。对于基本 类型(不包括结构体,联合体和数组)不同的数据类型的对齐模数通常等于这个数据 类型的大小,比如char
的对齐模数为1,short
的对齐模 数为2,int
的对齐模数为4,float
的对齐模数是 4,double
的对齐模数为8。当然,这个对齐模数每一个编译器可能有差 异,并且可以设置。在Visual C++中可以通过/Zp选项 获#pragma
设置。综合起来,基本类型的对齐模数是这个类型的大小和 设置的对齐限制的较小者。对于结构体,联合体和数组,对齐模数是它的成员的对齐 模数的最大值。
现举几个例子说明一下。(下面例子都在Visual C++ 2008 Express Edition上测试 过。)
struct A { char c; int i; };
c
的对齐模数是1,i
的对齐模数是4,因此结构 体A
的对齐模数为4。c
本身占用1个字节,由 于i
的对齐模数为4,因此c
后面填充3个字节以 使i
的起始地址对齐。这样加起来是8个字节,A
已经对齐 了,后面不用再填充了,因此A
占用8个字节。
另一个例子。
struct A {
char c;
int i;
char d;
};
跟上面一样,c
的对齐模数是1,i
的对齐模数,d
的对齐模数是1,因此结构体A
的对齐 模数为4。c
占用1字节,为使i
对齐,c
后面要填充3个字节,因此c和d占用8个字节。d
占用1个字节,这样一共占用9个字节,但是A
的对齐模数是4,因此d
后面要填充4字节。 因此A占用12字节。
对于比特域来讲,上面的规则要稍稍修改一下。对于连续的声明类型大小相同的比特域来讲,如果它们申请的宽度加起来不超过它们声明的类型大小,那么它们可以压缩在一个分配单元中。对于声明类型大小不同的比特与来讲,它们分配在不同的分配单元中。
struct A {
char a : 3;
char b : 5;
};
a
和b
的类型长度都为1字节,因此可以考虑合并。它们加 起来为8比特,可以放在一个字节中,由于A
的对齐模数为1,因此不需在后面再填充数 据,所以A
的大小为1字节。
另一个例子.
struct A {
char a : 4;
char b : 5;
};
a
和b
的类型长度都为1字节,因此可以考虑合并,但跟上 例不同的是,它们无法放入一个字节中(加起来为9比特),因此它们要放在各自的分 配单元中,a
和b
之间有4比特的空隙。A
的 大小为2字节。
再考虑如下例子.
struct A {
char a : 3;
int b : 2;
char c : 2;
};
a
和b
的类型长度不相等(a
的为1字 节,b
的为4字节),因此不能合并,同样b
和c
也不能合并。因此,a
单 独占用1字节,后面填充3字节以使b
对齐,b
占用4字节,c
占用1字节。由于A
的对齐模 数是4字节,所以c
后面要填充3字节,A
的大小为12字节。
下面再考虑匿名比特域的例子。匿名比特域主要目的是在两个比特域之间加入一段空隙。
struct A {
char a : 2;
char : 2;
char b : 2;
};
匿名比特域对对齐规则没有影响,它唯一的不同是无法引用这个域。由于这3个域的类 型大小是一样的,且可以放在一个字节之中,因此可以合并,A
的大小为1字节。
下面再考虑匿名比特域宽度为0的,它的作用是使它后面的比特域对齐到下个比特域的对齐模数上,也就是说,不允许它跟上一个比特域合并。
struct A {
char a : 2;
char : 0;
char b : 2;
};
尽管这三个比特域的类型一样,但是由于a
和b
之间有一个宽度为0的匿名比特域,因此 它们不能合并。a
和b
个占用1个字节,所以A
的大小为2字节。
最后,对于具有普通成员和比特域的结构体,它们的对齐规则跟上面的一样。注意,普 通成员与比特域不能合并,只有连续的比特域之间可以考虑合并,每一个成员都要对 齐到自己的对齐模数上。
首先 几个概念: