诚然,我不明白。假设您有一个长度为 1 字节的内存字的内存。为什么不能在未对齐地址(即不能被 4 整除)上的单个内存访问中访问 4 字节长的变量,就像对齐地址的情况一样?
8 回答
现代处理器上的内存子系统仅限于以字长的粒度和对齐方式访问内存;出于多种原因,情况就是如此。
速度
现代处理器具有多级高速缓存,数据必须通过这些高速缓存。支持单字节读取将使内存子系统的吞吐量与执行单元的吞吐量紧密绑定(又名 cpu-bound);这一切都让人想起PIO 模式是如何被 DMA 超越的,原因与硬盘驱动器中的许多相同。
CPU总是以其字长(在 32 位处理器上为 4 个字节)读取,因此当您在支持它的处理器上执行未对齐的地址访问时,处理器将读取多个字。CPU 将读取您请求的地址跨越的每个内存字。这导致访问请求的数据所需的内存事务数量最多增加 2 倍。
因此,读取两个字节很容易比读取四个字节慢。例如,假设您在内存中有一个如下所示的结构:
struct mystruct {
char c; // one byte
int i; // four bytes
short s; // two bytes
}
在 32 位处理器上,它很可能会如下所示对齐:
处理器可以在一个事务中读取这些成员中的每一个。
假设你有一个打包版本的结构,可能来自为了传输效率而打包的网络;它可能看起来像这样:
读取第一个字节将是相同的。
当您要求处理器从 0x0005 给您 16 位时,它必须从 0x0004 读取一个字并左移 1 个字节以将其放入 16 位寄存器中;一些额外的工作,但大多数可以在一个周期内处理。
当您从 0x0001 请求 32 位时,您将获得 2X 放大。处理器将从 0x0000 读取到结果寄存器并左移 1 个字节,然后再次从 0x0004 读取到临时寄存器,右移 3 个字节,然后OR
与结果寄存器一起。
范围
对于任何给定的地址空间,如果架构可以假设 2 个 LSB 始终为 0(例如,32 位机器),那么它可以访问 4 倍的内存(2 个保存的位可以代表 4 个不同的状态),或者相同的数量带有 2 位的内存,用于标志之类的东西。从地址中取出 2 个 LSB 将使您获得 4 字节对齐;也称为 4 字节的步幅。每次增加地址时,它实际上是在增加第 2 位,而不是第 0 位,即最后 2 位将始终继续为00
。
这甚至会影响系统的物理设计。如果地址总线需要少 2 个位,CPU 上的引脚可以少 2 个,电路板上的走线可以少 2 个。
原子性
CPU 可以原子地对对齐的内存字进行操作,这意味着没有其他指令可以中断该操作。这对于许多无锁数据结构和其他并发范式的正确操作至关重要。
结论
处理器的内存系统比这里描述的要复杂得多。关于x86 处理器如何实际寻址内存的讨论会有所帮助(许多处理器的工作方式类似)。
您可以在这篇 IBM 文章中阅读到坚持内存对齐的更多好处。
计算机的主要用途是转换数据。现代内存架构和技术经过数十年的优化,以一种高度可靠的方式促进更多数据的输入、输出以及在更多更快的执行单元之间。
奖励:缓存
我之前提到的另一个性能对齐是缓存行上的对齐(例如,在某些 CPU 上)64B。
有关通过利用缓存可以获得多少性能的更多信息,请查看处理器缓存效果库;从这个关于缓存行大小的问题
了解高速缓存行对于某些类型的程序优化可能很重要。例如,数据的对齐可以确定一个操作是触及一个还是两个高速缓存行。正如我们在上面的示例中看到的,这很容易意味着在未对齐的情况下,操作将慢两倍。
这是许多底层处理器的限制。它通常可以通过执行 4 次低效的单字节提取而不是一次有效的字提取来解决,但许多语言说明符认为将它们取缔并强制所有内容对齐会更容易。
OP 在此链接中发现了更多信息。
你可以使用一些处理器(nehalem 可以做到这一点),但以前所有内存访问都在 64 位(或 32 位)线上对齐,因为总线是 64 位宽,你必须一次获取 64 位,并且在对齐的 64 位“块”中获取这些内容要容易得多。
所以,如果你想得到一个字节,你可以获取 64 位块,然后屏蔽掉你不想要的位。如果您的字节位于右端,则简单快捷,但如果它位于该 64 位块的中间,则您必须屏蔽不需要的位,然后将数据转移到正确的位置。更糟糕的是,如果你想要一个 2 字节的变量,但它被分成 2 个块,那么这需要双倍的内存访问。
因此,由于每个人都认为内存很便宜,他们只是让编译器在处理器的块大小上对齐数据,以便您的代码以浪费内存为代价运行得更快、更高效。
从根本上说,原因是内存总线有一些特定的长度,它比内存大小小得多。
因此,CPU 从片上 L1 高速缓存中读取数据,如今通常为 32KB。但是将 L1 高速缓存连接到 CPU 的内存总线的高速缓存行宽度将大大减小。这将是 128位的数量级。
所以:
262,144 bits - size of memory
128 bits - size of bus
未对齐的访问偶尔会重叠两个缓存行,这将需要全新的缓存读取才能获取数据。它甚至可能会一直丢失到 DRAM。
此外,CPU 的某些部分将不得不从这两个不同的缓存行中组合出一个对象,每个缓存行都有一段数据。在一条线上,它将处于非常高阶的位中,在另一条中,它将处于非常低阶的位中。
将有专门的硬件完全集成到管道中,处理将对齐的对象移动到 CPU 数据总线的必要位上,但是对于未对齐的对象可能缺少这样的硬件,因为使用这些晶体管来加速正确优化可能更有意义程式。
在任何情况下,有时需要的第二次内存读取会减慢管道速度,无论有多少专用硬件(假设和愚蠢地)专门用于修补未对齐的内存操作。
@joshperry 对这个问题给出了很好的答案。除了他的回答之外,我还有一些数字以图形方式显示了所描述的效果,尤其是 2X 放大。这是一个指向Google 电子表格的链接,显示了不同单词对齐的效果。此外,这里是一个Github 要点的链接,其中包含测试代码。测试代码改编自@joshperry 引用的 Jonathan Rentzsch 撰写的文章。测试在配备四核 2.8 GHz Intel Core i7 64 位处理器和 16GB RAM 的 Macbook Pro 上运行。
如果您有 32 位数据总线,则连接到内存的地址总线地址线将从 A 2开始,因此在单个总线周期内只能访问 32 位对齐的地址。
因此,如果一个字跨越地址对齐边界——即16/32 位数据的A 0或 32 位数据的 A 1不为零,则需要两个总线周期来获取数据。
一些架构/指令集不支持未对齐访问,并且会在此类尝试时生成异常,因此编译器生成的未对齐访问代码不仅需要额外的总线周期,还需要额外的指令,从而使其效率更低。
如果具有字节可寻址内存的系统具有 32 位宽的内存总线,这意味着实际上有四个字节宽的内存系统,它们都被连线以读取或写入相同的地址。对齐的 32 位读取需要将信息存储在所有四个内存系统的相同地址中,因此所有系统都可以同时提供数据。未对齐的 32 位读取将需要一些内存系统从一个地址返回数据,而另一些则需要从下一个更高地址返回数据。尽管有一些内存系统经过优化以能够满足此类请求(除了它们的地址之外,它们实际上还有一个“加一”信号,导致它们使用比指定高一的地址)这样的特性增加了相当大的成本和内存系统的复杂性;
在 PowerPC 上,您可以毫无问题地从奇数地址加载整数。
Sparc 和 I86 以及(我认为)Itatnium 在您尝试此操作时会引发硬件异常。
在大多数现代处理器上,一个 32 位负载与四个 8 位负载不会有太大的不同。数据是否已经在缓存中会产生更大的影响。