SSE - 不存在的 haddsub 内在?

use*_*583 2 sse simd intrinsics

在浏览可用的内在函数时,我注意到无处可见可用的水平 addsub/subadd 指令。它在过时的 3DNow 中可用!扩展但是由于显而易见的原因,它的使用是不切实际的。在 SSE3 扩展中没有实现这种“基本”操作以及类似的水平和 addsub 操作的原因是什么?

顺便说一句,现代指令集(SSE3、SSE4、AVX……)中最快的替代方法是什么?(每个值有 2 个双打,即 __m128d)

Pet*_*des 5

通常,您首先要避免将代码设计为使用水平操作;尝试对多个数据并行做同样的事情,而不是用不同的元素做不同的事情。但有时局部优化仍然值得,横向的东西可能比纯标量更好。

英特尔尝试在 SSE3 中添加水平操作,但从未添加专用硬件来支持它们。它们在支持它们的所有 CPU(包括 AMD)上解码为 2 次随机播放 + 1 次垂直操作。请参阅Agner Fog 的说明表。最近的 ISA 扩展大多不包含更多的水平操作,除了 SSE4.1 dpps/ dppd(与手动改组相比,它通常也不值得使用)。

SSSE3pmaddubsw是有道理的,因为元素宽度已经是扩大乘法的一个问题,并且SSE4.1 立即phminposuw获得了专门的硬件支持以使其值得使用(并且在没有它的情况下做同样的事情会花费很多 uop,而且它特别有用用于视频编码)。但是 AVX / AVX2 / AVX512 水平操作非常稀缺。AVX512 确实引入了一些不错的 shuffle,因此您可以在需要时从强大的 2 输入通道交叉 shuffle 中构建自己的水平操作。


如果您的问题最有效的解决方案已经包括将两个输入以两种不同的方式混在一起并将其提供给 add 或 sub,那么肯定haddpd是一种有效的编码方式;尤其是在没有 AVX 的情况下,准备输入可能也需要一条movaps指令,因为shufpd它具有破坏性(在使用内在函数时由编译器默默地发出,但仍会消耗前端带宽,以及像 Sandybridge 和更早版本的 CPU 上的延迟,这些 CPU 并不能消除 reg- reg 移动)。

但是,如果您打算两次使用相同的输入,haddpd则是错误的选择。另请参阅在 x86 上进行水平浮点向量求和的最快方法hadd/hsub只是具有两个不同输入的好主意,例如作为动态转置的一部分作为矩阵上其他一些操作的一部分。


无论如何,关键是,如果需要,可以根据需要构建自己的haddsub_pd,从两次 shuffle + SSE3addsubpd(在支持它的 CPU 上确实有单 uop 硬件支持。)使用 AVX,它将与假设haddsubpd指令一样快,没有 AVX 通常会额外花费一个,movaps因为编译器需要保留第一次 shuffle 的两个输入。(代码大小会更大,但我说的是前端的 uops 成本和后端的执行端口压力。)

 // Requires SSE3 (for addsubpd)

  // inputs: a=[a1 a0]  b=[b1 b0]
  // output:   [b1+b0, a1-a0],  like haddpd for b and hsubpd for a
static inline
__m128d haddsub_pd(__m128d a, __m128d b) {
    __m128d lows  = _mm_unpacklo_pd(a,b);  // [b0,    a0]
    __m128d highs = _mm_unpackhi_pd(a,b);  // [b1,    a1]
    return _mm_addsub_pd(highs, lows);     // [b1+b0, a1-a0]
}
Run Code Online (Sandbox Code Playgroud)

使用gcc -msse3和 clang(在 Godbolt 上)我们得到了预期的结果:

    movapd  xmm2, xmm0          # ICC saves a code byte here with movaps, but gcc/clang use movapd on double vectors for no advantage on any CPU.
    unpckhpd        xmm0, xmm1
    unpcklpd        xmm2, xmm1
    addsubpd        xmm0, xmm2
    ret
