在左移一位的特殊情况下,您可以使用paddb xmm0, xmm0。
正如 Jester 在 cmets 中指出的那样,模拟不存在的 psrlb 和 psllb 的最佳选择是使用更宽的移位,然后屏蔽任何跨越元素边界的位。
例如
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 的 vpbroadcastd 或 vbroadcastss,来自 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,然后使用pack 或pshufb。
我没有看到任何类似有效的方法来模拟算术右移(不存在的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);
}