SSE: shuffle (permutevar) 4x32 整数

SSE: shuffle (permutevar) 4x32 integers

我有一些代码使用 AVX2 内在 _mm256_permutevar8x32_epi32 又名 vpermd 到 select 来自索引向量的输入向量的整数。现在我需要同样的东西,但 4x32 而不是 8x32。 _mm_permutevar_ps 用于浮点数,但我使用的是整数。

一个想法是 _mm_shuffle_epi32,但我首先需要将我的 4x32 索引值转换为单个整数,即:

imm[1:0] := idx[31:0]
imm[3:2] := idx[63:32]
imm[5:4] := idx[95:64]
imm[7:6] := idx[127:96]

我不确定这样做的最佳方式是什么,而且我也不确定这是继续进行的最佳方式。我正在寻找 Broadwell/Haswell 上最有效的方法来模拟 "missing" _mm_permutevar_epi32(__m128i a, __m128i idx)。如果可能的话,我宁愿使用 128 位指令而不是 256 位指令(即我不想扩大 128 位输入然后缩小结果)。

尽管 Peter Cordes 说的正确,AVX 指令 vpermilps 及其内在的 _mm_permutevar_ps() 可能会完成这项工作,但如果您使用的机器比 Sandy Bridge 更旧,即 SSE4。使用 pshufb 的 1 个变体也很好用。

AVX 变体

感谢@PeterCordes

#include <stdio.h>
#include <immintrin.h>


__m128i vperm(__m128i a, __m128i idx){
    return _mm_castps_si128(_mm_permutevar_ps(_mm_castsi128_ps(a), idx));
}


int main(int argc, char* argv[]){
    __m128i a   = _mm_set_epi32(0xDEAD, 0xBEEF, 0xCAFE, 0x0000);
    __m128i idx = _mm_set_epi32(1,0,3,2);
    __m128i shu = vperm(a, idx);
    printf("%04x %04x %04x %04x\n", ((unsigned*)(&shu))[3],
                                    ((unsigned*)(&shu))[2],
                                    ((unsigned*)(&shu))[1],
                                    ((unsigned*)(&shu))[0]);
    return 0;
}

SSE4.1 变体

#include <stdio.h>
#include <immintrin.h>


__m128i vperm(__m128i a, __m128i idx){
    idx = _mm_and_si128  (idx, _mm_set1_epi32(0x00000003));
    idx = _mm_mullo_epi32(idx, _mm_set1_epi32(0x04040404));
    idx = _mm_or_si128   (idx, _mm_set1_epi32(0x03020100));
    return _mm_shuffle_epi8(a, idx);
}


int main(int argc, char* argv[]){
    __m128i a   = _mm_set_epi32(0xDEAD, 0xBEEF, 0xCAFE, 0x0000);
    __m128i idx = _mm_set_epi32(1,0,3,2);
    __m128i shu = vperm(a, idx);
    printf("%04x %04x %04x %04x\n", ((unsigned*)(&shu))[3],
                                    ((unsigned*)(&shu))[2],
                                    ((unsigned*)(&shu))[1],
                                    ((unsigned*)(&shu))[0]);
    return 0;
}

这会编译成清晰的

0000000000400550 <vperm>:
  400550:       c5 f1 db 0d b8 00 00 00         vpand  0xb8(%rip),%xmm1,%xmm1        # 400610 <_IO_stdin_used+0x20>
  400558:       c4 e2 71 40 0d bf 00 00 00      vpmulld 0xbf(%rip),%xmm1,%xmm1        # 400620 <_IO_stdin_used+0x30>
  400561:       c5 f1 eb 0d c7 00 00 00         vpor   0xc7(%rip),%xmm1,%xmm1        # 400630 <_IO_stdin_used+0x40>
  400569:       c4 e2 79 00 c1                  vpshufb %xmm1,%xmm0,%xmm0
  40056e:       c3                              retq

如果您可以保证控制索引始终为 32 位整数 0、1、2 或 3,则 AND-masking 是可选的。

