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

Posted

技术标签:

【中文标题】使用最少的指令将 4 个单精度浮点数加载并复制到打包的 __m256 变量中【英文标题】:Load and duplicate 4 single precision float numbers into a packed __m256 variable with fewest instructions 【发布时间】:2021-06-16 19:20:16 【问题描述】:

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

【问题讨论】:

您是否有可用的 AVX2,用于为 vpmovzdq ymm, mem(英特尔上为 2 微指令)设置 vmovsldup?或者只是 AVX2 vpermps 在 128 位加载后使用随机向量常量。 @PeterCordes 是的,AVX2 可用。我的目标是一个普通的桌面 CPU 好的,那么我建议您接受我的回答。它至少与 Mike 对带有 AVX2 的现代主流 CPU 的回答一样好。 (或使用 clang,编译为相同的 asm。) 【参考方案1】:

如果您的数据是另一个向量计算的结果(并且在 __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

【讨论】:

如果访问相邻元素 p[-2], ..., p[5] 是保存的,还可以加载该向量而不是广播并进行通道内随机播放。 我真的希望我对编译器和选择编译器有更多的了解,这对我来说就像一个黑匣子,所以我宁愿在这个阶段不考虑编译器因素 @Noob:是的,如果您希望您的代码能够与多个编译器很好地编译,我建议您在花时间弄清楚它的外观后编写最佳内在函数。 (例如,通过查看来自 _mm256_setr_ps 的 clang 输出并将其转换回内在函数。)但是使用像 clang 这样的好的编译器意味着您可以用更少的工作获得好的结果,并且编译器通常会找到更好的方法来做事/如果您自己没有想到最好的方法。这个答案的重点是第一个代码块,使用_mm256_broadcast_ps_mm256_permutevar_ps @chtz 我明白了,加载似乎比广播有更小的延迟。或者加载包含 A、B、C、D 的任何内容,例如 p[0]~p[7],只要没有发生内存访问冲突。 @chtz 和 Noob:但另请参阅 agner.org/optimize/blog/read.php?i=872&v=f#854 - 256 位向量操作消耗的 128 位零扩展负载也有额外的延迟。所以实际上可能是 7+1 与 7+3 周期。而 256 位 vmovups ymm, [mem] 负载为 7 个周期。 (或者更糟糕的是缓存行拆分,所以在具有廉价vbroadcastf128 的 CPU 上最好这样做。)【参考方案2】:

_mm_load_ps -> _mm256_castps128_ps256 -> _mm256_permute_ps

【讨论】:

_mm256_permute_psvpermilps 的内在函数,因为英特尔在命名其内在函数时目光短浅,对 AVX1 指令使用“permute”,然后不得不使用 _mm256_permutexvar_ps aka _mm256_permutevar8x32_psvpermps 一旦AVX2 出现。但是,是的,如果您有 AVX2,这是最有效的方法,特别是如果您只能加载一个随机播放控制向量。 否则对于 AVX1,我猜是 VBROADCASTF128 / _mm_permutevar_ps (vpermilps ymm, ymm, ymm 带有矢量控制)。实际上,这更好,因为广播负载与常规负载一样便宜,并且通道内随机播放延迟更低,在 Zen 1 上更快。 阅读 Intels 文档后,我仍然不明白 permute 是如何工作的。但是下面的这个链接真的很有帮助。 codeproject.com/Articles/874396/… @Noob: _mm256_permute_ps 实际上适用于此;这个答案有一个有效的想法,但对vpermps 使用了错误的内在函数。 __m256 _mm256_permute_ps (__m256 a, int control);vpermilps 的内在函数,具有立即控制操作数(每个 128 位通道中的相同随机播放)(felixcloutier.com/x86/vpermilps)。 @PeterCordes 你是对的! _mm256_permutevar_ps 应该做的工作

以上是关于使用最少的指令将 4 个单精度浮点数加载并复制到打包的 __m256 变量中的主要内容,如果未能解决你的问题,请参考以下文章

怎么将4字节16进制转化成浮点数

Abplc浮点数怎么传给4个字节

怎么将4字节16进制转化成浮点数

如何将 4 个浮点数的 ps 向量转换为 4 个双精度数并存储到 pd 数组?

将字节转换为浮点数?

4字节单精度二进制浮点数的解码