关于c ++:将4个整数右移不同的值SIMD

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位不同的组。

那么最佳的解决方案是什么?

我想到的事情:
您可以模拟可变的右移,可变的左移和统一的右移。我考虑过将打包的整数分别乘以不同的量(因此模拟左移)。然后使用结果,您可以执行统一的右移操作以获得答案。我将用于乘法的特定运算的问题是_mm_mullo_epi32,它具有令人失望的延迟(haswell为10个周期),并且给定我的程序,它必须等待结果,因为该特定结果是对接下来的说明。总的来说,我认为该方法仅比蛮力方法快一点,后者是解压缩,使用标量指令进行移位,然后重新打包向量,我认为这大约需要20个周期。


如果AVX2可用,则仅需执行一条有效指令。例如__m128i _mm_srlv_epi32 (__m128i a, __m128i count)(vpsrlvd)和256位版本。左,右算术和右逻辑提供了通过相应计数元素对32位和64位元素进行的可变移位。 (算术右移不适用于64位元素大小。)

AVX512BW增加了16位可变移位。

AVX512VBMI在每个qword中具有vpmultishiftqb位域提取。有一个使用它将int-> hex的8个字节解压缩为8个字节的示例。为此,您将使用AND掩码来跟随它,因为它以8位块的形式捕获数据(但可以从不必与字节边界对齐的源位置中捕获数据)。

在没有AVX2的情况下进行仿真:

这部分属于哪种依赖链?您能否展开和交织,使两个向量同时飞行?如果很长,以至于无序窗口在下一个循环迭代中看不到下一个dep链,那么两个平行的dep链要比一个长的dep链好得多。

可能需要为您的函数制作单独的AVX2版本,以在Haswell和更高版本的CPU(可以在其中使用可变移位)上使用。如果这样做,您的函数将仅在效率最高的CPU上使用pmulld(mullo_epi32)。 (即,避免在AVX2 CPU上使用SSE4.1 mullo_epi32,因为事实证明,这些CPU使该指令变慢了。)

pmulld看起来是我们在吞吐量和融合域uop计数上所能做到的最好方法,即使在Haswell上也是如此。

在SnB / IvB上,它是向量整数乘法单元的单个uop,整个功能仅为2 uops / 6周期延迟/每1c吞吐量一个。 (这比我使用shift / blend所管理的要差,因此如果吞吐量/代码大小根本不重要,并且您不仅仅在延迟方面成为瓶颈,例如在展开后,就只想使用pmulld。)

如果移位计数是常量,并且寄存器的顶部有备用位,则可以乘以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 vpblendd用于可以在任何端口上运行的高效dword混合。pblendw仅限于Intel CPU上的端口5。blendps对于吞吐量(可以在任何端口上运行)可能很好,但是可以在整数指令之间引入旁路延迟。)

移位和混合,使每个元素最终都具有正确的总移位计数。

将所有内容合并为一个向量后,对低6位进行AND屏蔽。

与英特尔CPU上的蛮力方式(请参阅下文)具有相同的延迟,并且吞吐量更高(由于uops更少)。绑定port5的两个立即混合才是不错的选择。 (AVX2 vpblendd可以在任何端口上运行,但是我们只使用vpsrlvd。)

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 -O3 -march=ivybridge编译:

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可以,但是随后的两个混合周期会吃尽改进。

"强力"方式:

对三个不同的移位计数进行三个移位,然后使用三个立即混合(pblendw)将四个向量组合为一个具有每个所需元素的向量。

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则可能是不利的。