为什么数据结构对齐对性能很重要?

35

有人可以给我一个简短而可信的解释,为什么编译器会添加填充以对齐数据结构中的成员?我知道这是为了使 CPU 更有效地访问数据,但我不明白为什么会这样。

如果这只涉及到 CPU,为什么在 Linux 中双倍精度值需要 4 字节对齐,在 Windows 中需要 8 字节对齐呢?


2
有两个独立但相关的问题:数据对齐和数据结构填充。 - Mitch Wheat
gcc 在 x86 机器上也将 double 类型对齐到 8 字节,与微软的编译器相同。 - nos
1
为什么 double 类型需要 8 字节对齐,而 CPU 每次读取数据是按照 4 字节为单位的?如果是这样的话,double 类型是否 8 或 4 字节对齐并不重要,对吗? - Mat
4个回答

19

对齐有助于CPU以高效的方式从内存中获取数据:减少缓存未命中、刷新,减少总线交易等。

某些内存类型(如RDRAM、DRAM等)需要以结构化方式访问(对齐的“字”和“突发事务”,即一次读取多个字),以产生高效结果。这是由于许多因素造成的,其中包括:

  1. 设置时间:内存设备访问内存位置所需的时间
  2. 总线仲裁开销,即许多设备可能想要访问内存设备

"填充"用于纠正数据结构的对齐,以优化传输效率。


换句话说,访问“未对齐”的结构将导致较低的整体性能。一个很好的例子是:假设一个数据结构未对齐且需要CPU / Memory Controller执行2个总线事务(而不是1个)以获取所述结构,则性能相应降低。


如果一个浮点数是1字节对齐的,会发生什么? - Mat
@Mat:然后,根据“浮点变量”在内存中分配的“位置”,访问此“浮点变量”的效率将有所不同。 - jldupont
但是我理解得对吗,访问错误对齐的浮点数的性能不会比访问正确对齐的双精度浮点数更差? - Mat
@Mat:不行:访问未对齐的结构将导致更低的整体效率,即更低的性能。 - jldupont

14

CPU以每4个字节为一组的方式从内存中获取数据(对于某些硬件,实际上取决于硬件可能是8或其他值,但为了简单起见,我们假设其为4), 如果数据的起始地址能够被4整除,则一切顺利,CPU会跳转到内存地址并加载数据。

现在假设数据起始地址不能够被4整除,为了简便起见,假设数据的起始地址为1,那么CPU必须从地址0处获取数据,然后应用某种算法来丢弃0地址处的字节,以访问字节1处的实际数据。这需要时间,因此会降低性能。因此,将所有数据地址对齐更加高效。


3
不一定以4字节为一组:这高度取决于CPU类型。 - jldupont
1
这有点简化了:在内存位置上,如果一个BYTE大小的值不能被4整除也是可以的。如果一个WORD大小的值能够被2整除,那么它也可以放在内存位置上。 - Niki
2
@Alon:情况可能比你描述的“转储字节”要糟糕得多:假设数据结构生成需要额外总线事务的边界,那么性能就会变得非常糟糕。 - jldupont
2
@jldupont:我同意。为了更清晰地阐明主要观点,我忽略了分页和缓存级别以及大小。 - Alon

9

缓存行是缓存的基本单位。通常为16-64字节或更多。

Pentium IV: 64字节; Pentium Pro/II: 32字节; Pentium I: 32字节; 486: 16字节。

myrandomreader:
  ; ...
  ; ten instructions to generate next pseudo-random
  ; address in ESI from previous address
  ; ...
  MOV EAX, DS:[ESI]   ; X
  LOOP myrandomreader

对于跨越两个缓存行的内存读取:

(对于L1缓存未命中) 处理器必须等待从L2->L1读取整个缓存行1到处理器中,然后才能请求第二个缓存行,导致短暂的执行停顿。

(对于L2缓存未命中) 处理器必须等待两个突发读取从L3缓存(如果存在)或主存储器中完成,而不是一个。

处理器停顿

  • 大约5%的时间,随机4字节读取将跨越64字节缓存行的边界,32字节的情况下为10%,16字节的情况下为20%。

  • 即使在缓存行内,某些指令对于不对齐的数据可能会有额外的执行开销。这在Intel网站上谈论了一些SSE指令。

  • 如果您自己定义结构,则可能有意义查看在struct中列出所有<32位数据字段,以减少填充开销,或者审核是否更好地为特定结构打开或关闭打包。

  • 在MIPS和许多其他平台上,您没有选择并且必须对齐-如果不这样做,会出现内核异常!

  • 如果您正在总线上进行I/O或使用原子操作(例如原子递增/递减),或者希望能够将代码移植到非Intel平台,则对齐可能特别重要。

  • 在仅限于Intel的代码中,一种常见的做法是为网络和磁盘定义一组紧凑的结构,为内存中的另一组填充,并拥有将数据在这些格式之间转换的例程(还考虑磁盘和网络格式的"字节序")。


4
除了jldupont的回答之外,有些体系结构具有仅在字对齐边界上操作(用于从内存读取/写入的)负载和存储指令 - 因此,从非对齐的字中加载内存需要两个负载指令,一个移位指令,然后是掩码指令 - 效率要低得多!

读取小于4字节的类型(bool、short等)是否总是包括掩码操作,如果它不是4字节对齐还需要移位指令? - Mat
@Mat:不一定是“移位指令”:在电路层面上,芯片设计师通常将这种操作称为“字节交换器”。 - jldupont

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接