记忆对齐的目的

ark*_*ark 175 memory alignment memory-alignment

诚然,我不明白.假设您的内存中包含长度为1个字节的内存字.为什么不能在未对齐地址的单个内存访问中访问一个4字节长的变量(即不能被4整除),因为对齐地址就是这种情况?

jos*_*rry 288

现代处理器上的存储器子系统仅限于以其字大小的粒度和对齐方式访问存储器; 出于多种原因,情况就是如此.

速度

现代处理器具有多级高速缓冲存储器,必须通过数据; 支持单字节读取会使内存子系统吞吐量与执行单元吞吐量紧密绑定(也称为cpu-bound); 这一切都让人联想起DMA如何超越PIO模式,其原因与硬盘相同.

CPU 始终读取其字大小(32位处理器上的4个字节),因此当您在支持它的处理器上执行未对齐的地址访问时 - 处理器将读取多个字.CPU将读取您请求的地址跨越的每个内存字.这导致访问所请求数据所需的存储器事务数量的最多放大2倍.

因此,读取两个字节比四个字节更容易慢.例如,假设你在内存中有一个结构,如下所示:

struct mystruct {
    char c;  // one byte
    int i;   // four bytes
    short s; // two bytes
}
Run Code Online (Sandbox Code Playgroud)

在32位处理器上,它很可能像这里显示的那样对齐:

结构布局

处理器可以在一个事务中读取每个成员.

假设您有一个打包版本的结构,可能来自它所包装的网络,以提高传输效率; 它可能看起来像这样:

打包结构

读第一个字节将是相同的.

当您要求处理器从0x0005给出16位时,它必须从0x0004读取一个字,并向左移1个字节以将其置于16位寄存器中; 一些额外的工作,但大多数可以在一个周期内处理.

当您从0x0001请求32位时,您将获得2倍的放大.处理器将从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.

有关多少性能可以通过利用高速缓存来获得更多的信息,看看处理器高速缓存影响的画廊 ; 从缓存行大小的这个问题

了解缓存行对于某些类型的程序优化很重要.例如,数据的对齐可以确定操作是否触摸一个或两个高速缓存行.正如我们在上面的示例中看到的,这很容易意味着在未对齐的情况下,操作将慢两倍.

  • 我认为“mystruct”的对齐是错误的。C 结构体始终与其最大成员的对齐方式对齐,因此在“s”之后应该有两个额外的填充字节。 (4认同)
  • @joshperry:轻微校正:8086可以在*4*周期内进行字对齐的16位读取,而未对齐的读取需要*8*.由于存储器接口较慢,基于8088的计算机上的执行时间通常由指令提取决定.像"MOV AX,BX"这样的指令名义上比"XCHG AX,BX"快一个周期,但除非它的前面或后面是一个指令,每个代码字节的执行时间超过4个周期,否则需要4个周期才能完成执行.在8086上,代码获取有时可以跟上执行,但在8088上,除非使用... (2认同)
  • 非常正确,@martin。我省略了这些填充字节以集中讨论结构内,但也许包含它们会更好。 (2认同)

Pau*_*lin 57

这是许多底层处理器的限制.它通常可以通过执行4个低效的单字节提取而不是一个有效的单词提取来解决,但是许多语言指定者认为仅仅取消它们并迫使所有内容对齐会更容易.

OP发现此链接中有更多信息.


gbj*_*anb 22

您可以使用某些处理器(nehalem可以执行此操作),但之前所有内存访问都是在64位(或32位)线上对齐,因为总线是64位宽,您必须一次取64位,并且在64位的对齐"块"中获取它们要容易得多.

因此,如果您想获得单个字节,则获取64位块,然后屏蔽掉您不想要的位.如果您的字节位于右端,则简单快速,但如果它位于该64位块的中间,则必须屏蔽掉不需要的位,然后将数据移到正确的位置.更糟糕的是,如果你想要一个2字节变量,但是它被分成2个块,那么这需要双倍所需的内存访问.

因此,每个人都认为内存很便宜,他们只是让编译器将数据与处理器的块大小对齐,这样你的代码就会以浪费的内存为代价更快,更高效地运行.


Dig*_*oss 7

从根本上说,原因是内存总线有一些特定的长度,比内存大小小得多。

因此,CPU 会读取片上 L1 缓存,目前通常为 32KB。但是将 L1 缓存连接到 CPU 的内存总线的缓存线宽度将小得多。这将是 128位的数量级

所以:

262,144 bits - size of memory
    128 bits - size of bus
Run Code Online (Sandbox Code Playgroud)

未对齐的访问偶尔会重叠两条缓存线,这将需要全新的缓存读取才能获取数据。它甚至可能完全错过 DRAM。

此外,CPU 的某些部分将不得不站在它的头上,将这两个不同的缓存行中的单个对象放在一起,每个缓存行都有一个数据。在一条线上,它将位于非常高的位,在另一条线上,将位于非常低的位。

将有专用硬件完全集成到管道中,用于将对齐的对象移动到 CPU 数据总线的必要位上,但是对于未对齐的对象可能缺少此类硬件,因为使用这些晶体管来加速正确优化可能更有意义程式。

在任何情况下,无论有多少专用硬件(假设和愚蠢地)专门用于修补未对齐的内存操作,有时必要的第二次内存读取都会减慢管道速度。


adi*_*ino 5

@joshperry 对这个问题给出了很好的答案。除了他的回答之外,我还有一些数字以图形方式显示了所描述的效果,尤其是 2X 放大。这是一个指向Google 电子表格的链接,显示了不同单词对齐的效果。此外,这里有一个指向Github 要点的链接,其中包含测试代码。测试代码改编自@joshperry 引用的 Jonathan Rentzsch 撰写的文章。测试在配备四核 2.8 GHz Intel Core i7 64 位处理器和 16GB RAM 的 Macbook Pro 上运行。

在此处输入图片说明

  • `x` 和 `y` 坐标是什么意思? (4认同)
  • 什么一代酷睿i7?(感谢您发布代码链接!) (2认同)