如果您的数据是另一个向量计算的结果(并且在 __m128 中),您需要 AVX2 vpermps (_mm256_permutexvar_ps) 和 _mm256_set_epi32(3,3, 2,2, 1,1, 0,0) 的控制向量。
vpermps ymm 在 Intel 上是 1 uop,但在 Zen2 上是 2 uop(具有 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 个 shuffle uop。它在 Zen1 上也很有效:唯一的车道交叉洗牌是 128 位广播负载。
在 Intel 和 Zen2(256 位 shuffle 执行单元)上,使用车道内随机播放的延迟低于车道交叉。这仍然需要一个 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 直接在加载端口处理广播负载,无需 shuffle uop。 (Sandybridge 确实需要为vbroadcastf128 提供端口 5 shuffle uop,这与更窄的广播不同,但 Haswell 及更高版本是纯粹的端口 2/3。但 SnB 不支持 AVX2,因此粒度小于 128 位的车道交叉 shuffle 不是'不是一个选项。)
所以即使 AVX2 可用,我认为 AVX1 指令在这里更有效。在 Zen1 上,vbroadcastf128 为 2 微秒,而 128 位 vmovups 为 1,但vpermps(车道交叉)为 3 微秒,vpermilps 为 2。
不幸的是,clang 将其悲观为 vmovups 加载和 vpermps ymm,但 GCC 将其编译为书面形式。 (Godbolt)
如果您想避免使用随机控制向量常量,vpmovzxdq ymm, [mem](Intel 上为 2 微指令)可以为vmovsldup(1 微指令在通道随机播放)设置元素。还是广播加载和vunpckl/hps 然后混合?
我知道使用 _mm256_set_ps() 始终是一种选择,但使用 8 个 CPU 指令似乎很慢。
那么,获得更好的编译器吧! (或者记得启用优化。)
__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 次随机播放,不是很好。它可以使用 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