在Intel x86架构上使用非AVX指令移动xmm整数寄存器值

时间:2017-10-28 20:07:35

标签: c++ x86 simd intrinsics sse2

我有以下问题需要使用除AVX2以外的任何其他方法来解决。

我有3个值存储在m128i变量中(不需要第4个值),需要将这些值移动4,3,5。我需要两个功能。一个用于右侧逻辑移位,另一个用于左侧逻辑移位。

有没有人知道使用SSE / AVX解决问题的方法?我唯一能找到的是_mm_srlv_epi32() AVX2。

添加更多信息。这是我尝试使用SSE / AVX进行优化的代码。这是我的草稿/跳棋 - 发动机的一部分。

uint32_t Board::getMoversBlack(){
    const uint32_t nocc=~(BP|WP);
    const uint32_t BK = BP & K;
    uint32_t movers = (nocc >> 4) & BP;
    movers |= ((nocc & MASK_R3) >>3) & BP;
    movers |= ((nocc & MASK_R5) >>5) & BP;
    if (BK != 0) {
        movers |= (nocc << 4) & BK;
        movers |= ((nocc & MASK_L3) << 3) & BK;
        movers |= ((nocc & MASK_L5) <<5) & BK;
    }
    return movers;
}

感谢任何帮助。

2 个答案:

答案 0 :(得分:1)

如果您真的需要这个(并且无法通过重新排列数据来避免它),您可以完全/安全地模拟_mm_srlv_epi32,而不会破坏任何高位或低位。

对于编译时常量计数,您可以使用左右移位混合使用其中大部分。

可能选择不好:

  • 打开标量:yuck。对于编译时常量计数有点不好,但对于运行时变量计数更糟糕,特别是如果您必须解包计数向量。没有BMI2 shrx的x86变量计数移位具有笨重的语义,并在Intel SnB系列上解码为多个uop。他们还会采取额外的mov指示,以便将班次计入cl,如果它不在那里。

  • 进行单独的移位然后混合以从移动了该数量的向量中获取元素。这并不是很好,但您可以通过在复制它们时将不需要的元素归零来降低混合成本。 (例如,如果已知高元素为零,则使用pshufd进行复制,以从{0,22,0,0}的起始向量获取{11,22,33, 0}的向量,并重复{0,0,33,0}。)

    所以,将你不使用的高元素归零,将2x pshufd复制+ shuffle零到位,3x psrld用不同的计数,以及你没有复制的向量中的其他元素,然后将3个向量重新组合在一起。 (如果你不使用矢量的一个元素,这需要更多的工作。)

    根据代码的其余部分和微体系结构,使用shuffle而不是MOVDQA + PAND可能不值得。如果任何元素使用相同的移位计数,则此选项变得更具吸引力。

    此外,您可以将低元素与movss混合到一个向量中,并将低半部分与movsd混合。那些使用shuffle端口,因此shuffle吞吐量可能是一个问题。这实际上可能非常稳固。

希望有更好的选择。

  • Marc建议的SSE2版本(见下文)也适用于完全一般的情况。

  • 当最小和最大班次计数之差<=最小班次计数时,您可以use @Marc's SSE4.1 suggestion 使用乘法作为变量左移来计算右移计数的差异。或者单独作为左移。对于大多数情况来说,这可能是最好的,即使vector-int乘法很慢,也会减少很少的指令。

__m128i srlv435_sse4(__m128i v)
{
    __m128i rshift = _mm_srli_epi32(v, 3);   // v >> 3
    // differences in shift count by multiplying by powers of 2
    __m128i vshift = _mm_mullo_epi32(rshift, _mm_setr_epi32(2,4,1,0)); // [ x >> 2,  y >> 1, z >> 3, 0 ]  Except with low bits truncated.
    __m128i shift2 = _mm_srli_epi32(vshift, 2);                        // [ x >> 4,  y >> 3, z >> 5, 0 ]
     return shift2;
}

这很好,因为即使没有AVX1,它也可以在没有编译器需要任何MOVDQA指令来复制寄存器的情况下就地运行。

