use*_*761 4 c++ bit-shift bitwise-operators avx avx2
在AVX2我们已经_mm256_srlv_epi32(a, b)和_mm256_sllv_epi32(a, b)用于在"A"通过在"B"的8个值移位的一组8个值的.是否有一个使用AVX的有效替代方案,以便我可以留在AVX而不必吐出标量代码?
AVX1没有256b整数运算,只有FP.所以我假设你真的在寻找替代方案__m128i _mm_srlv_epi32().使用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 shuffle单元,而不是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 ]
Run Code Online (Sandbox Code Playgroud)
vblendps在SnB/IvB上的p0/5上运行.等效vpblendw运行在SnB/IvB上的p1/p5上.在Haswell/SKL上,p015与p5相比,所以混合效果要好得多(同样选择端口PAND).对于SnB,可以使用两者的组合来混合移位结果.对于内在函数,在整数数据上使用FP指令需要大量的转换,这使得源难看且难以阅读.除非你打算通过perf计数器和pblendw 微基准测试来调整它以适应周围的代码,否则只需使用SnB/IvB.否则只是投射和使用blendps.
另外,如果你有一个[ 0 -1 0 -1 ]可用的掩码,一个向量AND可以在更多的端口上运行,并缩短依赖链xmm3.这不足以证明加载或生成掩码的合理性,因此更喜欢使用shift/shuffles/blend进行所有操作的先前版本.
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 ]
Run Code Online (Sandbox Code Playgroud)
旁注:奇怪的是,在Skylake上,VPSRLVD ymm,ymm,ymm比PSRLD xmm,xmm,xmm(2 uop)更便宜(1 uop ).但是,立即计数PSRLD仅为1 uop.(来自Agner Fog的insn表).
@ BeeOnRope的测试证实,Agner的延迟数字是从数据输入到数据输出,而移位计数不在关键路径上.从移位计数输入到数据输出的延迟为2c(xmm)或4c(ymm),通常用于1c的车道内广播,而3c用于车道交叉广播.
movaps [rsp - 16], xmm0
shr [rsp - 16], 3 ; 3 uops with a memory-destination. 5 uops for variable count with a memory destination
shr [rsp - 12], 1
shr [rsp - 8], 4
shr [rsp - 4], 1
movaps xmm0, [rsp - 16] ; store-forwarding stall here from the 4x 32b stores to the 128b load
Run Code Online (Sandbox Code Playgroud)
或者可能是变量计数:
## 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
Run Code Online (Sandbox Code Playgroud)
因此,可变计数移位的全寄存器方式是6uops + 9uops*3,总共33 uop.
内存目标版本是14个融合域uops,因为我计算了一个具有shift-count作为编译时常量的版本.加载或pextr计数到ecx会更多,因为每个变量计数移位比立即计数移位多2个uop.
因此即使SSE/AVX版本非常讨厌,也不是那么令人讨厌.完全变量的矢量版本仍然存在
vpsrld xmm,xmminsn有8个uopsvpblendw或uops用于vblendps合并这些结果.因此,全变量向量版本与完全常量存储/标量shuffle/reload版本一样糟糕,并且其中存在转储停顿.
请注意,仅计算融合域uops并不总是唯一相关的事情.延迟可能很重要,未融合域中的执行端口压力可能很重要.
为了比较:
vpsrlvd ymm, ymm, ymm1 uop,1c延迟,每0.5c吞吐量一个.vpsrlvd ymm, ymm, ymm3 uops,2c延迟,每2c吞吐量一个.请记住,这是一个256b的矢量.我所做的所有计数都是针对128b向量的.
在Haswell(而不是SnB/IvB)上,我的SSE版本可能会成为洗牌端口吞吐量的瓶颈.延迟也会稍微恶化,因为资源冲突限制了它可以利用的insn级别并行性的数量.
pmulld乘以2的幂.在SnB/IvB上,SSE4.1 pmulld是1 uop,5c延迟,每1c吞吐量一个.
在Haswell上,它是2 uops,10c延迟,每2c吞吐量一个.(Skylake的吞吐量是其两倍,因为它的uop可以在p1和p0上运行)
诀窍是将班次计数变为2 c.一种方法是使用可变班次.如果你可以重复使用2 c的指数向量来移动多个其他向量,那么这很好,否则它就是鸡与蛋的问题.
如果移位计数范围很小(即0..7),则可以使用SSSE3 pshufb作为LUT将计数向量映射到2 ^ c的向量. 0在每个元素的低字节必须变为1(2 0),但0在其他字节中必须保持为零.
## 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
Run Code Online (Sandbox Code Playgroud)
Intel SnB/IvB:3 uops(不包括AVX不需要的movdqa).从班次计数到结果的延迟:7c.从班次数据到结果的延迟:5c.吞吐量:每1c一个(因为所有三个uop都可以在不同的端口上运行).
Haswell和后来:5c更高的延迟.Penryn/Nehalem也pmulld比SnB 更多地使用uop,但没有像Haswell那样糟糕的延迟.
LUT在高64b中全为零,但说服编译器仅存储相关部分并使用movq加载它并非易事.我不会在这里讨论.
为了处理更大的移位计数,我们可以使用带有查找的多个LUT [ D-8 C-8 B-8 A-8 ]来获取每个32b元素的第二个字节的值,等等.注意,C-8如果符号位设置为C<8,并BLENDVB根据设置的符号位进行合并.但是它很昂贵,所以一系列的合并可能不会比使用早期的shift/blend-immediate方法更好.
除了屏蔽pshufb结果,您可以改为添加矢量set1_epi32(1).那么LUT中具有非零字节的索引范围将是1..8,并且移位计数向量中的填充0字节将查找LUT的低元素(应该是0).这样做可以使动态恒定生成更加可行:
## 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
Run Code Online (Sandbox Code Playgroud)
没有优势,除非你真的关心在一个较少的insn中动态生成一个常数.(使用pcmpeqw/psrld 24生成set1_epi32(0xff)很快,但编译器通常只能在一个insn中执行时动态生成.)
OP在聊天中澄清说问题实际上要简单得多:被移位的数据是编译时常量(特别是0xF).此外,只需要结果的低8位.
这使得仅用PSHUFB作为LUT实现它是微不足道的,不需要乘法.请参阅使用pshufb执行此答案的上一部分2<<count.
如果您想要32b结果,则可能会生成[ 0 0 D+8 D | 0 0 C+8 C | ... ]用作控件掩码.利用LUT的每一半中的正确数据,将产生正确的两个字节.