将 4 个整数向右移动不同的值 SIMD

Shifting 4 integers right by different values SIMD

本文关键字:SIMD 移动 右移 整数      更新时间:2023-10-16

SSE 不提供将打包整数按可变量移动的方法(我可以使用任何指令 AVX 及更早)。您只能进行统一轮班。我试图为向量中的每个整数实现的结果是这样的。

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位域提取。 有一个示例,用于将 8 个半字节解压缩为 8 个字节以进行 int->hex。 为此,您将使用 AND 掩码跟随它,因为它以 8 位块的形式抓取数据(但来自不必与字节边界对齐的源位置)。


在没有 AVX2 的情况下模拟它:

这部分属于什么样的依赖链? 你能展开和交错,让两个向量同时飞行吗? 并行的两个长 dep 链比一个长 dep 链要好得多,如果它太长以至于乱序窗口在下一个循环迭代中看不到下一个 dep 链。


可能值得为您的函数制作一个单独的 AVX2 版本,以便在 Haswell 和更高版本的 CPU(您可以使用可变移位)上使用。 如果这样做,你的函数将只在最有效的CPU上使用pmulldmullo_epi32)。 (即,您避免在 AVX2 CPU 上mullo_epi32 SSE4.1,因为事实证明这些 CPU 使该指令变慢。

pmulld看起来,即使在 Haswell 上,我们也能为吞吐量和融合域 uop 计数做最好的事情。

在 SnB/IvB 上,它是向量整数乘法单元的单个 uop,整个函数仅为 2 uops/6 周期延迟/每 1c 吞吐量一个。 (这比我用shift/blend管理更糟糕,所以你只想在吞吐量/代码大小很重要的情况下使用pmulld,而且你不会纯粹在延迟上遇到瓶颈。 例如,展开后。

如果您的移位计数是常量,并且寄存器顶部有备用位,则可以乘以 2 的幂,然后使用固定的右移。 将__m128i中的每个 DW 右移不同的量。 敲掉高位对于您的位域提取来说不是问题,因为无论如何您都只保留低位。

// 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仅限于英特尔 CPU 上的端口 5。 blendps可能有利于吞吐量(在任何端口上运行),但可能会在整数指令之间引入旁路延迟。

移位

和混合,以便每个元素最终获得正确的总移位计数。

AND-屏蔽将所有内容组合成一个向量后的低 6 位。

与英特尔 CPU 上的暴力方式(见下文)相同的延迟,以及更好的吞吐量(因为更少的 uops)。 只有两个立即混合捆绑 port5 是不错的。 (AVX2 vpblendd可以在任何端口上运行,但我们只使用vpsrlvd

// 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 uops,使用 gcc 5.3 编译-O3 -march=ivybridge

    # 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)将四个向量组合成一个具有每个所需元素的向量。

// 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

使用线性依赖链而不是树进行合并意味着合并可以在最后一个班次结果准备就绪后更快地完成:

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 来说可能是缺点。