sse/avx相当于霓虹灯vuzp

Ral*_*alf 5 sse simd avx neon

英特尔的矢量扩展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)
Run Code Online (Sandbox Code Playgroud)

解压缩的等价物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)
Run Code Online (Sandbox Code Playgroud)

如何vuzp使用SSE或AVX内在函数有效实现?似乎没有针对它的指示.对于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)
Run Code Online (Sandbox Code Playgroud)

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

知道这一点对于编写用于数据调配和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)
Run Code Online (Sandbox Code Playgroud)

这是我的版本,每个输出元素都有一个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)
Run Code Online (Sandbox Code Playgroud)

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)
Run Code Online (Sandbox Code Playgroud)

Pet*_*des 9

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

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

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


不过,有一个 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)
Run Code Online (Sandbox Code Playgroud)

_MM_SHUFFLE()使用最重要的元素第一个表示法,就像英特尔的所有文档一样。你的记法是相反的。

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);
}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

在最近的 Intel/AMD CPU 上,对整数数据使用 FP 混洗是没问题的。没有额外的旁路延迟延迟(请参阅此答案,其中总结了Agner Fog 的微体系结构指南对此的看法)。它在 Intel Nehalem 上有额外的延迟,但可能仍然是最佳选择。FP 加载/洗牌不会出错或损坏表示 NaN 的整数位模式,只有实际的 FP 数学指令才关心这一点。

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


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

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

对于吞吐量而言,1 个 shuffle + 2 个非 shuffle 可能比 2 个 shuffle 更高效,并且具有相同的延迟。或者甚至 2 次洗牌和 2 次混合可能比 3 次洗牌更有效,具体取决于周围代码中的瓶颈。但我认为我们不能shufps用那几条指令来取代 2x。


没有SHUFPS

你的 shuffle + unpacklo/hi 非常好。总共需要进行 4 次洗牌:2 次pshufd用于准备输入,然后是 2punpck升/小时。这可能比任何旁路延迟更糟糕,但在 Nehalem 上,延迟很重要但吞吐量不重要的情况除外。

任何其他选项似乎都需要准备 4 个输入向量,用于混合或packss. 请参阅@Mysticial 对整数向量 (__m128i) 的 _mm_shuffle_ps() 等效项的回答?对于混合选项。对于两个输出,总共需要 4 次洗牌才能生成输入,然后是 2 倍pblendw(快速)或vpblendd(甚至更快)。

对 16 位或 8 位元素使用packsswdwb也可以。需要 2xpand条指令来屏蔽 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;
}
Run Code Online (Sandbox Code Playgroud)

Nehalem 是唯一值得使用 2x 以外的 CPU shufps,因为它的旁路延迟很高 (2c)。它每时钟有 2 个洗牌吞吐量,并且pshufd是复制+洗牌,因此需要 2 倍pshufd准备副本a,之后b只需要额外一个即可将结果放入单独的寄存器中。(不是免费的;它有 1c 延迟,需要 Nehalem 上的向量执行端口。如果您在 shuffle 吞吐量上遇到瓶颈,而不是整体前端带宽(uop 吞吐量)或其他问题,那么它只比 shuffle 便宜。)movdqapunpckldqpunpckhdqmovdqa

我强烈建议只使用 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