sse / avx相当于氖vuzp

时间:2017-07-28 14:36:10

标签: sse simd neon avx

英特尔的矢量扩展SSE,AVX等为每个元素大小提供两个解包操作,例如: SSE内在函数为_mm_unpacklo_*_mm_unpackhi_*。对于向量中的4个元素,它执行此操作:

inputs:      (A0 A1 A2 A3) (B0 B1 B2 B3)
unpacklo/hi: (A0 B0 A1 B1) (A2 B2 A3 B3)

在ARM的NEON指令集中,unpack的等效值为vzip。但是,NEON指令集还提供操作vuzp,它是vzip的反转。对于向量中的4个元素,它执行此操作:

inputs: (A0 A1 A2 A3) (B0 B1 B2 B3)
vuzp:   (A0 A2 B0 B2) (A1 A3 B1 B3)

如何使用SSE或AVX内在函数有效地实现vuzp?似乎没有针对它的指示。对于4个元素,我假设它可以使用shuffle和随后的unpack移动2个元素来完成:

inputs:        (A0 A1 A2 A3) (B0 B1 B2 B3)
shuffle:       (A0 A2 A1 A3) (B0 B2 B1 B3)
unpacklo/hi 2: (A0 A2 B0 B2) (A1 A3 B1 B3)

使用单条指令是否有更高效的解决方案? (也许对于SSE优先 - 我知道对于AVX我们可能有另外的问题,洗牌和解包不会跨越车道。)

知道这可能对编写数据调配和deswizzling的代码很有用(应该可以通过基于解包操作反转调配代码的操作来导出deswizzling代码)。

编辑:这是8元素版本:这是NEON vuzp的效果:

input:         (A0 A1 A2 A3 A4 A5 A6 A7) (B0 B1 B2 B3 B4 B5 B6 B7)
vuzp:          (A0 A2 A4 A6 B0 B2 B4 B6) (A1 A3 A5 A7 B1 B3 B5 B7)

这是我的版本,每个输出元素有一个shuffle和一个unpack(似乎推广到更大的元素数字):

input:         (A0 A1 A2 A3 A4 A5 A6 A7) (B0 B1 B2 B3 B4 B5 B6 B7)
shuffle:       (A0 A2 A4 A6 A1 A3 A5 A7) (B0 B2 B4 B6 B1 B3 B5 B7)
unpacklo/hi 4: (A0 A2 A4 A6 B0 B2 B4 B6) (A1 A3 A5 A7 B1 B3 B5 B7)

EOF建议的方法是正确的,但每个输出都需要log2(8)=3 unpack次操作:

input:         (A0 A1 A2 A3 A4 A5 A6 A7) (B0 B1 B2 B3 B4 B5 B6 B7)
unpacklo/hi 1: (A0 B0 A1 B1 A2 B2 A3 B3) (A4 B4 A5 B5 A6 B6 A7 B7)
unpacklo/hi 1: (A0 A4 B0 B4 A1 A5 B1 B5) (A2 A6 B2 B6 A3 A7 B3 B7)
unpacklo/hi 1: (A0 A2 A4 A6 B0 B2 B4 B6) (A1 A3 A5 A7 B1 B3 B5 B7)

1 个答案:

答案 0 :(得分:4)

  

应该可以通过反转操作来导出deswizzling代码

