如何在SSE/AVX中执行位求反操作?

16

是我想象还是SSE和AVX中缺少PNOT指令?也就是说,缺少一个翻转向量中每个位的指令。

如果是的话,有比使用全1向量执行PXOR更好的模拟方法吗?这很烦人,因为我需要设置一个全1向量来使用这种方法。


1
设置一个全是1的向量并不特别困难:[v]pcmpe[typesize] %[x/y]mmN, %[x/y]mmN[, %[x/y]mmN]或类似的指令。单个指令来设置常量似乎并不太繁琐。如果您特别反感xor,则pandnandnps也可用。 - EOF
1
这并不糟糕 - 但对于像这样的基本操作,它的长度是我预期的两倍。当然,常量可以提升,但会牺牲一个寄存器。无论如何,只是检查我的假设,以确保我没有错过什么。@EOF - SODIMM
2
我总体上同意。在我的情况下这很重要。我在3个向量端口上的吞吐量和端口受限。每个向量操作对我来说都会花费1/3个周期(在合理范围内)。@EOF - SODIMM
3
SSE 中有一个“ANDNPD”(与非)指令。 - Chuck Walbourn
3
同样地:PNEG 指令在哪里? - Joost
显示剩余6条评论
4个回答

17

对于这种情况,查看编译器会生成什么代码是很有指导意义的。

比如下面的函数:

#include <immintrin.h>

__m256i test(const __m256i v)
{
  return ~v;
}

无论是gcc还是clang,似乎都会生成大致相同的代码

test(long long __vector(4)):
        vpcmpeqd        ymm1, ymm1, ymm1
        vpxor   ymm0, ymm0, ymm1
        ret

3
谢谢Paul。这相当不错,我想这意味着没有更好的选择了。 - SODIMM
1
s/indicate/indication/ - SODIMM

8
如果您使用Intrinsics,可以使用这样的内联函数来单独执行not操作。
 inline __m256i _mm256_not_si256 (__m256i a){    
     //return  _mm256_xor_si256 (a, _mm256_set1_epi32(0xffffffff));
     return  _mm256_xor_si256 (a, _mm256_cmpeq_epi32(a,a));//I didn't check wich one is faster   
 }

3
优秀的编译器通常会将 _mm256_set1_epi32(-1) 优化为 vpcmpeqd same,same。我猜使用 AVX 技术,如果编译器本来不会生成这种指令,那么尝试“欺骗”它生成这种指令可能不会有太大影响。(在 SSE 中可能需要额外的 MOVDQA 指令,但是 AVX 的三操作数编码方式解决了这个问题。) - Peter Cordes

7
AVX512F vpternlogd / _mm512_ternarylogic_epi32(__m512i a, __m512i b, __m512i c, int imm8) 最终提供了一种实现NOT操作的方式,无需任何额外的常量,只需要使用单个指令即可在Skylake-avx512上的任何向量ALU端口运行。而且,使用AVX512VL,对于128位和256位向量也可以实现,而不会弄脏ZMM的上部。(除Xeon Phi之外的所有AVX512 CPU均具有AVX512VL)。
在Intel CPU上,它可以在端口0、1或5中的任何一个运行,因此128位和256位版本的吞吐量为3/clock。对于512位向量,通常是2/clock,因为当飞行中有任何512位uops时,端口1会关闭。https://www.uops.info/html-instr/VPTERNLOGD_XMM_XMM_XMM_I8.html

vpternlogd zmm,zmm,zmm, imm8有3个输入向量和一个输出,会直接修改目标寄存器。通过正确的立即数,您仍然可以在不同的寄存器中实现复制和NOT操作,但是它将对输出寄存器产生“虚假”依赖关系(而vpxord dst,src,all-ones则不会)。

TL:DR:除非寄存器不够用,否则可能仍然使用带有全1的xor作为循环的一部分。如果需要后续使用其输入,则vpternlog可能会增加一个vmovdqa寄存器复制指令。

循环外,vpternlogd zmm,zmm,zmm, 0xff编译器创建512位全1向量的最佳选项, 因为AVX512比较指令将比较结果存储在掩码寄存器 (k0-k7) 中,因此与全1异或可能已经涉及到了一个vpternlogd,或者可能是从内存中广播常量,对于512位向量。或者是一个128位或256位vpcmpeqd same,same的dep-breaking ALU uop。


对于每个位位置,输出位是imm[ (DEST[i]<<2) + (SRC1[i]<<1) + SRC2[i]],其中imm8被视为一个8元素位图。
因此,如果我们希望结果仅依赖于SRC2(即zmm/m512/m32bcst操作数),我们应选择一个重复1,0的位图,并在偶数位置(由src2=0选择)上选择1
vpternlogd  zmm1,zmm1, zmm2,  01010101b  ; 0x55  ; false dep on zmm1

如果你很幸运,编译器会对_mm512_xor_epi32(v, _mm512_set1_epi32(-1))进行优化,并为你生成vpternlogd,如果这是划算的话。
// To hand-hold a compiler into saving a vmovdqa32 if needed:
__m512i tmp = something earlier;
__m512i t2 = _mm...(tmp);
// use-case: tmp is dead, t2 and ~t2 are both needed.
__m512i t2_inv = _mm512_ternarylogic_epi32(tmp, t2, t2, 0b01010101);

如果您不确定这是一个好主意,只需保持简单,并对所有3个输入使用相同的变量:
__m512i t2_inv = _mm512_ternarylogic_epi32(t2, t2, t2, 0b01010101);

我不同意“它不如 PXOR 好”的说法。在 Skylake 上,AVX512 指令的最大吞吐量为每周期 2 个操作,而 PADD 和 PAND 组中只有少数命令能达到这个速度。2512 > 3256,因此在这些命令上,AVX512 仍比 AVX2 快 33%。 - Bulat
@Bulat:这不是我想说的。它不太好,因为它不能复制而不对输出寄存器产生虚假依赖,所以你的编译器可能会在vmovdqa32上浪费一些前端吞吐量。此外,AVX512VL指令的128位和256位版本通常是3/clock,包括vpternlogd ymm。如果您只使用256位向量来避免整个程序中某个小部分的turbo惩罚,您仍然可以使用vpternlogd ymm。此外,Gold上的FP FMA/add/mul每个时钟周期可以运行2次,所以我不知道您所说的“只有少数”指令在512位向量上以2/clock运行是什么意思。 - Peter Cordes

3
您可以使用PANDN操作码进行此操作。 PANDN实现了该操作。
DEST = NOT(DEST) AND SRC   ; (SSEx)

或者

DEST = NOT(SRC1) AND SRC2  ; (AVXx)

将这个操作与全1向量结合,有效地得到一个PNOT操作。


一些x86(SSEx)汇编代码如下:

; XMM0 is input register
PCMPEQB   xmm1, xmm1        ; Whole xmm1 reg set to 1's
PANDN     xmm0, xmm1        ; xmm0 = NOT(xmm0) AND xmm1
; XMM0 contains NOT(XMM0)

一些x86(AVXx)汇编代码可能如下所示:

; YMM0 is input register
VPCMPEQB  ymm1, ymm1, ymm1  ; Whole ymm1 reg set to 1's
VPANDN    ymm0, ymm0, ymm1  ; ymm0 = NOT(ymm0) AND ymm1
; YMM0 contains NOT(YMM0)

这两个内容(当然)都可以很容易地转换为内部函数。


2
由于仍需要一个全为1的向量,因此它似乎并不比问题中提出的PXOR更好。 - Nate Eldredge

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接