Run Code Online (Sandbox Code Playgroud)

这通常不会不管内联时,但作为一个独立的功能gcc和铿锵有麻烦时,他们需要在同一个寄存器,返回值b开始的,而不是a。(例如,如果 args 被反转,那么它是haddsub(b,a))。

# gcc for  haddsub_pd_reverseargs(__m128d b, __m128d a) 
    movapd  xmm2, xmm1          # copy b
    unpckhpd        xmm1, xmm0
    unpcklpd        xmm2, xmm0
    movapd  xmm0, xmm1          # extra copy to put the result in the right register
    addsubpd        xmm0, xmm2
    ret
Run Code Online (Sandbox Code Playgroud)

clang 实际上做得更好,使用不同的 shuffle(movhlps而不是unpckhpd)仍然只使用一个寄存器副本:

# clang5.0
    movapd  xmm2, xmm1              # clangs comments go in least-significant-element first order, unlike my comments in the source which follow Intel's convention in docs / diagrams / set_pd() args order
    unpcklpd        xmm2, xmm0      # xmm2 = xmm2[0],xmm0[0]
    movhlps xmm0, xmm1              # xmm0 = xmm1[1],xmm0[1]
    addsubpd        xmm0, xmm2
    ret
Run Code Online (Sandbox Code Playgroud)

对于带有__m256d向量的 AVX 版本, 的 in-lane 行为_mm256_unpacklo/hi_pd实际上是您想要的,一次,获得偶数/奇数元素。

static inline
__m256d haddsub256_pd(__m256d b, __m256d a) {
    __m256d lows  = _mm256_unpacklo_pd(a,b);  // [b2, a2 | b0, a0]
    __m256d highs = _mm256_unpackhi_pd(a,b);  // [b3, a3 | b1, a1]
    return _mm256_addsub_pd(highs, lows);     // [b3+b2, a3-a2 | b1+b0, a1-a0]
}

# clang and gcc both have an easy time avoiding wasted mov instructions
    vunpcklpd       ymm2, ymm1, ymm0 # ymm2 = ymm1[0],ymm0[0],ymm1[2],ymm0[2]
    vunpckhpd       ymm0, ymm1, ymm0 # ymm0 = ymm1[1],ymm0[1],ymm1[3],ymm0[3]
    vaddsubpd       ymm0, ymm0, ymm2
Run Code Online (Sandbox Code Playgroud)

当然,如果你有两次相同的输入,即你想要一个向量的两个元素之间的总和和差,你只需要一个 shuffle 来馈送 addsubpd

// returns [a1+a0  a1-a0]
static inline
__m128d sumdiff(__m128d a) {
    __m128d swapped = _mm_shuffle_pd(a,a, 0b01);
    return _mm_addsub_pd(swapped, a);
}
Run Code Online (Sandbox Code Playgroud)

这实际上用 gcc 和 clang 编译得相当笨拙:

    movapd  xmm1, xmm0
    shufpd  xmm1, xmm0, 1
    addsubpd        xmm1, xmm0
    movapd  xmm0, xmm1
    ret
Run Code Online (Sandbox Code Playgroud)

但是第二个 movapd 应该在内联时消失,如果编译器不需要它开始的同一个寄存器中的结果。我认为 gcc 和 clang 都缺少这里的优化:它们可以xmm0在复制后交换:

     # compilers should do this, but don't
    movapd  xmm1, xmm0         # a = xmm1 now
    shufpd  xmm0, xmm0, 1      # swapped = xmm0
    addsubpd xmm0, xmm1        # swapped +- a
    ret
Run Code Online (Sandbox Code Playgroud)

据推测,他们的基于SSA的寄存器分配器不会考虑使用第二个寄存器为相同的值a来释放 xmm0 swapped。通常在不同的寄存器中生成结果很好(甚至更可取),因此在内联时这很少成为问题,仅在查看函数的独立版本时