为什么存在内存对齐

CPU访问某个数据时,要求其存储地址必须是相应数据类型的自然边界。对于存储地址不在其相应类型自然边界的数据,不支持非对齐数据访问的CPU,会导致CPU异常;即使是支持非对齐数据访问的CPU,也会严重影响程序效率。

假设非对齐访问出现在位于操作系统之上的进程,且CPU不支持非对齐数据访问,那么对于出现CPU异常的情况,可能操作系统会对其进行处理,(1)将所需要的数据装载,并返回,或者说(2)直接让进程死掉。情形(2)不需要多做解释;对情况(1)来说,非对齐访问每次都要进入异常处理程序,相比于一条指令直接拿到数据,效率极其低下。

假设非对齐访问出现在直接位于硬件之上的进程,且CPU不支持非对齐数据访问,那么对于出现CPU异常的情况来说,基本上的直观反应是进程退出并出现堆栈信息。

现在假设有一个8字节数据如下,|表示数据开始位置,|-|表示自然边界

  |-|BBBBB|BBB|-|BBBBB|BBB

其前三字节为前一个对齐的八字节数据的后三字节,其后五字节为后一个对齐的八字节数据的前五字

  对于不支持非对齐装载指令的CPU来说,要装载这样的一个数据,需要先装载前一个八字节数据,再装载后一个八字节数据,然后将前一个八字节数据的后三字节与后一个八字节数据的前五字节数据合并才能得到结果,与对齐数据的访问相比,多了一个装载指令以及相关合并指令的开销,一般来说,在忽视缓存未命中的情况下,装载指令的执行与得到结果之间是存在额外开销的,因此这个差别是很大的,何况上边说的,假设是在操作系统对CPU异常进行处理时为其加载数据,那么异常处理程序的开销可能更大;对非对齐数据的写入时也需要额外的加载,合并操作。

  即使对于支持非对齐数据加载的CPU,依然会极大的影响效率,差别只是它省略掉了CPU异常处理过程。

  再进一层,假设之前描述的非对齐数据刚好横跨两个cache line,而且这两个cache line至少有一个不在cache中(虽然对齐数据也会存在未命中,但是与非对齐相比,它不会横跨两个cache line),那么这个访问效率绝对不是多几十条指令的问题了。

  因此,内存不对齐的坏处不是浪费内存,因为即使我写一个随便在不同位置放置不同大小的数据结构时,只要告诉编译器说必须按照一字节对齐,编译器编译时肯定按照我的意愿不浪费一个字节的内存。编译器默认按照自然边界对齐,是因为它要求效率,保证程序的正常运行(因为非对齐访问可能导致进程退出)。我们对结构体的组织的调整是为了节约内存,而调整的规则就是按照内存对齐来安插数据。

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。

规则:

1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。

2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

3、结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。

Win32平台下的微软C编译器的对齐策略:

1)结构体变量的首地址是其最长基本类型成员的整数倍;

备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。

2)结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);

备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。

3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节。

备注:

a、结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/b509e9a3f8084728c24a68462c871de1.html