霓虹灯vuzp的sse / avx等效项

拉尔夫

英特尔的向量扩展SSE,AVX等为每种元素大小提供了两种解压缩操作,例如SSE内在函数are_mm_unpacklo_*_mm_unpackhi_*对于向量中的4个元素,它将执行以下操作:

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

相当于解压缩的内容vzip在ARM的NEON指令集中。但是,NEON指令集也提供vuzp与相反的运算vzip对于向量中的4个元素,它将执行以下操作:

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

如何vuzp使用SSE或AVX内部函数有效地实现?似乎没有相关说明。对于4个元素,我认为可以使用随机播放和随后的拆包移动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,我们可能还有一个额外的问题,即洗牌和拆包不会越过车道。)

知道这一点对于编写用于数据混乱和反混乱的代码可能是有用的(仅通过基于解压缩操作来反转混乱代码的操作,就应该有可能派生出混乱代码)。

编辑:这是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)
彼得·科德斯

只需要通过反转操作就可以导出令人耳目一新的代码

习惯于因英特尔矢量混编的非正交性而感到失望和沮丧。没有的直接逆punpckSSE / AVXpack指令用于缩小元素大小。(因此,1packusdwpunpck[lh]wd与零的倒数,但与两个任意向量一起使用时则不是)。此外,pack指令仅适用于32-> 16(双字至字)和16-> 8(单字至字节)元素大小。没有packusqd(64-> 32)。

PACK指令仅在饱和时可用,而不能截断(直到AVX512 vpmovqd),因此对于此用例,我们需要为2个PACK指令准备4个不同的输入向量。事实证明,这太可怕了,比您的3-shuffle解决方案要糟糕得多(请参阅unzip32_pack()下面的Godbolt链接)。


有一个2输入混洗,会做你想要的32位元素,但:shufps结果的低2个元素可以是第一个向量的任何2个元素,高2个元素可以是第二个向量的任何元素。我们想要的改组符合这些约束,因此我们可以使用它。

我们可以用2条指令解决整个问题movdqa对于非AVX版本,还要加上a ,因为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()像Intel的所有文档一样,使用最重要的第一符号您的表示法是相反的。

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去掉,我们可以看到他们是如何编制非内联函数。我把它们放在Godbolt编译器浏览器上

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随机播放是可以的。没有额外的旁路延迟延迟(请参阅此答案,该总结总结了Agner Fog的微架构指南对此进行了说明)。它在Intel Nehalem上具有额外的延迟,但仍然可能是那里的最佳选择。FP加载/混洗不会出错或破坏表示NaN的整数位模式,只有实际的FP数学指令会对此予以关注。

有趣的事实:在AMD Bulldozer系列CPU(和Intel Core2)上,FP改组shufps在ivec域中运行,因此在FP指令之间使用时,实际上在整数指令之间使用时会有额外的延迟!


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

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

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


没有SHUFPS

您的shuffle + unpacklo / hi非常好。总共将进行4次混洗:pshufd准备2次输入,然后2 punpckl / h。这可能比任何旁路延迟都更糟,除非在延迟时间很重要但吞吐量无关紧要的情况下使用Nehalem。

任何其他选择似乎都需要准备4个输入向量(用于blend或)packss请参见@Mysticial对_mm_shuffle_ps()等价于整数矢量(__m128i)的答案?用于混合选项。对于两个输出,总共需要进行4次混洗才能构成输入,然后是2pblendw(快速)或vpblendd(甚至更快)。

对16或8位元素使用packsswdwb也可以。将需要2xpand条指令来掩盖a和b的奇数元素,并用2x指令psrld将奇数元素下移到偶数位置。这将使您设置2倍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以外的东西的CPU shufps,因为它的旁路延迟很高(2c)。每个时钟有2个shuffle吞吐量,并且pshufd是copy + shuffle,因此pshufd准备2x的副本,a然后b只需要一个额外的副本movdqa即可将punpckldqpunpckhdq结果放入单独的寄存器中。movdqa不是免费的;它具有1c的延迟,并且需要在Nehalem上使用向量执行端口。如果您在shuffle吞吐量方面遇到瓶颈,而不是整体前端带宽(uop吞吐量)之类的东西,它仅比shuffle便宜。)

我非常建议仅使用2x shufps在一般的CPU上会很好,并且在任何地方都不可怕。


AVX512

AVX512引入了带有截断的跨行打包指令,该指令缩小了单个向量的范围(而不是2输入的随机播放)。它是的倒数pmovzx,可以缩小64b-> 8b或任何其他组合,而不是仅缩小2倍。

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

它还提供有符号/无符号饱和版本。这些指令甚至具有内存目标形式,内部函数公开这些形式以使您可以进行屏蔽存储。

但是针对此问题,正如Mysticial指出的那样,AVX512提供了2输入行车道交叉洗牌,您可以使用它们shufps来解决整个问题,而只需两个洗牌即可:vpermi2d/vpermt2d

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章