【问题标题】:Vectorize equality test without SIMD没有 SIMD 的向量化相等测试
【发布时间】:2020-01-05 00:32:01
【问题描述】:

我想对一个相等性测试进行矢量化,其中将矢量中的所有元素与相同的值进行比较,并将结果写入一个 8 位字数组。结果数组中的每个 8 位字应为零或一。 (这有点浪费,但是在这个问题中,对布尔值进行打包并不是一个导入细节)。这个函数可以写成:

#include <stdint.h>

void vecEq (uint8_t* numbers, uint8_t* results, int len, uint8_t target) {
  for(int i = 0; i < len; i++) {
    results[i] = numbers[i] == target;
  }
}

如果我们知道两个向量都是 256 位对齐的,我们可以首先将 target 广播到 AVX 寄存器,然后使用 SIMD 的 _mm256_cmpeq_epi8 一次执行 32 次相等测试。但是,在我正在使用的设置中,numbersresults 都已由运行时分配(GHC 运行时,但这无关紧要)。它们都保证是 64 位对齐的。有没有办法向量化这个操作,最好不使用 AVX 寄存器?

我考虑过的方法是将 8 位字预先广播到 64 位字,然后一次与 8 个元素进行异或运算。这不起作用,因为我找不到矢量化方法将 XOR 的结果(零表示相等,其他任何表示不相等)转换为我需要的相等测试结果(0 表示不相等,1 表示相等,不应该存在其他任何东西)。粗略地说,我的草图是:

void vecEq (uint64_t* numbers, uint64_t* results, int len, uint_8 target) {
  uint64_t targetA = (uint64_t)target;
  uint64_t targetB = targetA<<56 | targetA<<48 | targetA<<40 | targetA<<32 | targetA<<24 | targetA<<16 | targetA<<8 | targetA;
  for(int i = 0; i < len; i++) {
    uint64_t tmp = numbers[i] ^ targetB;
    results[i] = ... something with tmp ...;
  }
}

【问题讨论】:

  • AVX 不像 SSE 那样支持未对齐的负载?
  • 您使用的是什么编译器,您的目标平台是什么?所有三个主要编译器的当前版本已经按原样矢量化您的代码:godbolt.org/z/-p_MxP...
  • 您可以将uint64_t*s 转换为uint8_t*s 并执行原始循环,但是当我尝试它时,这并不会改变生成的(已经矢量化,如 Michael Kenzel 所说的)代码。如果适用于您的情况,请确保指定 restrict(即,如果 numbersresults 不能重叠)。

标签: c x86 vectorization memory-alignment avx


【解决方案1】:

进一步了解上面的 cmets(代码将很好地矢量化)。如果您使用的是 AVX,最好的策略通常是使用未对齐的加载/存储内在函数。如果您的数据碰巧对齐,它们不会产生额外成本,并且在硬件不对齐的情况下,它们的价格与硬件一样便宜。 (在 Intel CPU 上,跨越两个缓存行的加载/存储仍然会受到惩罚,也就是缓存行拆分)。

理想情况下,您仍然可以将缓冲区对齐 32,但如果您的数据必须来自 L2 或 L3 或 RAM,则未对齐通常不会产生可测量的差异。处理可能的未对齐的最佳策略通常只是让硬件处理它,而不是标量到对齐边界或像您对 SSE 或 AVX512 所做的事情,对齐再次很重要(任何未对齐都会导致每个加载/存储是缓存行拆分)。

只需使用_mm256_loadu_si256 / _mm256_storeu_si256 就可以了。

有趣的是,Visual C++ 将不再发出对齐的加载或存储,即使您请求它们。 https://godbolt.org/z/pL9nw9(例如 vmovups 而不是 vmovaps)

如果使用 GCC 编译,您可能希望使用 -march=haswell-march=znver1 而不仅仅是 -mavx2,或者至少是 -mno-avx256-split-unaligned-load-mno-avx256-split-unaligned-store so 256-bit unaligned loads compile to single instructions. 受益于这些 tune=generic 默认值的 CPU 不要'不支持 AVX2,例如 Sandybridge 和 Piledriver。

【讨论】:

  • 从技术上讲,是否存在未对齐负载的性能影响是处理器模型的一个特性,而不是 AVX。虽然未对齐的负载通常以与对齐的负载相同的速度执行,但我相信(从一段时间前的内存中)它们可能仍然使用更多的处理器资源,这可能会对某些工作负载产生明显的性能影响。 (在某些情况下确实如此——按列遍历数组有时会使用两倍于未对齐加载的缓存行,这会严重影响性能。)
  • 缓存行拆分负载仍然有点贵! 对齐数据上的非对齐加载没有额外成本。因此,理想情况下,您确实将缓冲区对齐 32。但无论如何,是的,即使您不能保证对齐,这里最好的策略仍然是 _mm256_loadu_si256。另请参阅Why doesn't gcc resolve _mm256_loadu_pd as single vmovupd? - 确保您使用gcc -march=haswell 而不仅仅是-mavx2,否则它将针对 Sandybridge / Bulldozer 进行优化并将_mm256_loadu_si256 拆分为vmovdqu xmm / vinserti128,即使 SnB 无法运行 AVX2 代码。
  • @EricPostpischil:在 Intel CPU 上,只要不跨越缓存线边界,错位的额外成本几乎为零。但在这种情况下,加载 uop 必须重放才能从其他缓存行加载。因此,除了缓存占用空间和额外的延迟之外,它在加载/存储端口上使用了更多的后端周期。 (How can I accurately benchmark unaligned access speed on x86_64 总结了一些效果;它们对于标量和 SIMD 负载是相同的)。
猜你喜欢
  • 1970-01-01
  • 2013-03-10
  • 2017-04-21
  • 2011-11-04
  • 2011-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多