如何最好地移动 __m128i?

5
我需要将一个__m128i变量(比如v)向左移动m位,使得所有位都经过移动(因此,结果变量表示v*2^m)。最好的方法是什么?请注意,_mm_slli_epi64会分别移动v0和v1:
r0 := v0 << count
r1 := v1 << count

所以v0的最后一些位被忽略了,但我想把这些位移到r1。

编辑: 我正在寻找比这个代码更快的代码(m < 64):

r0 = v0 << m;
r1 = v0 >> (64-m);
r1 ^= v1 << m;
r2 = v1 >> (64-m);

1
如果 m 恰好是 8 位的倍数,并且您拥有 SSSE3,那么您很幸运:使用 palignr。如果没有,情况会变得非常丑陋,您真的需要进行移位、AND、洗牌和OR操作。 - Iwillnotexist Idonotexist
1
请参见https://dev59.com/SWkw5IYBdhLWcg3wUI7x。 - Craig Estey
你是在处理位流还是算术变量(整数、浮点数等)? - bazza
@user0,我原本想提出的答案没有用处,抱歉。 - bazza
1
如果你不必使用SSE,shld+sal并不算太糟糕。 - Marc Glisse
显示剩余6条评论
2个回答

3
对于编译时常量移位计数,您可以得到相当不错的结果。否则实际上不行。
这只是您问题中r0 / r1代码的SSE实现,因为没有其他明显的方法可以做到这一点。变量计数移位仅适用于向量元素内的位移,而不适用于整个寄存器的字节移位。因此,我们只需将低64位传递到高64位,并使用可变计数移位将它们放在正确的位置即可。
// untested
#include <immintrin.h>

/* some compilers might choke on slli / srli with non-compile-time-constant args
 * gcc generates the   xmm, imm8 form with constants,
 * and generates the   xmm, xmm  form with otherwise.  (With movd to get the count in an xmm)
 */

// doesn't optimize for the special-case where count%8 = 0
// could maybe do that in gcc with if(__builtin_constant_p(count)) { if (!count%8) return ...; }
__m128i mm_bitshift_left(__m128i x, unsigned count)
{
    __m128i carry = _mm_bslli_si128(x, 8);   // old compilers only have the confusingly named _mm_slli_si128 synonym
    if (count >= 64)
        return _mm_slli_epi64(carry, count-64);  // the non-carry part is all zero, so return early
    // else
    carry = _mm_srli_epi64(carry, 64-count);  // After bslli shifted left by 64b

    x = _mm_slli_epi64(x, count);
    return _mm_or_si128(x, carry);
}

__m128i mm_bitshift_left_3(__m128i x) { // by a specific constant, to see inlined constant version
    return mm_bitshift_left(x, 3);
}
// by a specific constant, to see inlined constant version
__m128i mm_bitshift_left_100(__m128i x) { return mm_bitshift_left(x, 100);  }

我以为这会比实际情况更不方便。即使计数不是编译时常量,_mm_slli_epi64 在 gcc/clang/icc 上也可以工作(从整数寄存器生成 movd 到 xmm 寄存器)。有一个 _mm_sll_epi64 (__m128i a, __m128i count)(注意缺少 i),但至少现在,i 内置函数可以生成两种形式的 psllq
编译时常数版本相当高效,编译为4条指令(如果没有AVX则为5条):
mm_bitshift_left_3(long long __vector(2)):
        vpslldq xmm1, xmm0, 8
        vpsrlq  xmm1, xmm1, 61
        vpsllq  xmm0, xmm0, 3
        vpor    xmm0, xmm0, xmm1
        ret

性能表现:

在Intel SnB/IvB/Haswell上,这个操作的延迟为3个周期(vpslldq(1) -> vpsrlq(1) -> vpor(1)),吞吐量每2个时钟周期限制为1个(饱和向量移位单元在端口0上)。字节移位在不同端口的洗牌单元上运行。立即计数向量移位是所有单独uop指令,因此当与其他代码混合使用时,仅占用4个融合域uops管道空间。(变量计数向量移位是2 uop,2个周期延迟,因此从计算指令数量来看,该函数的变量计数版本比它看起来更糟糕。)

或当计数>=64时:

mm_bitshift_left_100(long long __vector(2)):
        vpslldq xmm0, xmm0, 8
        vpsllq  xmm0, xmm0, 36
        ret

如果您的移位计数不是编译时常量,则必须根据计数是否大于64进行分支,以确定是左移还是右移进位。我认为移位计数被解释为无符号整数,因此负计数是不可能的。
将int计数和64-count转换为向量寄存器需要额外的指令。使用向量比较和混合指令无需分支可能是可行的,但是分支可能是一个好主意。

__uint128_t在GP寄存器中的变量计数版本看起来相当不错,比SSE版本更好。Clang做得比gcc稍微好一点,发出较少的mov指令, 但对于计数 >= 64 的情况仍使用了两个 cmov 指令。(因为x86整数移位指令掩码计数,而不是饱和)

__uint128_t leftshift_int128(__uint128_t x, unsigned count) {
    return x << count;  // undefined if count >= 128
}

非常感谢。不幸的是,count 不是编译时常量。但我会测试两个建议。 - user0
根据我的测试,使用4个int64_t变量编写的旧代码在随机生成的count情况下更快(>2倍);但对于编译时常量的countmm_bitshift_left至少快1.5倍。 - user0
@user0:我并不感到惊讶。在一个真正的应用程序中,我希望移位计数有一定的可预测性。此外,你的微基准测试是仅测试了移位,还是将移位作为两个其他矢量内部操作之间的操作进行测试?在这种情况下,int64_t 移位将不得不从矢量获取值到 GP 寄存器,然后再返回。 (我认为在我的答案中说过,如果你的数据不在矢量寄存器中,__uint128 移位(或其手写等效项 int64_t)应该做得很好。) - Peter Cordes
仅测量移位操作所需的时间。我将测试__uint128 - user0
是的!__uint128 比其他方法更快。对于随机 count,它至少比 int64_t 方法快 1.5 倍。但似乎有些机器不支持 128 位整数。 - user0
1
@user0:这是一个编译器扩展。当你说“一些机器”时,你指的是一些编译主机,而不是一些目标。编译使用__uint128t的代码时发出的机器指令只是基线x86的标准加法进位、双倍移位等。 - Peter Cordes

1
在SSE4.A中,指令insrqextrq可用于每次以1-64位的方式移位(和旋转)__mm128i。与8/16/32/64位对应物pextrN/pinsrX不同,这些指令选择或插入m位(介于1到64之间)在0到127之间的任何位偏移。但需要注意的是,长度和偏移量的总和不能超过128。

请查看修订后的答案。正确指令中没有字母p。 - Aki Suihkonen
3
限制条件似乎是它只支持AMD。 - Aki Suihkonen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接