请注意, SSE4.1 _mm_mullo_epi32速度不快:Haswell上的p0为2 uops:10c延迟,每2c吞吐量为1。 Skylake的吞吐量更高,其中2个uop中的每一个都可以在p0或p1上运行,但仍然依赖于10c延迟。 (http://agner.org/optimize/标记维基中的和其他链接。)

这在Haswell之前具有更好的延迟,其中pmulld是单uop指令(~5个周期,1c吞吐量),而不是10个周期的2个相关uop。

在AMD Bulldozer系列和Ryzen上,延迟= 4或5,吞吐量= pmulld每2c一个。

我没有通过向量转换来检查端口冲突。

没有SSE4.1 ,您可以使用2x SSE2 _mm_mul_epu32一次进行2次乘法运算。排列奇数元素(1和3),pshufd复制+将它们拖拽到位置0和2,pmuludq寻找它们。

从偶数2个32位元素产生2个64位结果,因此您不需要预移位以避免溢出。当移位计数之间的差异大于最小移位时,它也意味着可以安全使用,因此SSE4.1方式不能保留元素中保留位最多的所有必需位。

// general case: substitute in *any* shift counts and it still works.
__m128i srlv_sse2(__m128i v)  // [x  y  z  w]
{
    __m128i vs_even = _mm_mul_epu32(v, _mm_setr_epi32(1U<<1, 1U<<2, 1U<<0, 0));    // [ x<<1    z<<0 ]  (64-bit elements)
    // The 4 (1U<<2) is unused, but this lets us share a constant with the SSE4 version, saving rodata size.  (Compilers optimize duplicate constants for you; check the disassembly for same address)
    vs_even = _mm_srli_epi64(vs_even, 5);  // [ x>>4  0   x>>5  0 ]  (32-bit elements ready for blending with just an OR)

    __m128i odd = _mm_shuffle_epi32(v, _MM_SHUFFLE(3, 3, 1, 1));
    __m128i vs_odd =  _mm_mul_epu32(v, _mm_setr_epi32(1U<<(32-3),0,0,0));    // [ (y<<32) >> 3    0 ]  (64-bit elements)

    // If any elements need left shifts, you can't get them all the way out the top of the high half with a 32-bit power of 2.
    //vs_odd = _mm_slli_epi64(vs_odd, 32 - (3+2));       // [ garbage,  y>>3,  0, 0 ]

    // SSE2 doesn't have blend instructions, do it manually.
    __m128i vs_oddhi = _mm_and_si128(vs_odd, _mm_setr_epi32(0, -1, 0, -1));
    __m128i shifted = _mm_or_si128(vs_even, vs_oddhi);

     return shifted;
}

这里有一些明显的优化:

你的情况不是使用第4个元素,所以第2个乘法是没有意义的:只需移动并使用AND掩码来清除高元素。 vs_odd = _mm_srli_epi32v, 3);并使用0,-1,0,0作为您的AND掩码。

不是左移1和0,而是将x添加到自身并保持z不变。使用高64位的归零复制向量非常便宜(movq),但不如movdqa便宜(在具有mov-elimination的CPU上)。

    __m128i rshift = _mm_srli_epi32(v, 3);         // v >> 3
    __m128i xy00   = _mm_move_epi64(rshift);
    __m128i vshift = _mm_add_epi32(rshift, xy00);       // [ x >> 2,  y >> 2, z >> 3, 0 ]

但这并不能处理y。我们可以将y>>2vshift隔离开来并再次添加以生成y>>1。 (但请记住不要使用y>>3中的旧xy00

我们还可以考虑使用_mm_mul_epu32pmuludq)一次,并使用复制+ shift + AND进行另一步骤(从原始v而不是rshift复制到缩短dep链)。这在你的情况下非常有用,因为你没有使用top元素,因此只有一个有效的奇数元素,因此你不需要变量。

结合movqmovssmovsd,这里可能会有更多的东西从基本上分别移动3个元素。在端口压力,延迟,uop计数(前端吞吐量)和诸如此类的东西之间存在权衡。例如我在想

movdqa  xmm1, xmm0
psrld   xmm0, 3        #  [ x>>3  y>>3    garbage ]
psrld   xmm1, 4        #  [ x>>4  y>>4    garbage ] 
movss   xmm1, xmm0     #  [ x>>3  y>>4    garbage ]   # FP shuffle

psrld   xmm0, 2        #  [ garbage        z>>5 ]
movsd   xmm0, xmm1     #  [ x>>3  y>>4     z>>5 ]     # FP shuffle
例如,Haswell每个时钟吞吐量只有1个移位,所以这并不奇妙。与乘法选项相比,它具有相当好的延迟。它在Skylake上很不错,其中2个端口可以立即进行向量转换。

整数指令之间的FP shuffle在除Nehalem之外的Intel CPU上是很好的(其中每个方向都有2个周期的旁路延迟延迟惩罚,但吞吐量仍然可以)。我认为AMD也很好。

当然所有这些CPU都有SSE4.1,所以如果使用动态运行时调度,SSE2版本只需要在Core2 / K10上运行。 (我猜老Atom,或其他什么)。

code + asm output on Godbolt

答案 1 :(得分:0)

SSE2英特尔架构指令集扩展引入了整数移位操作。下面我插入了一个可用的编译器内在函数列表,在SSE2中实现逻辑移位操作:

psllw
__m128i _mm_sll_epi16 (__m128i a, __m128i count)

pslld
__m128i _mm_sll_epi32 (__m128i a, __m128i count)

psllq
__m128i _mm_sll_epi64 (__m128i a, __m128i count)

psllw
__m128i _mm_slli_epi16 (__m128i a, int imm8)

pslld
__m128i _mm_slli_epi32 (__m128i a, int imm8)

psllq
__m128i _mm_slli_epi64 (__m128i a, int imm8)

pslldq
__m128i _mm_slli_si128 (__m128i a, int imm8)

psrlw
__m128i _mm_srl_epi16 (__m128i a, __m128i count)

psrld
__m128i _mm_srl_epi32 (__m128i a, __m128i count)

psrlq
__m128i _mm_srl_epi64 (__m128i a, __m128i count)

psrlw
__m128i _mm_srli_epi16 (__m128i a, int imm8)

psrld
__m128i _mm_srli_epi32 (__m128i a, int imm8)

psrlq
__m128i _mm_srli_epi64 (__m128i a, int imm8)

psrldq
__m128i _mm_srli_si128 (__m128i a, int imm8)

有关详情,请访问Intel Intrinsics Guide网站。

如果上述内在函数将所有值移位相同位数的限制并不适合每个人,则可以使用乘以2的幂并除以2的幂,但这会对性能产生重大影响。可能右移3个32位整数,每个不同的值将比矢量分割更快。乘法也是如此,但首先我会在代码中测试它。