对齐和未对齐的内存访问?

26

对齐和不对齐内存访问有什么区别?

我在TMS320C64x DSP上工作,我想使用内嵌函数(汇编指令的C函数),它有

ushort & _amem2(void *ptr);
ushort & _mem2(void *ptr);

在哪些情况下应该使用哪个?_amem2执行对齐的2字节访问,而_mem2执行非对齐访问。

6个回答

20

许多计算机体系结构将内存存储在每个几个字节的“字”中。例如,Intel 32位体系结构存储由4个字节组成的32位字。但是,内存是以单字节级别寻址的;因此,一个地址可以是“对齐的”,这意味着它从一个字边界开始,或者是“不对齐的”,这意味着它不是。

在某些体系结构上,对于非对齐地址,某些内存操作可能会变慢甚至完全不允许执行。

因此,如果您知道您的地址在正确的地址上对齐,您可以使用_amem2()进行加速。否则,您应该使用_mem2()。


1
很好的解释!你能添加一些不允许非对齐内存访问的CPU的示例吗? - rph

20

对齐内存访问就是指指针(作为整数)是类型特定值(称为对齐)的倍数。对齐是类型必须或应该存储(例如出于性能原因)在CPU上的自然地址倍数。例如,CPU可能要求所有两个字节的加载或存储都通过既是2的倍数的地址进行。对于小型基本类型(小于4个字节),对齐几乎总是类型的大小。对于结构体,对齐通常是任何成员的最大对齐方式。

C编译器始终将您声明的变量放置在满足“正确”对齐的地址处。因此,如果ptr指向例如uint16_t变量,则它将被对齐,并且您可以使用_amem2。仅当您访问例如通过I/O接收到的紧密包装的字节数组或字符串中间的字节时,才需要使用_mem2。


13

我知道这是一个旧问题,有一个被选中的答案,但没有人解释什么是对齐和未对齐的内存访问之间的区别...

无论是dram还是sram还是flash或其他。 以sram为简单例子,它由位构建而成,特定的sram将由一定数量的位宽和一定数量的行深度构建。 假设宽度为32位,行数为若干/很多。

如果我在此sram的地址0x0000处进行32位写入,则围绕此sram的内存控制器可以直接对第0行进行单个写入周期。

如果我在此sram的地址0x0001处进行32位写入(假设允许),则控制器将需要读取第0行,修改其中三个字节,保留一个,然后将其写入到第0行,再读取第1行,将一个字节修改为原样并将其余三个字节保持不变,然后将其写回。要修改哪些字节与系统的字节序有关。

前者是对齐的,后者是未对齐的,显然有性能差异,需要额外的逻辑来完成四个内存周期并合并字节通道。

如果我从地址0x0000处读取32位,则只需对第0行进行单次读取。 但是如果从0x0001处读取,则必须读取row0和row1,具体取决于系统设计是否仅将这64位发送回处理器,可能需要两个总线时钟周期而不是一个。或者内存控制器具有额外的逻辑,使32位在数据总线上对齐,并在一个总线周期内完成。

16位读取比较好,从0x0000、0x0001和0x0002进行的读取只是从行0进行读取。基于系统/处理器设计,可以向处理器发送这32位数据并由处理器在内存控制器中将它们放置在特定的字节通道上,以便处理器不必旋转。如果不是两者都要做。0x0003的读取与上述情况类似,因为你必须从行0和行1读取一个字节,并且然后发送64位回到处理器进行提取,或者内存控制器将这些位组合成一个32位总线响应(假设处理器和内存控制器之间的总线在这些示例中是32位宽)。

但是,在这个示例的SRAM中,16位写入始终会以至少一次读取-修改-写入的形式结束。对于地址0x0000、0x0001和0x0002,先读取行0,然后修改两个字节并写回。对于地址0x0003,先读取两行,然后分别修改一个字节,并写回。

8位读取只需要读取包含该字节的一行,但写入则必须进行一次读取-修改-写入。

ARMv4不喜欢不对齐的操作,虽然你可以禁用陷阱并且结果不像上面所期望的那样重要,但是当前的ARM支持不对齐且提供了上述行为,你可以在控制寄存器中改变一位然后它将中止不对齐的传输。MIPS以前不允许,现在不确定他们是否这么做。X86、68K等允许,内存控制器可能会做最多的工作。

