C++ AVX替代AVX2';s矢量偏移?

C++ AVX替代AVX2';s矢量偏移?,c++,bitwise-operators,bit-shift,avx,avx2,C++,Bitwise Operators,Bit Shift,Avx,Avx2,在AVX2中,我们有\u mm256\u srlv\u epi32(a,b)和\u mm256\u sllv\u epi32(a,b)用于将“a”中的一组8个值移动到“b”中的8个值。有没有一种有效的替代方法可以使用AVX,这样我就可以留在AVX中,而不必吐出标量代码?AVX1没有256b整数运算,只有FP。因此,我假设您确实在寻找\uuum128i\umm\usrlv\uepi32()的替代方案。使用extractf128/insertf128,您可以轻松地对256b向量执行此操作,但最好只使

在AVX2中,我们有
\u mm256\u srlv\u epi32(a,b)
\u mm256\u sllv\u epi32(a,b)
用于将“a”中的一组8个值移动到“b”中的8个值。有没有一种有效的替代方法可以使用AVX,这样我就可以留在AVX中,而不必吐出标量代码?

AVX1没有256b整数运算,只有FP。因此,我假设您确实在寻找
\uuum128i\umm\usrlv\uepi32()
的替代方案。使用extractf128/insertf128,您可以轻松地对256b向量执行此操作,但最好只使用更多128b加载/存储,特别是如果您有一个可以在支持AVX2的CPU上运行的AVX2版本。(现有的仅限AVX1的CPU碰巧都有128b的加载/存储数据路径,因此256b的加载/存储几乎不是优势。)

从矢量到标量的往返相当昂贵(或者标量存储后重新加载时存储转发会暂停,或者大量的
movd
/
pextrd
/
pinsrd
),因此即使是非常笨重的东西也可能比整数代码好,这取决于吞吐量或延迟在您使用的代码中更重要

我最好的想法基本上是向量regs中的标量:4个移位(每个不同移位计数一个)和3个立即混合来组合结果。

更新:想法2:使用32位乘以2计数的左移位。请看这个答案的结尾