在 run-time 处生成一个立即数是没有用的,除非你正在 JIT 新代码。立即数是一个字节,它实际上是 machine-code 指令编码的一部分。如果你有一个 compile-time-constant 洗牌(在内联 + 模板扩展之后),那就太好了,否则忘记那些将控制操作数作为整数的洗牌 1.


在 AVX 之前, variable-control shuffle 是 SSSE3 pshufb。 (_mm_shuffle_epi8)。那是 still AVX2 中唯一的 128 位(或 in-lane)integer shuffle 指令,我认为是 AVX512。

AVX1 添加了一些 in-lane 32 位变量随机播放,例如 vpermilps (_mm_permutevar_ps)。 AVX2 添加了 lane-crossing 整数和 FP 混洗,但有点奇怪的是没有 vpermd 的 128 位版本。也许是因为英特尔微体系结构对整数数据使用 FP 洗牌没有任何惩罚。 (这在 Sandybridge 系列上是正确的,我只是不知道这是否是 ISA 设计的部分原因)。但是你会认为他们会为 vpermilps 添加 __m128i 内在函数,如果那是你要 "supposed" 做的。或者也许编译器/内在函数设计人员不同意 asm instruction-set 人?


如果你有一个 32 位索引的 runtime-variable 向量并且想要以 32 位粒度进行随机播放,到目前为止你最好的选择是只使用 AVX _mm_permutevar_ps.

_mm_castps_si128( _mm_permutevar_ps (_mm_castsi128_ps(a), idx) )

至少在 Intel 上,在像 paddd 这样的整数指令之间使用时,它甚至不会引入任何额外的旁路延迟;即 FP shuffles 特别是(不是混合)对 Sandybridge-family CPUs 中整数数据的使用没有惩罚。 =39=]

如果对 AMD Bulldozer 或 Ryzen 有任何惩罚,这比为 (v)pshufb.

计算 shuffle-control 矢量的成本小而且绝对便宜

使用 vpermd ymm 并忽略输入和输出的高 128 位(即通过使用强制转换函数)在 AMD 上会 慢很多(因为它的 128 位SIMD 设计必须将 lane-crossing 256 位洗牌分成几个 uops),而且在 Intel 上更糟,它使它成为 3c 延迟而不是 1 个周期。


@Iwill 的回答显示了一种从 4x32 位双字索引向量计算 pshufb 字节索引向量的方法。但它使用 SSE4.1 pmulld,在大多数 CPU 上是 2 微指令,很容易成为比随机播放更糟糕的瓶颈。 (请参阅该答案下的评论中的讨论。)特别是在没有 AVX 的旧 CPU 上,其中一些可以每个时钟执行 2 pshufb,这与现代英特尔不同(Haswell 和后来只有 1 个洗牌端口并且很容易成为瓶颈关于洗牌。根据英特尔的 Sunny Cove 演示,IceLake 将添加另一个洗牌端口。)

如果您确实必须编写此版本的 SSSE3 或 SSE4.1 版本,最好仍然只使用 SSSE3 并使用 pshufb 加上左移在双字中复制一个字节,然后再进行 ORing in 0,1,2,3 进入低位,而不是 pmulld。 SSE4.1 pmulld 是多个 uops,甚至比 pshufb 在某些 CPU 上更差 pshufb。 (在仅使用 SSSE3 而不是 SSE4.1,即 first-gen Core2,CPUs 上,您可能根本无法从矢量化中受益,因为它具有 slow-ish pshufb。)

在第二代 Core2 和 Goldmont 上,pshufb 是一条 single-uop 指令,具有 1 个周期的延迟。在 Silvermont 和 first-gen Core 2 上,它不是很好。但总体而言,如果 AVX 不可用,我建议 pshufb + pslld + por 为另一个 pshufb 计算 control-vector.

为准备洗牌而进行的额外洗牌比仅在任何支持 AVX 的 CPU 上使用 vpermilps 更糟糕。


脚注 1:

您必须使用 switch 或其他东西来 select 具有正确 compile-time-constant 整数的代码路径,这太可怕了;只有在您甚至没有可用的 SSSE3 时才考虑这一点。它可能比标量差,除非 jump-table 分支预测完美。