那些不允许非对齐内存访问的设计通常是为了提高性能和减少逻辑负担,有些人认为这给程序员带来了额外的工作量,而另一些人则认为程序员并没有增加额外的工作量或者更容易处理。与其尝试通过使用8位变量来节省任何内存,还不如直接使用32位寄存器或总线的自然大小,这可能会在稍微增加一些字节的成本下提高性能。此外,编译器需要添加额外的代码来模拟8位变量,包括掩码和有时进行符号扩展等操作,而使用寄存器原生大小则无需这些附加指令。您还可以将多个内容打包到总线/存储器宽位置中,并执行一个内存周期来收集或写入它们,然后使用一些额外的指令在寄存器之间进行操作,这不会占用RAM,指令的数量也能得到平衡。
我不同意编译器总是会根据目标正确地对齐数据,有时是可以打破这种情况的。如果目标不支持非对齐,你将会遭遇错误。程序员永远不需要谈论这个问题,如果编译器总是根据任何合法的代码正确地做到这一点,除非是为了性能,否则就没有理由讨论这个问题。如果您不能控制void指针地址的对齐或非对齐,则必须始终使用mem2()这种非对齐访问,否则您必须根据指针的值在代码中使用if-then-else 条件判断,如Nik所指出的那样。通过声明为void,C编译器现在无法正确地处理您的对齐,并且不能保证对齐。如果您将char * prt传递给这些函数,编译器在不添加额外代码的情况下很难做到正确处理它们。因此,按照您问题中的描述,mem2()是唯一正确的答案。

你的台式机或笔记本电脑中使用的DRAM通常是64或72(带ECC)位宽的,对它们的每次访问都进行了对齐。即使内存条实际上是由8位宽、16位宽或32位宽的芯片组成的。(这可能会因为各种原因在手机/平板电脑中发生变化)内存控制器和至少一个缓存最好坐在DRAM前面,以便缓存SRAM处理非对齐甚至小于总线宽度的读取-修改-写操作,SRAM要快得多,而DRAM访问则都是对齐的完整总线宽度访问。如果DRAM前面没有缓存,并且控制器设计为全宽度访问,则性能最差。如果设计为逐个字节通知(假设8位宽芯片),则不需要进行读取-修改-写操作,但控制器更加复杂。如果典型用例是有缓存的情况(如果设计中有缓存),那么每个字节通道的额外工作可能没有意义,只需知道如何执行完整总线宽度大小的传输或其倍数即可。


6

对齐的地址是指与所访问的大小成倍数关系的地址。

  • 在4字节单词上访问与4的倍数相关的地址将被对齐
  • 从地址3处访问4字节将导致未对齐访问

很可能_mem2函数也可以用于未对齐的访问,但在其代码中实现正确的对齐会比较低效。这意味着_mem2函数可能比其_amem2版本更昂贵。

因此,当需要性能时(特别是当知道访问延迟很高时),最好确定何时可以使用对齐访问。为此,_amem2存在的目的就是在您知道访问已对齐时提供性能。

对于2字节访问,识别对齐操作非常简单。
如果所有操作的访问地址都是“偶数”(即它们的LSB为零),则具有2字节对齐。可以轻松检查:

if (address & 1) // is true
    /* we have an odd address; not aligned */
else
    /* we have an even address; its aligned to 2-bytes */

3

_mem2更为通用。如果ptr对齐或不对齐,它都可以工作。_amem2更为严格:它要求ptr对齐(尽管可能稍微更有效)。因此,除非您能保证ptr始终对齐,否则请使用_mem2。


如何确保我的数据是否对齐?如果您能提供一个具体的简单示例,我将不胜感激。 - Can Bal
1
这取决于你从哪里获取ptr。例如,如果ptr来自对malloc的调用,并且您的系统保证结果对齐,则ptr将对齐。您需要检查malloc的文档以查看它是否保证对齐。如果ptr是结构体中字段的地址,则取决于整个结构体是否对齐,以及您的编译器是否“打包”或对齐结构体的字段。您需要查阅编译器文档才能确定。在确定之前,只需使用_mem2,因为它可以处理ptr是否对齐。 - Laurence Gonsalves
1
您可以假设由 C 或 C++ 创建的任何对象都已正确对齐,除非您有特别的操作来防止它(例如声明不寻常的打包)。但是,在汇编代码中创建的对象可能没有被对齐。 - Max Lybbert

3
许多处理器在内存访问时有对齐限制。非对齐访问会生成异常中断(例如ARM),或者速度较慢(例如x86)。 _mem2 可能是通过获取两个字节并使用移位和按位操作将它们组合成一个16位的ushort来实现的。 _amem2 可能只是从指定的ptr读取16位的ushort。
我不了解TMS320C64x,但我猜测它需要16位对齐进行16位内存访问。因此,您总是可以使用 _mem2,但会带来性能损失,并且当您可以保证ptr为偶地址时,可以使用 _amem2

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