内存对齐的目的

诚然,我不明白。假设您有一个内存,内存字的长度为1字节。为什么你不能访问一个4字节长的变量在一个内存访问上一个未对齐的地址(即。不能被4整除,就像对齐地址的情况一样?

106889 次浏览

这是许多底层处理器的限制。它通常可以通过进行4次低效的单字节读取来解决,而不是进行一次高效的单词读取,但许多语言说明符认为,直接禁止它们并强制所有内容对齐会更容易。

OP在这个链接中发现了更多的信息。

在PowerPC上,可以毫无问题地从奇数地址加载整数。

Sparc、I86和(我认为)Itatnium会在您尝试时引发硬件异常。

在大多数现代处理器上,一个32位负载和四个8位负载并没有太大区别。数据是否已经在缓存中将产生更大的影响。

你可以使用一些处理器(尼哈伦可以做到这一点),但以前所有的内存访问都是在64位(或32位)行上对齐的,因为总线是64位宽的,你必须一次获取64位,而且以64位的对齐“块”获取这些要容易得多。

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

因此,当每个人都认为内存很便宜时,他们只是让编译器在处理器的块大小上对齐数据,这样你的代码以浪费内存为代价运行得更快更有效。

现代处理器上的内存子系统仅限于按其字大小的粒度和对齐方式访问内存;出现这种情况的原因有很多。

速度

现代处理器有多层高速缓存,数据必须通过这些缓存读取;支持单字节读取将使内存子系统的吞吐量与执行单元的吞吐量紧密绑定(又名cpu绑定);这一切都让人想起DMA模式超越了PIO模式在硬盘驱动器中的许多相同原因。

CPU 总是读取它的字大小(32位处理器上为4字节),因此当你在支持它的处理器上进行未对齐地址访问时,处理器将读取多个字。CPU将读取请求地址横跨的内存中的每个字。这将导致访问所请求数据所需的内存事务数增加到2倍。

因此,读取两个字节很容易比读取四个字节慢。例如,你在内存中有一个结构体,它看起来像这样:

struct mystruct {
char c;  // one byte
int i;   // four bytes
short s; // two bytes
}

在32位处理器上,它很可能像下面所示的那样对齐:

Struct Layout

处理器可以在一个事务中读取这些成员。

假设你有一个结构的打包版本,可能是从网络中打包的,它是为了传输效率;它可能看起来像这样:

包装结构

读取第一个字节是一样的。

当你要求处理器从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上)。

有关利用缓存可以获得多少性能的更多信息,请查看处理器缓存效果图库;从这个关于缓存行大小的问题

理解缓存线对于某些类型的程序优化是很重要的。例如,数据的对齐方式可以决定一个操作是涉及一条还是两条缓存线。正如我们在上面的例子中看到的,这很容易意味着在不对齐的情况下,操作将慢一倍。

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

因此,CPU从芯片上的L1缓存中读取,现在通常是32KB。但是连接L1缓存到CPU的内存总线的缓存线宽度要小得多。这将是128 的数量级。

所以:

262,144 bits - size of memory
128 bits - size of bus

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

此外,CPU的某些部分将不得不倒立起来,从这两条不同的缓存线中拼凑出一个单独的对象,每条缓存线都有一块数据。在一行上,它是非常高阶的位,在另一行上,它是非常低阶的位。

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

在任何情况下,无论有多少特殊用途的硬件(假设的和愚蠢的)致力于修补错位的内存操作,有时必要的第二次内存读取都会减慢管道。

如果一个具有字节寻址内存的系统有一个32位宽的内存总线,这意味着实际上有四个字节宽的内存系统,它们都连接在一起读写相同的地址。对齐的32位读取将要求信息存储在所有四个内存系统的相同地址中,因此所有系统都可以同时提供数据。未对齐的32位读取要求一些内存系统从一个地址返回数据,而另一些则从下一个更高的地址返回数据。虽然有一些内存系统被优化到能够满足这样的请求(除了他们的地址,他们有效地有一个“+ 1”信号,这使得他们使用一个比指定的地址高的地址),这样的特性增加了相当大的成本和内存系统的复杂性;大多数商品存储系统根本不能同时返回不同的32位字的部分。

如果您有一个32位数据总线,连接到内存的地址总线地址行将从A2开始,因此在单个总线周期中只能访问32位对齐的地址。

因此,如果一个字跨越了一个地址对齐边界——即16/32位数据的__abc0或32位数据的__abc1不为零,则需要两个总线周期来获取数据。

一些架构/指令集不支持未对齐的访问,并且会在这种尝试时生成异常,因此编译器生成的未对齐访问代码不仅需要额外的总线周期,还需要额外的指令,使其效率更低。

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

enter image description here