SIMD指令集:对齐操作和非对齐操作有何不同?

3

我开始学习一些SIMD指令。我注意到某些函数有对齐和非对齐两个版本,例如_mm_store_si128_mm_storeu_si128。我的问题是,这些函数是否执行不同的操作,如果没有,为什么会有两个不同的版本?

这些函数确实执行不同的操作。当使用对齐版本时,数据必须按照指定的对齐方式存储,否则可能会导致未定义的行为。而非对齐版本可以处理任意对齐方式的数据,但会降低性能。


3
SSE: why, technically, is 16-aligned data faster to move?: 为什么在SSE中16字节对齐的数据移动更快?[SSE:为什么从技术角度来看,16字节对齐的数据移动更快呢?]Purpose of memory alignment: 内存对齐的目的是什么?[内存对齐的目的是什么?]Are word-aligned loads faster than an unaligned loads on x64 processors: 在x64处理器上,字对齐加载是否比非对齐加载更快?[x64处理器上,字对齐加载是否比非对齐加载更快?] - undefined
3个回答

2

在较老的CPU上,对齐和不对齐的加载/存储之间存在显著的性能差异。 在更近期的CPU上,差异要小得多,但作为“经验法则”,您仍应尽可能选择对齐版本。


对你来说,什么是一台旧的CPU? - undefined
在Intel的Nehalem家族(即第一代Core i7之前)之前的任何内容。 - undefined

2
我建议“尽可能对齐”,这样无论如何都能得到保障。有些平台不支持未对齐的访问,而其他平台则会出现显著的性能下降。如果您选择对齐访问,则无论如何都能获得最佳性能。在某些平台上可能会有一些内存成本,但这是非常值得的,因为如果您使用SIMD,那么意味着您追求性能。我想不出任何理由为什么要实现未对齐的代码路径。也许只有当您需要处理一些旧设计时,该设计并没有考虑SIDM,但我认为这种情况的几率很小。

我认为同样适用于标量,正确的对齐在任何情况下都是正确的,并且可以节省一些麻烦,从而实现最佳性能...

至于为什么未对齐的访问可能会更慢甚至不被支持 - 这是由于硬件的工作方式。假设您有一个64位整数和一个64位内存控制器,如果您的整数是正确对齐的,内存控制器可以一次性访问它。但是如果它是偏移的,内存控制器将不得不执行2个操作,加上CPU可能需要移动数据以正确组合它。由于这是次优的,因此一些平台甚至不会隐式地支持它,因为这样可以强制实现效率。


2
你的总体观点是正确的,但在一些情况下,未对齐的加载/存储操作仍然是最佳选择,即使在旧的CPU上也是如此。 - undefined
1
好的 - 我之前有点时间紧迫,但一个例子是在SSSE3之前的CPU上进行邻域操作 - 在MNI之前没有有用的双向量水平移位指令,所以非对齐加载通常是必要的恶。还有一些情况下,非对齐访问更方便,并且如果有足够多的其他操作,延迟可以被隐藏起来。其他例子包括:支持外部API(例如,如果您想编写一个SIMD优化的memcpy或其他标准/遗留函数)。 - undefined
@PaulR:解释得很好,但我要补充一点,在处理大缓冲区时,通常你会希望对大部分数据进行对齐操作。可以通过进行较小的移动直到到达对齐边界,并对剩余部分进行对齐操作。或者对于像memcpy这样的情况,可以进行一个16字节的非对齐拷贝,然后使用p &= ~15UL来对齐指针(向下舍入),然后使用一个循环进行16字节的对齐拷贝。(其中的第一个拷贝可能与非对齐的前16字节重叠。)这样可以实现对齐性能而无需分支和代码膨胀。(无论如何,你仍然需要一个清理循环来处理最后不超过15字节的部分。) - undefined
@ddriver:对于一个大缓冲区来说,如果它们是不对齐的,每4次读取就会跨越一个缓存行。这仍然会有一定的惩罚。如果你不受L1缓存带宽(读/写端口uops)或内存延迟的限制,这就不是一个很大的问题。对于一个小缓冲区来说,如果2个不对齐的读取可以获取到所有数据,那么没有必要额外工作使其中一个加载对齐。通常情况下,为了使其余的加载对齐,需要一些额外的启动代码,如果没有足够的好处来摊销这个成本,那么这并不值得。 - undefined
2
相反,非HPC代码可以并且确实从SIMD中获益匪浅。看看整数压缩代码或DNA处理就知道了。如果你认为SIMD只适用于HPC,那你完全错了(SIMD指令集被应用于消费者和企业CPU,并从一开始就面向消费者和商业市场)。如果你认为HPC工作负载只使用对齐加载,那也是错误的(尽管大多数情况下是这样)-整个HPC领域不仅仅是LINPACK和朋友们。 - undefined
显示剩余5条评论