如果移位计数不是编译时常量,则需要解压缩移位计数向量,以便将每个移位计数作为向量的64b。(非变量移位指令可以在寄存器中进行计数,但它们查看整个低64b。它们不像标量移位那样屏蔽(模字大小),而是饱和

要将xmm寄存器的4个元素中的每一个都隔离在一个否则为零的目标中是很困难的。您不能将它们下移到底部,因为这样会从第二个元素中留下非零字节

由于这是针对没有AVX2的AVX,我假设您有一个单独的AVX2 CPU版本。因此对于Intel,此版本将在SnB/IvB上使用。这意味着您有两个128b洗牌单元,而不是Haswell和更高版本上的一个

## 4 shift-counts in the elements of   xmm0 = [ D C B A ].  element 1 isolated in xmm1, etc.
vpsrlq      xmm2, xmm0, 32           ; xmm2 = [ 0 D 0 B ]
vpunpckhqdq xmm4, xmm2, xmm0         ; xmm4 = [ D C 0 D ]
vpshufd     xmm3, xmm4, 0b01010110   ; xmm3 = [ 0 0 0 C ]
vblendps    xmm1, xmm2, xmm0, 0b0001 ; xmm1 = [ 0 D 0 A ]
; or
vpblendw     xmm1, xmm2, xmm0, 0b00000011 ; xmm1 = [ 0 D 0 A ]
vblendps
在SnB/IvB上的p0/5上运行。等效的
vpblendw
在SnB/IvB上的p1/p5上运行。在Haswell/SKL上,它是p015而不是p5,因此blendps更好(端口选择与
PAND
相同)。对于SnB,可能使用两者的组合来混合移位结果。对于Intrinsic,在整数数据上使用FP指令需要进行大量转换,这使得源代码难看且难以读取。除非您打算通过性能计数器和微基准点对其进行优化,以最适合周围的代码,否则只需对Sn使用
pblendw
B/IvB。否则只需强制转换并使用
blendps

如果你有一个可用的
[0-1 0-1]
掩码,一个向量,可以在更多的端口上运行,并缩短
xmm3
的依赖链。这还不足以证明加载或生成掩码的合理性,所以更喜欢使用移位/洗牌/混合的早期版本

vpcmpeqw   xmm5, xmm5,xmm5            ; all-ones
vpsrlq     xmm5, xmm5, 32             ; [ 0 -1  0 -1 ]: generate the mask on the fly if desired

vpand       xmm1, xmm5, xmm0           ; [ 0 C 0 A ]
vpsrlq      xmm2, xmm0, 32             ; [ 0 D 0 B ]
vpunpckhqdq xmm3, xmm1,xmm1            ; [ 0 C 0 C ]  ; saves 1B vs. the equivalent pshufd: no imm8 byte
vpunpckhqdq xmm4, xmm2,xmm2            ; [ 0 D 0 D ]

旁注:奇怪的是,在Skylake上,
VPSRLVD-ymm,ymm,ymm
PSRLD-xmm,xmm,xmm
(2个单位)便宜(1个单位)。但立即计数
PSRLD
仅为1个单位

@BeeOnRope的测试证实,Agner的延迟数是从数据输入到数据输出的,移位计数不在关键路径上。从移位计数输入到数据输出的延迟是2c(xmm)或4c(ymm),通常是1c车道内广播,而3c车道交叉广播


uop计数: 对于用于编译时常量移位计数的标量代码,整个过程可能如下所示:
或者对于可变计数:

## data in xmm0,  shift counts in xmm1, results in xmm2
vmovd      eax, xmm0      ; 1 uop
vmovd      ecx, xmm1      ; 1 uop
shr        eax, cl        ; 3 uops because of CISC stupidity
vmovd      xmm2, eax      ; 1 uop

vpextrd    eax, xmm0, 1   ; 2 uops
vpextrd    ecx, xmm1, 1   ; 2 uops
shr        eax, cl        ; 3 uops because of CISC stupidity
vpinsrd    xmm2, eax, 1   ; 2 uops

... repeat twice more, for indices 2 and 3    
因此,可变计数移位的所有寄存器方式是6uops+9uops*3,总共33 uops


内存目标版本是14个融合域uops,因为我将移位计数作为编译时常量计算。如果加载或
pextr
ing计数到ecx中,它会更多,因为每个变量计数移位比立即计数移位多2个uops


因此,尽管SSE/AVX版本非常糟糕,但它并没有那么糟糕。完全可变的矢量版本仍然很糟糕

  • 4个UOP用于打开计数
  • 四个
    vpsrld xmm、xmm
    INSN的8个UOP
  • 3个UOP用于
    vpblendw
    vblendps
    合并这些结果
  • 对于全变量AVX1,总计=15个融合域UOP
因此,完全可变向量版本只与完全常量存储/标量洗牌/重新加载版本一样糟糕,并且其中存在存储转发暂停

请注意,计算融合域UOP并不总是唯一相关的事情。延迟可能很重要,而未融合域中的执行端口压力可能很重要


作为比较:

  • Skylake:
    vpsrlvd ymm,ymm,ymm
    是1UOP,1c延迟,每0.5c吞吐量一个
  • Haswell/BDW:
    vpsrlvd ymm,ymm,ymm
    是3个UOP,2c延迟,每2c吞吐量一个
记住,这是一个256b的向量。我所做的所有计数都是针对128b的向量

在Haswell(而不是SnB/IvB)上,我的SSE版本可能会在混洗端口吞吐量上出现瓶颈。延迟也会更糟,因为资源冲突限制了它可以利用的insn级并行量


使用SSE4.1
pmulld
乘以二的幂进行左移。 在SnB/IvB上,SSE4.1
pmulld
是1uOP,5c延迟,每1c吞吐量一个。
在Haswell上,它是2个UOP,10c延迟,每2c吞吐量一个。(是Skylake上吞吐量的两倍,因为它的UOP可以在p1和p0上运行)

诀窍是将移位计数设为int
## data in xmm0,  shift counts in xmm1, results in xmm2
vmovd      eax, xmm0      ; 1 uop
vmovd      ecx, xmm1      ; 1 uop
shr        eax, cl        ; 3 uops because of CISC stupidity
vmovd      xmm2, eax      ; 1 uop

vpextrd    eax, xmm0, 1   ; 2 uops
vpextrd    ecx, xmm1, 1   ; 2 uops
shr        eax, cl        ; 3 uops because of CISC stupidity
vpinsrd    xmm2, eax, 1   ; 2 uops

... repeat twice more, for indices 2 and 3    
##           1<<8 or higher is 0, in an 8bit element
## xmm5 = _mm_set_epi8(0, 0, ..., 1<<7, ..., 1<<2, 1<<1, 1<<0);
## xmm4 = _mm_set1_epi32(0x000000ff);        
## data in xmm0, shift counts in xmm1
movdqa    xmm2, xmm5           ; avoid this with AVX
pshufb    xmm2, xmm5           ; 2^count
pand      xmm2, xmm4           ; zero all but the low byte in each element
pmulld    xmm0, xmm2           ; data * 2^count
## xmm5 = _mm_set_epi8(0, 0, ..., 1<<7, ..., 1<<2, 1<<1, 1<<0, 0);
## data in xmm0, shift counts in xmm1
pcmpeqw   xmm4,xmm4            ; all-ones

psubd     xmm1, xmm4           ; shift_counts -= -1
movdqa    xmm2, xmm5
pshufb    xmm2, xmm1           ; 2^count
pmulld    xmm0, xmm2           ; data * 2^count
__m128i mm_sllv_4_epi32(__m128i v, __m128i vcount)
{
    const __m128i vone = _mm_set1_epi32(1);
    __m128i vtest, vmask;

    vtest = _mm_set1_epi32(0);
    vmask = _mm_cmpgt_epi32(vcount, vtest);
    v = _mm_add_epi32(v, _mm_and_si128(v, vmask));

    vtest = _mm_add_epi32(vtest, vone);
    vmask = _mm_cmpgt_epi32(vcount, vtest);
    v = _mm_add_epi32(v, _mm_and_si128(v, vmask));

    vtest = _mm_add_epi32(vtest, vone);
    vmask = _mm_cmpgt_epi32(vcount, vtest);
    v = _mm_add_epi32(v, _mm_and_si128(v, vmask));

    vtest = _mm_add_epi32(vtest, vone);
    vmask = _mm_cmpgt_epi32(vcount, vtest);
    v = _mm_add_epi32(v, _mm_and_si128(v, vmask));

    return v;
}