31

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

如果这只是 CPU 相关的,为什么在 Linux 中对齐双 4 字节而在 Windows 中对齐 8 字节?

4

4 回答 4

18

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

某些存储器类型(例如 RDRAM、DRAM 等)需要以结构化方式(对齐的“字”和“突发事务”即一次多个字)进行访问,以产生有效的结果。这是由于许多原因,其中包括:

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

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


换句话说,访问“未对齐”的结构将产生较低的整体性能。这种陷阱的一个很好的例子:假设数据结构未对齐并且需要 CPU/内存控制器执行 2 次总线事务(而不是 1 次)以获取所述结构,因此性能因此较低。

于 2010-01-05T13:23:51.650 回答
13

CPU以4字节为一组从内存中获取数据(它实际上取决于硬件,它的8或某些类型硬件的其他值,但为了简单起见,我们坚持使用4),如果数据从地址开始,一切都很好可被 4 整除,CPU 转到内存地址并加载数据。

现在假设数据从一个不能被 4 整除的地址开始,比如为了简单起见在地址 1,CPU 必须从地址 0 获取数据,然后应用某种算法将字节转储到 0 地址,以获得对实际地址的访问字节 1 处的数据。这需要时间,因此会降低性能。因此,将所有数据地址对齐会更有效。

于 2010-01-05T13:33:32.260 回答
8

高速缓存行是高速缓存的基本单元。通常它是 16-64 字节或更多。

奔腾 IV:64 字节;奔腾 Pro/II:32 字节;奔腾 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 高速缓存未命中)处理器必须等待整个高速缓存行 1 从 L2->L1 读取到处理器中,然后才能请求第二个高速缓存行,从而导致短暂的执行停顿

(对于 L2 缓存未命中)处理器必须等待来自 L3 缓存(如果存在)或主内存的两次突发读取完成,而不是一次

处理器停顿

  • 对于 64 字节缓存线,随机 4 字节读取将跨越缓存线边界的时间约为 5%,32 字节缓存线为 10%,16 字节缓存线为 20%。

  • 一些关于未对齐数据的指令可能会产生额外的执行开销,即使它位于高速缓存行内。这在英特尔网站上讨论了一些 SSE 指令。

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

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

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

  • 仅在 Intel (!) 代码上,一种常见的做法是为网络和磁盘定义一组打包结构,为内存定义另一组填充结构,并使用例程在这些格式之间转换数据(也考虑为磁盘和网络格式)。

于 2010-01-05T16:46:12.537 回答
3

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

于 2010-01-05T13:34:17.787 回答