【问题标题】:SSE/SIMD shift with one-byte element size / granularity?单字节元素大小/粒度的 SSE/SIMD 移位?
【发布时间】:2016-05-02 08:56:27
【问题描述】:

如您所知,我们在 SIMD SSE 中有以下移位指令:PSLL(W-D-Q) 和 PSRL(W-D-Q)

没有PSLLB指令,那么我们如何移位8位值(单字节)的向量?

【问题讨论】:

  • 你可以移位和屏蔽。
  • 你能给我一个简单的例子吗??
  • psrldq xmm0, 1; pand xmm0, [mask]; mask dd 0x7f7f7f7f, 0x7f7f7f7f, 0x7f7f7f7f, 0x7f7f7f7f。请注意,左移一位只是paddb xmm0, xmm0。对于较大的班次,请酌情调整蒙版。
  • 我测试了它,但没有结果....我怎样才能用 and 操作数移位??
  • 总有结果 ;)

标签: assembly x86 sse bit-shift


【解决方案1】:

在左移一位的特殊情况下,您可以使用paddb xmm0, xmm0


正如 Jester 在 cmets 中指出的那样,模拟不存在的 psrlbpsllb 的最佳选择是使用更宽的移位,然后屏蔽任何跨越元素边界的位。

例如

    psrlw   xmm0, 2       ; doesn't matter what size (w/d/q): performance is the same for all sizes on all CPUs
    pand    xmm0, [mask_right2]

section .rodata
  align 16
    ;; required mask depends on the shift count
    mask_right2: times 16  db 0xff >> 2      (16 bytes of 0x3f)

或者以其他方式在循环之前将 0x3f 广播到向量寄存器中,例如来自内存中的 dword 的 vpbroadcastdvbroadcastss,来自 qword 的 SSE3 movddup,或者只是一个 movdqa 向量加载。 (vpbroadcastb 需要一个额外的 ALU uop,这与 dword 或更广泛的广播不同,它们只是简单的加载)。或generate on the fly with a sequence likepcmpeqd xmm0,xmm0/psrlw xmm0, 8+2/packuswb xmm0,xmm0。通过正确选择移位计数,您可以生成 2n-1 字节的任何模式(重复零,然后重复零)。

mov r32, imm32 / movd xmm, r32 和 shuffle 也是一种选择,但与 pcmpeqw / ... 序列相比可能不会节省指令字节。 (请注意,VBROADCASTSS 的寄存器源版本是 AVX2-only,这在此处无关紧要,因为 256b 整数移位也是 AVX2-only。)

