【问题标题】:Aligned and unaligned memory accesses?对齐和未对齐的内存访问?
【发布时间】:2010-11-07 00:30:34
【问题描述】:

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

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

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

_amem2 执行 2 个字节的对齐访问,_mem2 执行未对齐访问。

我应该什么时候使用哪个?

【问题讨论】:

    标签: c++ c memory memory-alignment omap


    【解决方案1】:

    _mem2 更通用。如果 ptr 是否对齐,它将起作用。 _amem2 更严格:它要求 ptr 对齐(尽管可能稍微更有效)。所以使用 _mem2 除非你能保证 ptr 总是对齐的。

    【讨论】:

    • 如何保证我的数据是否对齐?如果您能提供一个具体的简单示例,我将不胜感激。
    • 这取决于您从哪里获得 ptr。例如,如果 ptr 来自对 malloc 的调用,并且您的系统保证结果是对齐的,那么 ptr 将是对齐的。您需要检查 malloc 的文档以查看它是否保证对齐。如果 ptr 是结构中字段的地址,那么它取决于整个结构是否对齐,以及您的编译器是“打包”还是对齐结构的字段。您需要查阅您的编译器文档才能确定。在您确定之前,只需使用 _mem2,因为无论 ptr 是否对齐,它都可以工作。
    • 您可以假设由 C 或 C++ 创建的任何对象都正确对齐,除非您专门做了一些事情来防止它发生(例如声明异常打包)。但在汇编代码中创建的对象可能不是。
    【解决方案2】:

    许多计算机体系结构将内存存储在每个数字节的“字”中。例如,英特尔 32 位体系结构存储 32 位字,每个字 4 个字节。但是,内存是在单字节级别寻址的;因此,地址可以“对齐”,即从字边界开始,也可以“未对齐”,即不对齐。

    在某些架构上,某些内存操作可能会更慢,甚至在未对齐的地址上完全不允许。

    因此,如果您知道您的地址与正确的地址对齐,则可以使用 _amem2() 来提高速度。否则,您应该使用 _mem2()。

    【讨论】:

    • 很好的解释!您能否添加一些不允许未对齐内存访问的 CPU 示例?
    【解决方案3】:

    许多处理器对内存访问有对齐限制。未对齐的访问要么生成异常中断(例如 ARM),要么只是速度较慢(例如 x86)。

    _mem2 可能被实现为获取两个字节并使用移位和/或按位操作将它们制成 16 位 ushort。

    _amem2可能只是从指定的ptr读取16位ushort。

    我不具体了解 TMS320C64x,但我猜它需要 16 位对齐才能访问 16 位内存。因此,您可以始终使用_mem2,但会降低性能,当您可以保证 ptr 是偶数地址时,您可以使用_amem2

    【讨论】:

      【解决方案4】:

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

      C 编译器总是将您声明的变量放在满足“正确”对齐的地址处。因此,如果 ptr 指向例如一个 uint16_t 变量,它将被对齐,您可以使用 _amem2。只有在访问时才需要使用 _mem2,例如通过 I/O 接收的打包字节数组,或字符串中间的字节。

      【讨论】:

        【解决方案5】:

        对齐地址是相关访问大小的倍数。

        • 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 */
        

        【讨论】:

          【解决方案6】:

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

          无论是 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 读取只会从 row0 读取,并且可以基于系统/处理器设计将这些 32 位发送回,处理器提取它们或将它们移入内存控制器,以便它们降落在特定的字节通道上,因此处理器不必旋转。一个或另一个必须如果不是两者兼而有之。从 0x0003 读取虽然就像上面一样,但您必须读取第 0 行和第 1 行,因为每个字节中都有一个字节,然后将 64 位发送回处理器以提取或内存控制器将这些位组合成一个 32 位总线响应(对于这些示例,假设处理器和内存控制器之间的总线为 32 位宽)。

          虽然在这个示例 sram 中,16 位写入总是以至少一个 read-modify-write 结束,地址 0x0000、0x0001 和 0x0002 读取 row0 修改两个字节并写回。地址 0x0003 读取两行,每行修改一个字节并写回。

          8 位你只需要读取包含该字节的一行,写入虽然是一行的读取-修改-写入。

          armv4 不喜欢未对齐,尽管您可以禁用陷阱,但结果与您在上面所期望的不同,这并不重要,当前的 arm 允许未对齐并为您提供上述行为,您可以在控制寄存器中进行一些更改,然后它将中止未对齐的传输。 mips 以前不允许,现在不知道他们在做什么。 x86、68K 等是允许的,内存控制器可能不得不做最多的工作。

          显然不允许它的设计是为了性能和更少的逻辑,有些人会说这是程序员的负担,其他人可能会说这对程序员来说没有额外的工作或更容易。对齐与否,您还可以看到为什么最好不要尝试通过创建 8 位变量来节省任何内存,而是继续烧写 32 位字或寄存器或总线的自然大小。它可以以一些字节的小成本帮助您的性能。更不用说编译器需要添加的额外代码,以使 32 位寄存器模仿 8 位变量、屏蔽和有时符号扩展。在使用寄存器本机大小的情况下,不需要这些额外的指令。您还可以将多个东西打包到一个总线/内存范围的位置,并执行一个内存周期来收集或写入它们,然后使用一些额外的指令在不消耗内存的寄存器之间进行操作,并且可能会清洗指令数量。

          我不同意编译器总是为目标对齐数据,有办法打破这一点。如果目标不支持未对齐,您将遇到错误。如果编译器总是根据你能想到的任何合法代码正确地做到这一点,程序员就不需要谈论这个问题,除非是为了性能,否则没有理由提出这个问题。如果您不控制 void ptr 地址是否对齐,那么您必须始终使用 mem2() 未对齐访问,或者您必须根据 ptr 作为 nik 的值在代码中执行 if-then-else指出。通过声明为 void,C 编译器现在无法正确处理您的对齐方式,并且无法保证。如果您采用 char *prt 并将其提供给这些函数,那么所有的赌注都在于编译器是否正确,而无需添加额外的代码,无论是埋在 mem2() 函数中还是在这两个函数之外。所以写在你的问题 mem2() 是唯一正确的答案。

          说在您的台式机/笔记本电脑中使用的 DRAM 往往是 64 或 72(带 ecc)位宽,并且对它们的每次访问都是对齐的。即使记忆棒实际上是由 8 位宽或 16 或 32 位宽的芯片组成的。 (由于各种原因,这可能会随着手机/平板电脑而改变)内存控制器和理想情况下至少有一个缓存位于该 DRAM 前面,以便处理小于总线宽度的未对齐甚至对齐访问读取-修改-写入在缓存 sram 中速度更快,并且 DRAM 访问都是对齐的全总线宽度访问。如果您在 DRAM 前面没有缓存并且控制器设计用于全宽度访问,那么这是最差的性能,如果设计用于单独点亮字节通道(假设 8 位宽芯片),那么您没有读取修改-writes 但更复杂的控制器。如果典型用例是缓存(如果设计中有缓存),那么在控制器中为每个字节通道进行额外的工作可能没有意义,但让它知道如何进行全总线宽度大小的传输或倍数。

          【讨论】:

            猜你喜欢
            • 2013-10-21
            • 1970-01-01
            • 2014-06-25
            • 2015-09-14
            • 2011-07-31
            • 2013-05-09
            • 1970-01-01
            • 2013-06-27
            相关资源
            最近更新 更多