【问题标题】:sse/avx equivalent for neon vuzpsse/avx 等效于 neon vuzp
【发布时间】:2018-01-04 16:23:12
【问题描述】:

英特尔的向量扩展 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,我们可能会遇到额外的问题,即 shuffle 和 unpack 不会越过车道。)

了解这一点对于编写数据混杂和去混杂的代码可能很有用(应该可以通过基于解包操作反转混杂代码的操作来派生去混杂代码)。

编辑:这里是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)

【问题讨论】:

  • 很高兴 AVX512 解决了所有这些问题。终于!
  • @Mysticial:AVX512 在哪些方面修复了它?是否有逆解包,还是他们放弃了面向车道的处理?关于后者:根据英特尔内部指南,例如在 AVX512 中,解包仍然是面向通道的(例如 _mm512_unpackhi_epi8:“从 a 和 b 中每个 128 位通道的高半部分解包并交错 8 位整数,并将结果存储在 dst 中。”)。
  • @Ralf Just unpack[lo/hi] 再次log2(vectorlength) 次。 zip/unzip 是循环的。
  • @Ralf vpermi2ps/vpermt2ps, vpermi2d/vpermt2d
  • @BeeOnRope - NEON 的vuzp 实际上使用两个寄存器作为输入,使用相同的两个寄存器作为输出。英特尔指令/内在函数只有一个向量输出,因此,正如您所说,每行由两条指令生成(例如 unpacklounpackhi)。所以最少是 2 条指令(例如 2 次 shuffle_ps,正如 Peter Cordes 的回答),我的组合(shuffle 加上 unpack)使用 4。

标签: sse simd neon avx


【解决方案1】:

应该可以通过反转操作来导出去混杂的代码

习惯于对英特尔矢量洗牌的非正交性感到失望和沮丧。 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-input shuffle 可以为 32 位元素执行您想要的操作:shufps。结果的低 2 个元素可以是第一个向量的任意 2 个元素,高 2 个元素可以是第二个向量的任意元素。我们想要的 shuffle 符合这些约束,所以我们可以使用它。

我们可以用 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 上解码为多个微指令)。

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

对于吞吐量,1 个 shuffle + 2 个 non-shuffle 可能比 2 个 shuffle 更有效,并且具有相同的延迟。甚至 2 次 shuffle 和 2 次混合可能比 3 次 shuffle 更有效,具体取决于周围代码中的瓶颈。但我认为我们不能用那几条指令替换 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 次随机播放来进行输入,然后是 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 的 CPU,因为它的旁路延迟很高 (2c)。它有 2 个每时钟 shuffle 吞吐量,pshufd 是一个复制 + shuffle,所以 2x pshufd 准备 ab 的副本只需要一个额外的 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

【讨论】:

  • 非常感谢,很好的回答! _mm_shuffle_ps 似乎是 32 位类型的最佳解决方案。我的解决方案 (shuffle + unpack) 也适用于较小的类型(每个输出需要 2 次操作),但缺点是它需要 _mm_shuffle_epi8,其中随机掩码来自向量,而不是来自立即数。如果您想将其封装在解压缩函数中,这将导致效率低下。对此有什么想法吗?
  • @Ralf:编译器将在内联后将_mm_set_epi8(...) 常量提升出循环。只需“天真地”用_mm_shuffle_epi8(v, _mm_set_epi8(...)); 编写它,除非您使用的是 MSVC(我认为在内联后无法提升常量。)向量常量就像字符串文字:使用相同常量的多个函数最终共享一个实际定义。制作static const __m128i 实际上会更糟。
  • 就像您提到的那样,我是对 AVX 感到沮丧的人之一。与 AVX/2 相比,NEON 是天赐之物。自从我在 NEON 之后一直在研究 AVX2,我明白为什么英特尔在移动领域完全失败了。他们的指令集简直糟透了。
  • 顺便说一句,答案很好。
  • @PeterCordes 真是巧合。我在我最近的项目中这样做了,它可以通过 NEON 上的四个指令来实现。尽管我真的很欣赏 AVX2 上的 pmovmaskb,但我担心这会从 SIMD 转移到 GPR,从而可能导致管道停顿。从 SIMD 迁移到 GPR 确实会导致 NEON 出现严重问题。你能告诉我在 AVX2 上是不是这样吗?
猜你喜欢
  • 2012-08-05
  • 2011-11-04
  • 1970-01-01
  • 1970-01-01
  • 2012-11-17
  • 1970-01-01
  • 1970-01-01
  • 2012-07-02
  • 1970-01-01
相关资源
最近更新 更多