数据对齐

来源:互联网 发布:新津知美术馆 编辑:程序博客网 时间:2024/05/01 20:23
为什么要数据对齐?
所谓数据对齐是指访问数据的地址要满足一定的条件,能被这个数据的长度所整除。 例如,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倍。


假如要从地址1读取4字节数据会发生什么呢?首先,读取的数据位置不正确。从芯片1 读取的数据应该放在bits 0-7,它却对应CPU的bits 8-15。从芯片2,3,0读取的数据 也是一样。这并不是什么大问题,CPU可以把读入的数据循环左移8比特来把它们放置 在正确的位置上。问题在于,这一次发给4个芯片的地址不一样,给芯片1,2和3的地 址是0,给芯片0的地址是1。也就是说给每一个芯片的地址都不一样,这就需要多根地 址总线。32位的CPU就需要4个地址总线,每个地址总线需要用到CPU的至少32个引脚, 也就是必须要100多根引脚。而通常32位的CPU也就400多个引脚。这当然会增加CPU与 内存接口的复杂性(也可能会有其它的方案解决这个问题,但最终都会使接口变得更 复杂,还可能会降低效率)。但是增加这个复杂性是不值得。由于大多数数据访问都 可以做成对齐的(编译器通常会使数据对齐),因此为了使极少数的不对奇访问速度 更快而使CPU与内存的接口变的更复杂,是不划算的。


在这样的设计下,访问没有对齐的数据需要2次访问。比如,访问地址1, 2, 3, 4的4 字节数据,先读取地址0-3的数据,再读取地址4-7的数据,再把地址1-5的数据取出来 放到目的寄存器中。相应地,这增加了CPU内部的复杂性。这就解释了为什么不对齐的 数据要多次访问内存,速度不如访问对齐的数据快。而且也解释了为什么原子类型的 数据必须要对齐。有的CPU,例如Motorala生产的一些CPU,还有IBM的PowerPC,为了 消除这个复杂性,干脆禁止访问不对奇地址(若访问不对奇地址时将产生异常)。


总结一下为什么需要数据对齐。为了增加CPU读取数据的带宽,内存系统通常都采用并 行结构使得可以并行传输数据。这样的并行结构使得访问对齐的数据速度快,但是若 要使访问不对奇的数据也一样快会使CPU与内存系统的接口变得更复杂,而这是划不来 的。经过权衡之后,最终的结果是:访问对齐的数据速度快,访问不对奇的数据速度 慢(需要2次访问)或干脆禁止访问不对奇数据。


关于访问对齐数据和不对齐数据的速度差异请 看这里。关于为什么使访问不对齐数据需要多次访问请 看这 里。关于哪些CPU禁止访问不对奇数据以及在这些CPU上如何解决访问不对奇 数据的问题请 看这里。


Visual C++的结构体对齐规则
MSDN对Visual C++的结构体对齐规则有简单介绍,现摘录如下:


Structure members are stored sequentially in the order in which they are declared: the first member has the lowest memory address and the last member the highest.


Every data object has an alignment-requirement. The alignment-requirement for all data except structures, unions, and arrays is either the size of the object or the current packing size (specified with either /Zp or the pack pragma, whichever is less). For structures, unions, and arrays, the alignment-requirement is the largest alignment-requirement of its members. Every object is allocated an offset so that


offset % alignment-requirement == 0


Adjacent bit fields are packed into the same 1-, 2-, or 4-byte allocation unit if the integral types are the same size and if the next bit field fits into the current allocation unit without crossing the boundary imposed by the common alignment requirements of the bit fields.


每一个数据对象都有一个对齐限制,通常称为对齐模数。对于基本 类型(不包括结构体,联合体和数组)不同的数据类型的对齐模数通常等于这个数据 类型的大小,比如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字节。
 


最后,对于具有普通成员和比特域的结构体,它们的对齐规则跟上面的一样。注意,普 通成员与比特域不能合并,只有连续的比特域之间可以考虑合并,每一个成员都要对 齐到自己的对齐模数上。
原创粉丝点击