sse/avx 相当于 neon vuzp

sse/avx equivalent for neon vuzp

Intel 的矢量扩展 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)

unpack 相当于 ARM 的 NEON 指令集中的 vzip。但是,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)

it should be possible to derive deswizzling code just by inverting the operations

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

PACK 指令仅在饱和时可用,而不是 t运行cation(直到 AVX512 vpmovqd),因此对于此用例,我们需要为 2 个 PACK 准备 4 个不同的输入向量指示。结果证明这是可怕的,比你的 3-shuffle 解决方案更糟糕(参见下面 Godbolt link 中的 unzip32_pack())。


但是有一个 2 输入随机播放可以为 32 位元素执行您想要的操作:shufps。结果的低 2 元素可以是第一个向量的任意 2 个元素,高 2 元素可以是第二个向量的任意元素。我们想要的洗牌符合这些限制,所以我们可以使用它。

我们可以在 2 条指令中解决整个问题(为非 AVX 版本加上一个 movdqa,因为 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 洗牌没问题。没有额外的旁路延迟延迟(参见 this answer which summarizes what Agner Fog's microarch guide says about it). It has extra latency on Intel Nehalem , but may still be the best choice there. FP loads/shuffles ,只有实际的 FP 数学指令关心这一点。

有趣的事实:在 AMD Bulldozer 系列 CPUs(和 Intel Core2)上,FP 洗牌如 shufps 仍然 运行 在 ivec 域中,因此它们实际上有额外的延迟当在 FP 指令之间使用时,而不是在整数指令之间使用!


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

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

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


没有SHUFPS:

你的shuffle + unpacklo/hi很不错。总共有 4 次洗牌:2 pshufd 准备输入,然后 2 punpckl/h。这可能比任何旁路延迟都更糟糕,除了在 Nehalem 上,在延迟很重要但吞吐量不重要的情况下。

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

对 16 位或 8 位元素使用 packsswdwb 也可以。屏蔽 a 和 b 的奇数元素需要 2x pand 条指令,将奇数元素向下移动到偶数位置需要 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 是唯一一个 CPU 可能值得使用 2x shufps 以外的东西的地方,因为它具有高 (2c) 旁路延迟。它每个时钟有 2 个洗牌吞吐量,pshufd 是一个复制+洗牌,所以 2x pshufd 准备 ab 的副本只需要一个额外的 movdqa 之后将 punpckldqpunpckhdq 结果放入单独的寄存器中。 (movdqa 不是免费的;它有 1c 延迟并且需要 Nehalem 上的向量执行端口。如果你在洗牌吞吐量上遇到瓶颈,而不是整体前端带宽(uop 吞吐量),它只比洗牌便宜或其他东西。)

我非常推荐只使用 2x shufps 平均来说会很好 CPU,而且在任何地方都不可怕。


AVX512

AVX512 引入了一个跨车道 pack-with-t运行cation 指令,它缩小了单个向量(而不是 2 输入混洗)。它是 pmovzx 的倒数,可以缩小 64b->8b 或任何其他组合,而不仅仅是缩小 2 倍。

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

它还有 signed/unsigned 饱和版本。这些指令甚至具有内存目标形式,内在函数公开该形式让您进行屏蔽存储。

但是对于这个问题,正如 Mysticial 指出的那样,AVX512 提供了 2 个输入的车道交叉洗牌,您可以像 shufps 一样使用它来在两个洗牌中解决整个问题:vpermi2d/vpermt2d.