2

如果数据实际上已经对齐,那么未对齐的加载/存储将与对齐的存储具有相同的性能。

  • 未对齐操作:未对齐的数据将导致轻微的性能损失,但程序仍然可以正常工作。

  • 对齐操作:未对齐的数据将导致故障,让您检测到意外未对齐的数据,而不是悄无声息地导致性能损失。

现代CPU在未对齐加载方面具有很好的支持,但是当加载跨越缓存行边界时,仍会出现显着的性能损失。

使用SSE时,对齐的加载可以作为内存操作数折叠到其他操作中。这可以略微提高代码大小和吞吐量。

使用AVX时,两种类型的加载都可以折叠到其他操作中。(AVX默认行为是允许未对齐的内存操作数)。 如果对齐加载没有被折叠,并产生movdqamovaps,则它们仍将在未对齐地址上发生错误。 这适用于128位操作的VEX编码,您可以通过正确的编译选项获得,而不需要更改使用128b内在函数的代码。

对于开始使用内在函数,我建议始终使用未对齐的加载/存储内在函数(但尝试让您的数据至少在常见情况下对齐)。如果您担心未对齐数据导致问题,则在性能调整时使用对齐的加载/存储内在函数。


1
如果数据实际上是对齐的,那么未对齐的加载/存储与对齐的存储在性能上是相同的。这仅适用于Sandy Bridge及更高版本,我想是这样的吧?(不确定AMD在这方面的情况。) - undefined
2
@PaulR:实际上,根据 http://agner.org/optimize/ 上的指令表,从 Nehalem 及以后的处理器开始支持对齐和非对齐加载/存储操作。同样适用于 AMD Bulldozer 或更新的处理器。(即使 K10 没有对负载有惩罚,但是将非对齐存储到对齐地址会更慢)。所以在现阶段,使用对齐加载内联函数的主要原因是允许将加载操作折叠到内存操作数中,因为 AVX 还没有普及开来。(即使 Silvermont 也不支持它。) - undefined
感谢您的澄清 - 我并不完全确定。 - undefined
尚未讨论的是,对于给定任意指针的memcpy函数,在考虑各自的虚拟内存对齐情况之外,*psrc*pdst也可能不是相互四字节对齐的。在这种情况下,有四个选项:a.) 仅将源/加载四字节对齐,b.) 仅将目标/存储四字节对齐,c.) 两者都不四字节对齐,或者d.) 将源/加载四字节对齐,移动SIMD寄存器,将目标/存储四字节对齐。您能简要评论一下这种一般情况下的权衡和建议吗?谢谢。 - undefined
@GlennSlayden:在旧的CPU上,比如Core 2,使用palignr是值得的,这样你可以进行对齐加载和对齐存储,从16字节对齐的加载中获取正确的16字节窗口。但是在具有高效非对齐加载/存储的CPU上,据我所知,通常最好对目标进行对齐,如果你可以以低成本实现对齐;许多x86 CPU每个时钟周期可以执行2次加载(拆分加载在某些情况下计为2次),但每个时钟周期只能提交1次存储到缓存。 由于存储可能不会在存储缓冲区中合并,这可能限制您每个时钟周期内进行的矢量存储次数少于1次。 - undefined
显示剩余2条评论

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