SIMD 内在函数:对齐操作与未对齐操作不同吗?
SIMD intrinsics: aligned operation different than unaligned?
我开始学习一些关于 SIMD 内在函数的知识。我注意到对于某些函数,存在对齐和未对齐的版本,例如 _mm_store_si128
和 _mm_storeu_si128
。我的问题是,这些函数的执行方式是否不同?如果不是,为什么会有两个不同的版本?
在较旧的 CPU 上,对齐和未对齐之间存在显着的性能差异 loads/stores。在较新的 CPU 上,差异要小得多,但作为 "rule of thumb" 你仍然应该尽可能喜欢对齐版本。
我会说 "always align (wherever possible)",这样无论如何您都可以得到保障。一些平台不支持未对齐访问,其他平台会显着降低性能。如果您选择对齐访问,则在任何情况下都将获得最佳性能。在某些平台上可能会有少量内存成本,但这是非常值得的,因为如果你选择 SIMD,那意味着你追求性能。我想不出为什么要实现未对齐的代码路径。也许如果你不得不处理一些旧的设计,它不是在考虑 SIDM 的情况下构建的,但我会说这种可能性很小 none。
我想说这同样适用于标量,在任何情况下正确对齐都是正确的,并且在实现最佳性能时为您省去一些麻烦...
至于为什么未对齐的访问可能会变慢甚至不受支持 - 这是因为硬件的工作方式。假设您有一个 64 位整数和一个 64 位内存控制器,如果您的整数正确对齐,内存控制器可以一次性访问它。但如果它是偏移的,内存控制器将不得不执行 2 个操作,加上 CPU 可能需要移动数据以正确组合它。由于这是次优的,一些平台甚至不隐含地支持它,作为提高效率的手段。
如果数据实际上是对齐的,则未对齐的加载/存储与对齐的存储具有相同的性能。
unaligned ops:未对齐的数据会对性能造成小的影响,但您的程序仍然有效。
对齐操作:未对齐的数据会导致错误,让您检测到意外未对齐的数据,而不是默默地导致性能下降。
现代 CPU 对未对齐的负载有很好的支持,但当负载跨越缓存行边界时,性能仍然会受到显着影响。
使用 SSE 时,对齐加载可以折叠到其他操作中作为内存操作数。这会稍微改善代码大小和吞吐量。
使用 AVX 时,这两种负载都可以折叠到其他操作中。 (AVX 默认行为是允许未对齐的内存操作数)。如果对齐的加载没有折叠,并产生 movdqa
或 movaps
,那么它们仍然会在未对齐的地址上出错。这甚至适用于 128 位操作的 VEX 编码,您可以使用正确的编译选项获得它,而无需使用 128b 内在函数对代码进行源代码更改。
为了开始使用内部函数,我建议始终使用未对齐的 load/store 内部函数。 (但至少在常见情况下尽量让您的数据保持一致)。如果您担心未对齐的数据会导致问题,请在性能调整时使用对齐。
我开始学习一些关于 SIMD 内在函数的知识。我注意到对于某些函数,存在对齐和未对齐的版本,例如 _mm_store_si128
和 _mm_storeu_si128
。我的问题是,这些函数的执行方式是否不同?如果不是,为什么会有两个不同的版本?
在较旧的 CPU 上,对齐和未对齐之间存在显着的性能差异 loads/stores。在较新的 CPU 上,差异要小得多,但作为 "rule of thumb" 你仍然应该尽可能喜欢对齐版本。
我会说 "always align (wherever possible)",这样无论如何您都可以得到保障。一些平台不支持未对齐访问,其他平台会显着降低性能。如果您选择对齐访问,则在任何情况下都将获得最佳性能。在某些平台上可能会有少量内存成本,但这是非常值得的,因为如果你选择 SIMD,那意味着你追求性能。我想不出为什么要实现未对齐的代码路径。也许如果你不得不处理一些旧的设计,它不是在考虑 SIDM 的情况下构建的,但我会说这种可能性很小 none。
我想说这同样适用于标量,在任何情况下正确对齐都是正确的,并且在实现最佳性能时为您省去一些麻烦...
至于为什么未对齐的访问可能会变慢甚至不受支持 - 这是因为硬件的工作方式。假设您有一个 64 位整数和一个 64 位内存控制器,如果您的整数正确对齐,内存控制器可以一次性访问它。但如果它是偏移的,内存控制器将不得不执行 2 个操作,加上 CPU 可能需要移动数据以正确组合它。由于这是次优的,一些平台甚至不隐含地支持它,作为提高效率的手段。
如果数据实际上是对齐的,则未对齐的加载/存储与对齐的存储具有相同的性能。
unaligned ops:未对齐的数据会对性能造成小的影响,但您的程序仍然有效。
对齐操作:未对齐的数据会导致错误,让您检测到意外未对齐的数据,而不是默默地导致性能下降。
现代 CPU 对未对齐的负载有很好的支持,但当负载跨越缓存行边界时,性能仍然会受到显着影响。
使用 SSE 时,对齐加载可以折叠到其他操作中作为内存操作数。这会稍微改善代码大小和吞吐量。
使用 AVX 时,这两种负载都可以折叠到其他操作中。 (AVX 默认行为是允许未对齐的内存操作数)。如果对齐的加载没有折叠,并产生 movdqa
或 movaps
,那么它们仍然会在未对齐的地址上出错。这甚至适用于 128 位操作的 VEX 编码,您可以使用正确的编译选项获得它,而无需使用 128b 内在函数对代码进行源代码更改。
为了开始使用内部函数,我建议始终使用未对齐的 load/store 内部函数。 (但至少在常见情况下尽量让您的数据保持一致)。如果您担心未对齐的数据会导致问题,请在性能调整时使用对齐。