用最少的指令将 4 个单精度浮点数加载并复制到打包的 __m256 变量中

Load and duplicate 4 single precision float numbers into a packed __m256 variable with fewest instructions

我有一个包含 A、B、C、D 4 个浮点数的浮点数组,我希望将它们加载到 __m256 变量中,例如 AABBCCDD。最好的方法是什么? 我知道使用 _mm256_set_ps() 始终是一个选项,但使用 8 CPU 指令似乎很慢。谢谢

_mm_load_ps -> _mm256_castps128_ps256 -> _mm256_permute_ps

如果您的数据是另一个向量计算的结果(并且在 __m128 中),您希望 AVX2 vpermps (_mm256_permutexvar_ps) 的控制向量为 _mm256_set_epi32(3,3, 2,2, 1,1, 0,0).

vpermps ymm 在 Intel 上是 1 微指令,但在 Zen2 上是 2 微指令(具有 2 个周期吞吐量)。 Zen1 上为 3 微指令,每 4 个时钟吞吐量为 1。 (https://uops.info/)

如果它是单独标量计算的结果,您可能希望将它们与 _mm_set_ps(d,d, c,c)(1x vshufps)一起洗牌以设置 vinsertf128。


但是对于内存中的数据,我认为你最好的选择是 128 位广播加载,然后是通道内随机播放。它只需要 AVX1,而在现代 CPU 上,它在 Zen2 和 Haswell 及更高版本上是 1 负载 + 1 随机播放 uop。它在 Zen1 上也很有效:唯一的交叉洗牌是 128 位广播负载。

在 Intel 和 Zen2(256 位洗牌执行单元)上,使用通道内洗牌比通道交叉的延迟更低。这仍然需要一个 32 字节的洗牌控制向量常量,但如果您需要经常这样做,它通常/希望在缓存中保持热。

__m256  duplicate4floats(void *p) {
   __m256 v = _mm256_broadcast_ps((const __m128 *) p);   // vbroadcastf128
   v = _mm256_permutevar_ps(v, _mm256_set_epi32(3,3, 2,2,  1,1, 0,0));  // vpermilps
   return v;
}

现代 CPU 直接在加载端口处理广播加载,不需要随机播放 uop。 (Sandybridge 确实需要 vbroadcastf128 的端口 5 shuffle uop,与更窄的广播不同,但 Haswell 和更高版本纯粹是端口 2/3。但是 SnB 不支持 AVX2,因此粒度小于 128- 的通道交叉洗牌位不是一个选项。)

所以即使 AVX2 可用,我认为 AVX1 指令在这里更有效。在 Zen1 上,vbroadcastf128 是 2 微指令,而 128 位 vmovups 是 1,但是 vpermps(车道交叉)是 3 微指令,而 vpermilps 是 2。

不幸的是,clang 将其悲观化为 vmovups 加载和 vpermps ymm,但 GCC 将其编译为所写的。 (Godbolt)


如果您想避免使用洗牌控制向量常量,vpmovzxdq ymm, [mem](英特尔 2 微指令)可以为 vmovsldup(1 微指令通道内洗牌)设置元素。或者 broadcast-load 和 vunpckl/hps 然后混合?


I know using _mm256_set_ps() is always an option but it seems slow with 8 CPU instructions.

那就换个更好的编译器吧! (或者记得启用优化。)

__m256  duplicate4floats_naive(const float *p) {
   return _mm256_set_ps(p[3],p[3], p[2], p[2], p[1],p[1], p[0],p[0]);
}

用 gcc (https://godbolt.org/z/dMzh3fezE) 编译成

duplicate4floats_naive(float const*):
        vmovups xmm1, XMMWORD PTR [rdi]
        vpermilps       xmm0, xmm1, 80
        vpermilps       xmm1, xmm1, 250
        vinsertf128     ymm0, ymm0, xmm1, 0x1
        ret

所以 3 次随机播放 uops,不太好。它本可以使用 vshufps 而不是 vpermilps 来节省代码大小,并让它在 Ice Lake 的更多端口上使用 运行。但仍然比 8 条指令好得多。

clang 的 shuffle 优化器与我的优化内在函数生成相同的 asm,因为 clang 就是这样。这是相当不错的优化,只是不是很理想。

duplicate4floats_naive(float const*):
        vmovups xmm0, xmmword ptr [rdi]
        vmovaps ymm1, ymmword ptr [rip + .LCPI1_0] # ymm1 = [0,0,1,1,2,2,3,3]
        vpermps ymm0, ymm1, ymm0
        ret