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

Shifiting xmm integer register values using non-AVX instructions on Intel x86 architecture

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

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

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

再补充一点信息。这是我尝试使用 SSE/AVX 优化的代码。它是我的 draughts/checkers 引擎的一部分。

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;
}

非常感谢任何帮助。

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 位整数右移不同的值会比向量除法更快。乘法也是如此,但是,首先我会在代码中测试它。

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

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

可能是错误的选项:

  • 解压为标量:糟糕。对于编译时常量计数有点不好,但对于 运行 时间变量计数更糟,尤其是当您必须解压缩计数向量时。没有 BMI2 shrx 的 x86 变量计数移位具有笨拙的语义并在 Intel SnB 系列上解码为多个 uops。他们还采用额外的 mov 指令将班次计数放入 cl 中(如果尚不存在的话)。

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

    因此,将您未使用的高位元素归零,2x pshufd 复制+将零洗牌到位,3x psrld 具有不同的计数,并清除向量中您未复制的其他元素,然后 OR 3 个向量重新组合在一起。 (如果您不让矢量的一个元素未被使用,则需要做更多的工作。)

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

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


希望有更好的选择。

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

  • 当最小移位数和最大移位数之差<=最小移位数时,可以将multiply作为变量左移以解释右移计数的差异。或者单独作为左移。对于大多数情况,这可能是最好的,即使 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;
}

这很好,因为它就地运行,编译器不需要任何 MOVDQA 指令来复制寄存器,即使没有 AVX1。

请注意 SSE4.1 _mm_mullo_epi32 并不快:Haswell 上的 p0 为 2 微指令:10c 延迟和每 2c 吞吐量一个。 Skylake 上的吞吐量更好,其中每个 2 微指令都可以在 p0 或 p1 上 运行,但仍然依赖于 10c 延迟。 (http://agner.org/optimize/ and other links in the 标记 wiki。)

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

在 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。我们可以从 vshift 中分离出 y>>2 并再次添加它以产生 y>>1。 (但切记不要使用 xy00 中的旧 y>>3)。

我们还可以考虑使用 _mm_mul_epu32 (pmuludq) 一次,然后在另一步中使用 copy+shift+AND(从原始 v 复制而不是 rshift 来缩短 dep 链)。这对您的情况很有用,因为您没有使用顶部元素,因此只有一个有效的奇数元素,因此您不需要变量移位。

结合使用 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 随机播放在 Nehalem 以外的 Intel CPU 上很好(每条路都有 2 个周期的旁路延迟延迟惩罚,但吞吐量仍然可以)。我认为它在 AMD 上也很好。

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

code + asm output on Godbolt