使用 SSE 的任意位置 2 输入混洗

Posted

技术标签:

【中文标题】使用 SSE 的任意位置 2 输入混洗【英文标题】:Arbitrary position 2-input shuffling using SSE 【发布时间】:2019-07-16 11:01:07 【问题描述】:

我有两个 4 分量向量,我将它们加载到两个 __m128 变量中。 然后我需要洗牌,使结果看起来像这样:

给定:

__m128 mmMin = _mm_load_ps(&glm::vec4(-1.0f,-2.0f,-3.0f,-4.0f)[0]);
__m128 mmMax = _mm_load_ps(&glm::vec4(1.0f,2.0f,3.0f,4.0f)[0]);

我希望随机播放的结果如下所示:

 //    mmMin.x,mmMax.x,mmMin.x,mmMax.x

但我认为_mm_shuffle_ps 是不可能的。

来自SSE docs 我看到_mm_shuffle_ps 总是戴着面具 首先从 __m128 的低 2 个分量插入结果 2 个值,然后从高 2 个分量插入 2 个值。

SPU 内部函数具有si_shufb 方法,该方法允许定义基于qword 的掩码并随机播放我希望的任何位置。 SSE有类似的方法吗?

我正在使用 SSE2,但也很高兴看到它可以如何与其他版本(包括 AVX)一起使用。

【问题讨论】:

可能最多可以使用 SSE4,但更喜欢保持在 SSE2 级别。 你检查gcc的__builtin_shuffle或clang的__builtin_shufflevector生成什么了吗? 有那个功能。有什么功能? .x 是哪个成员?您可能需要unpcklps,然后是movsldupunpcklpd 来复制下半部分。 (或shufps same,same,但在旧 CPU 上可能会更慢,并且 AVX 版本需要额外的字节)。要在 1 次随机播放中执行此操作,您可能需要 AVX512F vpermt2ps。 (这些指令都有内在函数,但助记符更容易记住和输入)。在 AVX512F 之前没有 2 输入可变控制随机播放,我认为任何固定随机播放都不够灵活,shufps 是最接近的。 【参考方案1】:

仅使用 SSE2,我认为您需要 2 次随机播放:unpcklps 交错,然后 unpcklpd same,sameshufps same,same 广播低 64 位。

使用 AVX512F,vpermt2ps 可以一次性完成此操作(使用控制向量);我认为在 AVX2 或更早版本中没有任何 2-source shuffle,在此之前具有足够精细的粒度和灵活的源位置。并且没有固定的随机播放重复元素以及交错。

2-source shuffle 在 AVX512 之前很少见:主要是固定的 shuffle,例如 unpckl/h*palignr。在那之前,它主要是[v]shufps / [v]shufpd。可变控制洗牌也很少见:在 AVX 之前,唯一的一个是 pshufb。 AVX1/2 添加了一些可变控制双字元素随机播放,但仅适用于 1 个源。在 AVX512 之前,没有可变控制 2 源 shuffle。

立即洗牌需要 4 组以上的 2 位索引来处理对两个 4 元素向量串联的任意索引。但是 x86 SIMD 指令始终最多有一个 8 位立即数操作数。 不幸的是,没有像 ARM 这样的广播立即数可以有效地创建 1.0f 或其他值的向量。


AVX

由于您只需要每个向量中的 1 个元素,因此您可以使用 AVX 广播加载而不是加载整个向量,然后使用 vblendps

广播负载的成本与英特尔 CPU 上的正常负载相同(不要为 shuffle 端口花费 uop,纯粹在加载端口中处理)。在 AVX512F 之前,它们不能折叠到 ALU 指令的内存操作数中,但它们确实避免了 shuffle-port 瓶颈。 AMD CPU 可能仍需要 ALU uop,但它们有更多的 shuffle ALU,因此 shuffle 吞吐量几乎不是瓶颈。 (https://agner.org/optimize/)

不幸的是,Ryzen vbroadcastss xmm, [mem] 是 2 个独立的前端微指令,但它仍然具有每时钟 2 个吞吐量。

blend-immediate 在 dword 和更高版本的元素上非常有效,可以在 Haswell 及更高版本的任何端口上运行,或者在 SnB/IvB 和 Ryzen 上的 2 个端口上运行。但即使在 Nehalem 上仍然存在单 uop / 1c 延迟。

#include <immintrin.h>
__m128 broadcast_interleave_scalars_avx(const float *min, const float *max) 
    __m128 minx = _mm_broadcast_ss(min);
    __m128 maxx = _mm_broadcast_ss(max);
    return _mm_blend_ps(minx, maxx, 0b1010);

On Godbolt,clang 的 asm cmets 确认我得到了正确的混合常量:

    vbroadcastss    xmm0, dword ptr [rdi]
    vbroadcastss    xmm1, dword ptr [rsi]
    vblendps        xmm0, xmm0, xmm1, 10 # xmm0 = xmm0[0],xmm1[1],xmm0[2],xmm1[3]

如果您的数据已经在寄存器中,而不是新加载的,您可能只想使用 2 次随机播放。


使用 SSE4.1,您可以执行 2 次 movddup 加载以从内存中广播 64 位(包括您关心的 32 位)然后 blendps。第一次加载将加载超过您关心的float 的 32 位,第二次将加载您关心的 float之前的 32 位。

要让 C++ 编译器为您生成此代码,您必须将指针强制转换为 double* 以加载 __m128d _mm_loaddup_pd (double const* mem_addr),然后使用 _mm_castpd_ps__m128d 获取 __m128

https://www.felixcloutier.com/x86/movsldup 也可用于设置 unpcklps

【讨论】:

_mm_broadcast_ss 的绝妙技巧,虽然我希望有类似 SPU 的单指令命令来执行这样的随机播放。 @MichaelIV:有 AVX512 vpermt2d。 SPU 是 PowerPC Altivec,对吧?不幸的是,在 AVX512F 之前,x86 SIMD 的 2 源 shuffle 非常有限,没有什么能与从 2x 16 字节向量的串联中选择 16 字节的灵活性相匹配。 (直到 AVX512VBMI vpermt2b 顺便说一句,由于 2 输入 uop 限制,在 Skylake 之前,两输入变量洗牌是不可能的。 FMA 一开始就很老套。 @Mysticial:Broadwell 添加了对 1-uop cmov、adc、sbb 的支持。 (和 ADCX/ADOX)。但在 FMA 之外,没有 3 输入向量指令; 1-uop 非 VEX blendvps xmm, xmm(隐式 XMM0)是 Skylake 的新功能。无论如何,是的,关于 uop 输入限制的好点。因此,再加上没有足够位用于完全灵活的 2 输入 shuffle 的 imm8 几乎排除了英特尔在 AVX2 或更早版本中将其添加为立即或矢量控制的希望。 Bulldozer 系列中可能有过这样的 AMD XOP 洗牌,但目前这几乎无关紧要。 我最近从 Bulldozer 优化的二进制文件中删除了 XOP,因为测试它们变得越来越难。 (我很少再打开我的推土机了。) Zen 1/+ 仍然(秘密地)支持 FMA4。但他们甚至从 Zen 2 中拿走了这一点。所以我有点难过。 :(

以上是关于使用 SSE 的任意位置 2 输入混洗的主要内容,如果未能解决你的问题,请参考以下文章

地理输入 - 如何输入、存储和显示位置

怎么利用python绘制sse值与k值的函数图像

百度地图为啥不能任意设置起点和终点

C++编写一个程序,将用户输入的十进制整数转换成任意进制的数

百度地图怎么设置起点

SSE4内存与差异位置比较