对于可变计数向量移位,在整数寄存器中创建掩码并将其广播到向量是一种选择(使用 pshufb 和全零寄存器来广播低字节,或使用 imul eax, eax, 0x01010101 到对于movd + pshufd,从一个字节到一个双字。您还可以使用pcmpeqd 方法创建一个全1 向量并使用psrlw xmm0, xmm1,然后使用packpshufb


我没有看到任何类似有效的方法来模拟算术右移(不存在的PSRAB)。每个字的高字节由PSRAW 正确处理。将每个字的低字节移到高位将使另一个PSRAW 根据需要多次复制其符号位。

;; vpblendvb is 2 uops on Intel so this is worse throughput in loops than the pxor/paddb version
;; Latency may be the same on Skylake because this has some ILP.

; input in xmm0.  Using AVX to save on mov instructions
VPSLLDQ   xmm1, xmm0, 1      ; or VPSLLW xmm1, xmm0, 8, but this distributes one of the uops to the shuffle port
VPSRAW    xmm1, xmm1, 8+2    ; shift low bytes back to final destination

VPSRAW    xmm0, xmm0, 2      ; shift high bytes, leaving garbage in low bytes
VPBLENDVB xmm0, xmm1, xmm0, xmm2  ; (where xmm2 holds a mask of alternating 0 and -1, which could be generated with pcmpeqw / psrlw 8).  This insn is fairly slow

没有字节粒度的立即混合,因为单个立即字节只能编码 8 个元素。


没有 VPBLENDVB(即使它可用,如果为它生成或加载常量很慢,可能会更好):

;; Probably worse than the PXOR/PADDB version, if 2 constants are cheap to load
;; Needs no vector constants, but this is inefficient vs. versions with constants.
VPSLLDQ   xmm1, xmm0, 1      ; or VPSLLW 8
VPSRAW    xmm1, xmm1, n      ; low bytes in the wrong place

VPSRAW    xmm0, xmm0, 8+n    ; shift high bytes all the way to the bottom of the element
VPSLLW    xmm0, xmm0, 8      ; high bytes back in place, with zero in the low byte.  (VPSLLDQ can't work: PSRAW 8+n leaves garbage we need to clear)

VPSRLW    xmm1, xmm1, 8      ; shift low bytes into place, leaving zero in the high byte.  (VPSRLDQ 1 could do this, if we started with VPSLLW instead of VPSLLDQ)
VPOR      xmm0, xmm0, xmm1

在寄存器中使用带有常数(交替 0/-1 字节)的 PAND/PANDN/POR 也可以进行字节混合(对移位端口的压力要小得多),如果您使用它,这是一个更好的选择必须循环执行。


将窄值符号扩展到字节的其余部分:

假设每个字节都是零扩展的,例如在使用 AND + shift/AND 将半字节解压缩为字节后。 (适用于任何字段宽度,只需调整常量。)

用 XOR 翻转高零和符号位。将符号位加 1,以便恢复正确的符号位,并通过进位传播清除高位(如果它变为 0 并执行)或保持设置(如果它变为 1 且未进位)。

; hoist the constants out of a loop if you're looping, of course.
; input in XMM0, upper bits of each byte already zeroed 
    pxor   xmm0,  [const_0xf8]     ;   1111 s'xxx
    paddb  xmm0,  [const_0x08]     ;   0000 0xxx   or  1111 1xxx

用它来模拟丢失的psrab

这仍然可能只使用内存中的 2 个常量。这很可能是循环的最佳选择,特别是如果您有空闲的寄存器来提升这些常量的负载。 (0xf0 可以与 vpandn 一起使用,以隔离低半字节,如果您还需要它。)

    psrld  xmm0,  4                              ;   ???? sxxx   (s = sign bit, xxx = lower bits)
    por    xmm0,  xmm5     ; set1_epi8(0xf0)     ;   1111 sxxx

    pxor   xmm0,  xmm6     ; set1_epi8(0x08)     ;   1111 s'xxx
    paddb  xmm0,  xmm6     ; set1_epi8(0x08)     ;   0000 0xxx   or  1111 1xxx

我认为我们不能避免使用 2 个单独的布尔值。我们需要 PXOR 来对抗 PADDB 或 PSUBB 翻转符号位,但只有 POR 可以设置位而不管它们的旧值。

我们可以隔离符号位并在加减之前将其左移 (pand + pslld + paddb) 但这会更糟,尤其是没有 AVX 用于 3 操作数指令以避免 movdqa。这也将是更全面的指令,包括我们仍然需要的 POR。

优点:

  • 可以在任何向量 ALU 端口上运行的简单指令。
  • Intel 上的微指令少于 vpblendvb 版本。

缺点:

  • 没有 ILP(指令级并行),因此延迟可能不会比 vpblendvb 版本更好,尤其是在 AMD Zen / Zen2 上,其中 vpblendvb 是一条只有 1c 延迟的单微指令。
  • 需要 2 个向量常量。

使用 PSHUFB 表查找的字段

代替 pxor / paddb,使用pshufb 根据低 4 位为每个字节查找一个新值。不幸的是,如果选择器设置了高位,pshufb 会将通道归零,因此我们不能在原始的 psrld 结果上使用它,因为这些结果可能已经转移到非零高位。

const __m128i sext_lut = _mm_setr_epi8( 0,  1,  2,  3,  4,  5,  6,  7,
                                       -8, -7, -6, -5, -4, -3, -2, -1);
return _mm_shuffle_epi8(sext_lut, v);

对于 3 操作数非破坏性的 AVX,这可以是在寄存器中重用查找表的单个指令。如果没有,则需要 movdqa 来复制 LUT。

用这个换挡:

__m128i srai_4_epi8(__m128i v) {
    v = _mm_srli_epi32(v, 4);
    v = _mm_and_si128(v, _mm_set1_epi8(0x0f));
  const __m128i sext_lut = _mm_setr_epi8( 0,  1,  2,  3,  4,  5,  6,  7,
                                         -8, -7, -6, -5, -4, -3, -2, -1);
    return _mm_shuffle_epi8(sext_lut, v);
}

【讨论】:

  • 对于“psrab”,如果您有暂存器,还有另一种方法:
  • Sign-extending 一个窄值到一个字节的其余部分:(x ^ m) - m
  • @aqrit:是的,这就是 clang 自动矢量化 int8_t x<<=4 的方式; x>>=4Deinterleve vector of nibbles using SIMD (godbolt.org/z/sGafhr)。使用高位 0 执行此操作结果等同于使用高位 1 执行 (x^m) + m,就像我想出的这个答案一样。
【解决方案2】:

这是模拟“psrab”的另一种方法,它适用于具有 1 个暂存寄存器的 SSE 或 AVX:

  __ punpckhbw(scratch, src);  // junk in low bytes
  __ punpcklbw(dst, src);      // junk in low bytes
  __ psraw(scratch, 8 + shift);
  __ psraw(dst, 8 + shift);
  __ packsswb(dst, scratch);   // pack words to get result

【讨论】:

  • 我想出了一种方法,只需要 4 个单微指令 SSE2 指令,需要 2 个常量,但不需要暂存寄存器。它使用来自paddb 的进位来清除或保留设置一些高1 位以重做每个字节中的符号扩展。请参阅我的答案的底部。
猜你喜欢
  • 2012-06-06
  • 1970-01-01
  • 1970-01-01
  • 2013-05-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多