习惯于对英特尔矢量改组的非正交性感到失望和沮丧。 punpck没有直接反转。 SSE / AVX pack指令用于缩小元素大小。 (因此一个packusdwpunpck[lh]wd与零的倒数,但与两个任意向量一起使用时则不然。此外,pack指令仅适用于32-> 16(字到字)和16-> 8(字到字)元素大小。没有packusqd(64-> 32)。

PACK指令仅适用于饱和,而不是截断(直到AVX512 vpmovqd),因此对于这种用例,我们需要为2个PACK指令准备4个不同的输入向量。事实证明这是可怕的,比你的3-shuffle解决方案更糟糕(见下面Godbolt链接中的unzip32_pack())。

有一个2输入的shuffle可以为32位元素执行所需的操作,但是:shufps 。结果的低2个元素可以是第一个向量的任何2个元素,高2个元素可以是第二个向量的任何元素。我们想要的洗牌符合这些约束条件,因此我们可以使用它。

我们可以解决2条指令中的整个问题(加上movdqa非AVX版本,因为shufps会破坏左输入寄存器:

inputs: a=(A0 A1 A2 A3) a=(B0 B1 B2 B3)
_mm_shuffle_ps(a,b,_MM_SHUFFLE(2,0,2,0)); // (A0 A2 B0 B2)
_mm_shuffle_ps(a,b,_MM_SHUFFLE(3,1,3,1)); // (A1 A3 B1 B3)

_MM_SHUFFLE() uses most-significant-element first notation,与英特尔的所有文档一样。你的符号是相反的。

shufps的唯一固有内容使用__m128 / __m256向量(float不是整数),因此您必须强制转换才能使用它。 _mm_castsi128_ps是reinterpret_cast:它编译为零指令。

#include <immintrin.h>
static inline
__m128i unziplo(__m128i a, __m128i b) {
    __m128 aps = _mm_castsi128_ps(a);
    __m128 bps = _mm_castsi128_ps(b);
    __m128 lo = _mm_shuffle_ps(aps, bps, _MM_SHUFFLE(2,0,2,0));
    return _mm_castps_si128(lo);
}

static inline    
__m128i unziphi(__m128i a, __m128i b) {
    __m128 aps = _mm_castsi128_ps(a);
    __m128 bps = _mm_castsi128_ps(b);
    __m128 hi = _mm_shuffle_ps(aps, bps, _MM_SHUFFLE(3,1,3,1));
    return _mm_castps_si128(hi);
}

gcc会将这些内容分别插入到一条指令中。删除static inline后,我们可以看到它们如何编译为非内联函数。我把它们放在the Godbolt compiler explorer

unziplo(long long __vector(2), long long __vector(2)):
    shufps  xmm0, xmm1, 136
    ret
unziphi(long long __vector(2), long long __vector(2)):
    shufps  xmm0, xmm1, 221
    ret

在最近的Intel / AMD CPU上使用整数数据的FP shuffle很好。没有额外的旁路延迟延迟(请参阅this answer,其中总结了Agner Fog's microarch guide对此的说法)。它在Intel Nehalem上具有额外的延迟,但可能仍然是那里的最佳选择。 FP加载/随机播放won't fault or corrupt integer bit-patterns that represent a NaN,只有实际的FP数学指令关心它。

有趣的事实:在AMD Bulldozer系列CPU(和Intel Core2)上,像shufps这样的FP shuffle仍然在ivec域中运行,所以它们在FP指令之间使用时实际上有额外的延迟,但在整数指令之间没有!

与ARM NEON / ARMv8 SIMD不同, x86 SSE没有任何2输出寄存器指令,并且它们在x86中很少见。 (它们存在,例如mul r64,但总是在当前CPU上解码为多个uops。)

它总是至少需要2条指令来创建2个结果向量。如果它们都不需要在shuffle端口上运行,那将是理想的,因为最近的Intel CPU具有每时钟仅1的shuffle吞吐量。当所有指令都是随机播放时,指令级并行性并没有多大帮助。

对于吞吐量,1次shuffle + 2次非shuffle可能比2次shuffle更有效,并且具有相同的延迟。或者甚至2次shuffle和2次混合比3次shuffle更有效,这取决于周围代码中的瓶颈。但我认为我们不能用少量指令替换2x shufps

没有SHUFPS

你的shuffle + unpacklo / hi非常好。共计4次洗牌:2 pshufd准备输入,然后2 punpck l / h。这可能比任何旁路延迟更糟,除了Nehalem,在延迟很重要但吞吐量不高的情况下。

任何其他选项似乎都需要为混合或packss准备4个输入向量。有关混合选项,请参阅@Mysticial's answer to _mm_shuffle_ps() equivalent for integer vectors (__m128i)?。对于两个输出,输入总共需要4次,然后是2次pblendw(快速)或vpblendd(甚至更快)。

对16位或8位元素使用packsswdwb也可以。需要2x pand指令来屏蔽a和b的奇数元素,并且2x psrld将奇数元素向下移动到偶数位置。这会为你设置2x packsswd来创建两个输出向量。总共6条指令,加上许多movdqa,因为这些指令都会破坏它们的输入(与pshufd不同,这是一个副本+随机播放)。

// don't use this, it's not optimal for any CPU
void unzip32_pack(__m128i &a, __m128i &b) {
    __m128i a_even = _mm_and_si128(a, _mm_setr_epi32(-1, 0, -1, 0));
    __m128i a_odd  = _mm_srli_epi64(a, 32);
    __m128i b_even = _mm_and_si128(b, _mm_setr_epi32(-1, 0, -1, 0));
    __m128i b_odd  = _mm_srli_epi64(b, 32);
    __m128i lo = _mm_packs_epi16(a_even, b_even);
    __m128i hi = _mm_packs_epi16(a_odd, b_odd);
    a = lo;
    b = hi;
}

Nehalem是唯一可能值得使用2x shufps以外的东西的CPU,因为它的高(2c)旁路延迟。它有2个时钟随机播放吞吐量,而pshufd是副本+随机播放,因此2x pshufd准备ab的副本只需要一个额外的{{1}之后,将movdqapunpckldq结果放入单独的寄存器中。 (punpckhdq不是免费的;它有1c延迟并且需要Nehalem上的矢量执行端口。如果您在随机播放吞吐量方面遇到瓶颈,而不是总体而言,它只比洗牌更便宜前端带宽(uop吞吐量)或其他东西。)

我非常推荐使用2x movdqa这对普通CPU来说会很好,而且在任何地方都不会太糟糕。

AVX512

AVX512引入了一个带有截断通道的带有截断的指令,该指令缩小了单个向量(而不是2输入的shuffle)。它是shufps的倒数,可以缩小64b-> 8b或任何其他组合,而不是仅为2倍。

对于这种情况,__m256i _mm512_cvtepi64_epi32 (__m512i a)pmovzx)将从向量中取出偶数32位元素并将它们打包在一起。 (即每个64位元素的低半部分)。但是,它仍然不是交错的良好构建块,因为你需要其他东西才能将奇数元素放到位。

它还有签名/无符号饱和版本。这些指令甚至还有一个内存目标表单,内在函数暴露给你,让你做一个蒙面存储。

但是对于这个问题,正如Mysticial指出的那样, AVX512提供了2输入交叉的shuffle,您可以像vpmovqd一样使用它来解决整个问题:vpermi2d/vpermt2d