我开始学习一些SIMD指令。我注意到某些函数有对齐和非对齐两个版本,例如_mm_store_si128
和_mm_storeu_si128
。我的问题是,这些函数是否执行不同的操作,如果没有,为什么会有两个不同的版本?
这些函数确实执行不同的操作。当使用对齐版本时,数据必须按照指定的对齐方式存储,否则可能会导致未定义的行为。而非对齐版本可以处理任意对齐方式的数据,但会降低性能。
我开始学习一些SIMD指令。我注意到某些函数有对齐和非对齐两个版本,例如_mm_store_si128
和_mm_storeu_si128
。我的问题是,这些函数是否执行不同的操作,如果没有,为什么会有两个不同的版本?
这些函数确实执行不同的操作。当使用对齐版本时,数据必须按照指定的对齐方式存储,否则可能会导致未定义的行为。而非对齐版本可以处理任意对齐方式的数据,但会降低性能。
在较老的CPU上,对齐和不对齐的加载/存储之间存在显著的性能差异。 在更近期的CPU上,差异要小得多,但作为“经验法则”,您仍应尽可能选择对齐版本。
我认为同样适用于标量,正确的对齐在任何情况下都是正确的,并且可以节省一些麻烦,从而实现最佳性能...
至于为什么未对齐的访问可能会更慢甚至不被支持 - 这是由于硬件的工作方式。假设您有一个64位整数和一个64位内存控制器,如果您的整数是正确对齐的,内存控制器可以一次性访问它。但是如果它是偏移的,内存控制器将不得不执行2个操作,加上CPU可能需要移动数据以正确组合它。由于这是次优的,因此一些平台甚至不会隐式地支持它,因为这样可以强制实现效率。
memcpy
这样的情况,可以进行一个16字节的非对齐拷贝,然后使用p &= ~15UL
来对齐指针(向下舍入),然后使用一个循环进行16字节的对齐拷贝。(其中的第一个拷贝可能与非对齐的前16字节重叠。)这样可以实现对齐性能而无需分支和代码膨胀。(无论如何,你仍然需要一个清理循环来处理最后不超过15字节的部分。) - undefined如果数据实际上已经对齐,那么未对齐的加载/存储将与对齐的存储具有相同的性能。
未对齐操作:未对齐的数据将导致轻微的性能损失,但程序仍然可以正常工作。
对齐操作:未对齐的数据将导致故障,让您检测到意外未对齐的数据,而不是悄无声息地导致性能损失。
现代CPU在未对齐加载方面具有很好的支持,但是当加载跨越缓存行边界时,仍会出现显着的性能损失。
使用SSE时,对齐的加载可以作为内存操作数折叠到其他操作中。这可以略微提高代码大小和吞吐量。
使用AVX时,两种类型的加载都可以折叠到其他操作中。(AVX默认行为是允许未对齐的内存操作数)。 如果对齐加载没有被折叠,并产生movdqa
或movaps
,则它们仍将在未对齐地址上发生错误。 这适用于128位操作的VEX编码,您可以通过正确的编译选项获得,而不需要更改使用128b内在函数的代码。
对于开始使用内在函数,我建议始终使用未对齐的加载/存储内在函数(但尝试让您的数据至少在常见情况下对齐)。如果您担心未对齐数据导致问题,则在性能调整时使用对齐的加载/存储内在函数。
*psrc
和*pdst
也可能不是相互四字节对齐的。在这种情况下,有四个选项:a.) 仅将源/加载四字节对齐,b.) 仅将目标/存储四字节对齐,c.) 两者都不四字节对齐,或者d.) 将源/加载四字节对齐,移动SIMD寄存器,将目标/存储四字节对齐。您能简要评论一下这种一般情况下的权衡和建议吗?谢谢。 - undefinedpalignr
是值得的,这样你可以进行对齐加载和对齐存储,从16字节对齐的加载中获取正确的16字节窗口。但是在具有高效非对齐加载/存储的CPU上,据我所知,通常最好对目标进行对齐,如果你可以以低成本实现对齐;许多x86 CPU每个时钟周期可以执行2次加载(拆分加载在某些情况下计为2次),但每个时钟周期只能提交1次存储到缓存。
由于存储可能不会在存储缓冲区中合并,这可能限制您每个时钟周期内进行的矢量存储次数少于1次。 - undefined