Shifting 4 integers right by different values SIMD
SSE不提供将打包整数移位可变数量的方法(我可以使用任何AVX及更早版本的指令)。您只能进行统一轮班。我试图为向量中的每个整数实现的结果是这样的。
1 2 3 4 | i[0] = i[0] & 0b111111; i[1] = (i[1]>>6) & 0b111111; i[2] = (i[2]>>12) & 0b111111; i[3] = (i[3]>>18) & 0b111111; |
本质上是尝试在每个整数中隔离6位不同的组。
那么最佳的解决方案是什么?
我想到的事情:
您可以模拟可变的右移,可变的左移和统一的右移。我考虑过将打包的整数分别乘以不同的量(因此模拟左移)。然后使用结果,您可以执行统一的右移操作以获得答案。我将用于乘法的特定运算的问题是
如果AVX2可用,则仅需执行一条有效指令。例如
AVX512BW增加了16位可变移位。
AVX512VBMI在每个qword中具有
在没有AVX2的情况下进行仿真:
这部分属于哪种依赖链?您能否展开和交织,使两个向量同时飞行?如果很长,以至于无序窗口在下一个循环迭代中看不到下一个dep链,那么两个平行的dep链要比一个长的dep链好得多。
可能需要为您的函数制作单独的AVX2版本,以在Haswell和更高版本的CPU(可以在其中使用可变移位)上使用。如果这样做,您的函数将仅在效率最高的CPU上使用
在SnB / IvB上,它是向量整数乘法单元的单个uop,整个功能仅为2 uops / 6周期延迟/每1c吞吐量一个。 (这比我使用shift / blend所管理的要差,因此如果吞吐量/代码大小根本不重要,并且您不仅仅在延迟方面成为瓶颈,例如在展开后,就只想使用
如果移位计数是常量,并且寄存器的顶部有备用位,则可以乘以2的幂,然后使用固定的右移。将__m128i中的每个DW右移不同的数量。对于您的位域提取来说,剔除高位并不是问题,因为您必须进行AND运算以仅保留低位。
1 2 3 4 5 6 7 8 9 10 11 | // See the godbolt link below for a version of this with more comments // SnB/IvB: 6c latency, 2 fused-domain uops. __m128i isolate_successive_6bits_mul (__m128i input) { // We can avoid the AND if we shift the elements all the way to the left to knock off the high garbage bits. // 32 - 6 - 18 = 8 extra bits to left shift __m128i mul_constant = _mm_set_epi32(1<<(0+8), 1<<(6+8), 1<<(12+8), 1<<(18+8)); __m128i left_vshift = _mm_mullo_epi32(input, mul_constant); __m128i rightshifted = _mm_srli_epi32(left_vshift, (18+8)); return rightshifted; } |
混合的聪明方法:
(不幸的是,我们没有AVX2
移位和混合,使每个元素最终都具有正确的总移位计数。
将所有内容合并为一个向量后,对低6位进行AND屏蔽。
与英特尔CPU上的蛮力方式(请参阅下文)具有相同的延迟,并且吞吐量更高(由于uops更少)。绑定port5的两个立即混合才是不错的选择。 (AVX2
1 2 3 4 5 6 7 8 9 10 11 12 | // seems to be optimal for Intel CPUs. __m128i isolate_successive_6bits (__m128i input) { // input = [ D C B A ] // output = [ D>>18 C>>12 B>>6 A ] & set1(0b111111) __m128i right12 = _mm_srli_epi32(input, 12); __m128i merged = _mm_blend_epi16(input, right12, 0xF0); // copy upper half, like `movhps` (but don't use that because of extra bypass delay) // merged = [ D>>12 C>>12 B>>0 A>>0 ] __m128i right6 = _mm_srli_epi32(merged, 6); merged = _mm_blend_epi16(merged, right6, 0b11001100); // blend in the odd elements // merged = [ D>>(12+6) C>>12 B>>(0+6) A>>0 ] return _mm_and_si128(merged, _mm_set1_epi32(0b111111)); // keep only the low 6 bits } |
我将两个版本都放在了Godbolt编译器浏览器上。
这个版本只有5微码,使用gcc 5.3
1 2 3 4 5 6 7 8 | # input in xmm0, result in xmm0 isolate_successive_6bits: vpsrld xmm1, xmm0, 12 # starts on cycle 0, result ready for the start of cycle 1 vpblendw xmm0, xmm0, xmm1, 240 # cycle 1 vpsrld xmm1, xmm0, 6 # cycle 2 vpblendw xmm0, xmm0, xmm1, 204 # cycle 3 vpand xmm0, xmm0, XMMWORD PTR .LC0[rip] # cycle 4, result ready on cycle 5 ret |
每条指令都取决于前一条指令,因此它具有5c的延迟。 SnB / IvB / HSW / BDW CPU仅具有一个移位端口,因此它们无法利用更强力的版本中可用的并行性(该版本以不同的移位计数执行三个移位)。 Skylake可以,但是随后的两个混合周期会吃尽改进。
"强力"方式:
对三个不同的移位计数进行三个移位,然后使用三个立即混合(
1 2 3 4 5 6 7 8 9 10 11 | // same latency as the previous version on Skylake // slower on previous Intel SnB-family CPUs. isolate_successive_6bits_parallel: vpsrld xmm1, xmm0, 6 # cycle 0. SKL: c0 vpsrld xmm2, xmm0, 12 # cycle 1 (resource conflict on pre-Skylake). SKL: c0 vpblendw xmm1, xmm0, xmm1, 12 # cycle 2 (input dep). SKL: c1 vpsrld xmm3, xmm0, 18 # cycle 2. SKL: c1 vpblendw xmm0, xmm2, xmm3, 192 # cycle 3 (input dep). SKL: c2 vpblendw xmm0, xmm1, xmm0, 240 # cycle 4 (input dep). SKL: c3 vpand xmm0, xmm0, XMMWORD PTR .LC0[rip] # cycle 5 (input dep). SKL: c4. ret |
使用线性依赖关系链而不是树进行合并意味着合并可以在最后一个移位结果准备好后更快完成:
1 2 3 4 5 6 7 8 9 | isolate_successive_6bits_parallel2: vpsrld xmm1, xmm0, 6 # c0. SKL:c0 vpsrld xmm2, xmm0, 12 # c1. SKL:c0 vpblendw xmm1, xmm0, xmm1, 12 # c2. SKL:c1 vpblendw xmm1, xmm1, xmm2, 48 # c3. SKL:c2 vpsrld xmm0, xmm0, 18 # c2. SKL:c1 vpblendw xmm0, xmm1, xmm0, 192 # c4. SKL:c3 (dep on xmm1) vpand xmm0, xmm0, XMMWORD PTR .LC0[rip] # c5. SKL:c4 ret |
嗯,不,没有帮助。 SnB到BDW或SKL的延迟没有增加。第一次合并只能在一个移位之后发生,因为未移位的输入正是我们需要的一个元素。如果元素0需要一个非零的移位计数,则这种方式对于SKL之前的版本将是有利的,而对于SKL则可能是不利的。