SSE优化的64位整数仿真

10

我正在从事一个爱好项目,需要在x86 CPU上模拟某些64位整数操作,并且需要保证速度

目前,我是通过使用MMX指令来完成这个任务,但这真的很麻烦,因为我必须一直刷新fp寄存器状态(而且因为大多数MMX指令处理有符号整数,而我需要无符号行为)。

所以我想知道SSE/优化高手们是否能够提供一种更好的实现方式,使用SSE来完成。

我需要的操作是以下(相当具体)的操作:

uint64_t X, Y;

X = 0;
X = 1;
X << 1;
X != Y;
X + 1;
X & 0x1 // get lsb
X | 0x1 // set lsb
X > Y;

我的需求很具体,我不需要通用的加法或移位,仅仅是加一和左移一。真的,只需要这里显示的确切操作。

当然,在x86上,uint64_t是通过使用两个32位标量来模拟的,这很慢(而且在我的情况下根本行不通,因为我需要加载/存储是原子的,但是当加载/存储两个单独的寄存器时,它们不会是原子的)。

因此,我需要一个SIMD解决方案。 其中一些操作是微不足道的,并已受到SSE2的支持。其他操作(例如!=<)则需要更多的工作。

有建议吗? SSE和SSE2都可以。允许SSE3需要一些说服力,而SSE4可能不可行(支持SSE4的CPU可能已经运行在64位上了,所以我不需要这些解决方案)。


不需要乘法,只需要我上面展示的特定操作(所以甚至没有一般的加法,只是递增1。是的,SSE2提供了加法,但我想我也可以展示我需要的所有操作,为了完整起见。这意味着其中一些很容易 :) - jalf
1
如果您使用的CPU不支持64位但支持SSE2,那么这可能是Athlon XP、Pentium III或旧款Pentium IV。在Athlon XP的情况下,我不会期望任何性能提升,因为它将每个SSE操作分成两个64位操作,然后分别执行。对于Pentium III-好吧,我不知道。对于Pentium IV,您可能能够获得一些加速-这取决于通用寄存器之间的传输有多频繁,因为这些硬件上的传输非常缓慢。 - Gunther Piez
@drhirsch,虽然人们仍在使用32位操作系统,但所有这些64位硬件都很好,但您经常无法使用它。 - harold
1
@drhirsch,我不知道你想表达什么意思。你是因为无聊而挑剔吗?是的,我知道操作系统不限制可用的SSE指令集。我的机器是一台i7运行在64位操作系统上。但我希望我的代码也能在其他计算机上运行,包括那些由于操作系统或CPU的原因被限制为32位代码的计算机。依赖于SSE4.2将切断大多数32位计算机。依赖于SSE2将覆盖几乎所有32位计算机。现在,你有什么相关的贡献吗? - jalf
1
为什么你不这样写你的问题呢?现在它听起来像是你需要在一个不能运行在64位模式下的CPU上进行64位操作,可能是一些老的东西。 - Gunther Piez
显示剩余8条评论
1个回答

18

SSE2对一些64位整数操作提供了直接支持:

将两个元素都设置为0:

__m128i z = _mm_setzero_si128();

将两个元素都设置为1:
__m128i z = _mm_set1_epi64x(1);      // also works for variables.
__m128i z = _mm_set_epi64x(hi, lo);  // elements can be different

__m128i z = _mm_set_epi32(0,1,0,1);  // if any compilers refuse int64_t in 32-bit mode.  (None of the major ones do.)

设置/加载低64位,将其扩展为__m128i(一种数据类型)
// supported even in 32-bit mode, and listed as an intrinsic for MOVQ
// so it should be atomic on aligned integers.
_mm_loadl_epi64((const __m128i*)p);     // movq or movsd 64-bit load

_mm_cvtsi64x_si128(a);      // only ICC, others refuse in 32-bit mode
_mm_loadl_epi64((const __m128i*)&a);  // portable for a value instead of pointer

基于_mm_set_epi32的内容可能会被一些编译器编译成混乱的代码,因此,在MSVC、ICC以及gcc/clang中,_mm_loadl_epi64似乎是最好的选择,并且实际上对于您在32位模式下的原子64位加载要求也应该是安全的。请参见Godbolt编译器浏览器
垂直加/减每个64位整数:
__m128i z = _mm_add_epi64(x,y)
__m128i z = _mm_sub_epi64(x,y)
左移位:
__m128i z = _mm_slli_epi64(x,i)   // i must be an immediate

http://software.intel.com/sites/products/documentation/studio/composer/en-us/2011/compiler_c/intref_cls/common/intref_sse2_int_shift.htm

按位运算符:

__m128i z = _mm_and_si128(x,y)
__m128i z = _mm_or_si128(x,y)

http://software.intel.com/sites/products/documentation/studio/composer/en-us/2011/compiler_c/intref_cls/common/intref_sse2_integer_logical.htm

SSE没有增量,所以您需要使用常数1


比较更难,因为直到SSE4.1的64位支持才存在pcmpeqq和SSE4.2的pcmpgtq

这是用于相等性的一个示例:

__m128i t = _mm_cmpeq_epi32(a,b);
__m128i z = _mm_and_si128(t,_mm_shuffle_epi32(t,177));

这将把每个64位元素设置为0xffffffffffff(也称为-1),如果它们相等。如果您想在int中使用01,可以使用_mm_cvtsi32_si128()将其提取出来并加1。(但有时您可以使用total -= cmp_result;而不是转换和添加。) 小于:(未经充分测试)
a = _mm_xor_si128(a,_mm_set1_epi32(0x80000000));
b = _mm_xor_si128(b,_mm_set1_epi32(0x80000000));
__m128i t = _mm_cmplt_epi32(a,b);
__m128i u = _mm_cmpgt_epi32(a,b);
__m128i z = _mm_or_si128(t,_mm_shuffle_epi32(t,177));
z = _mm_andnot_si128(_mm_shuffle_epi32(u,245),z);

如果a中的相应元素小于b,则此操作将每个64位元素设置为0xffffffffffff


这里有两个返回布尔值的版本:"equals"和"less-than"。它们返回底部64位整数的比较结果。

inline bool equals(__m128i a,__m128i b){
    __m128i t = _mm_cmpeq_epi32(a,b);
    __m128i z = _mm_and_si128(t,_mm_shuffle_epi32(t,177));
    return _mm_cvtsi128_si32(z) & 1;
}
inline bool lessthan(__m128i a,__m128i b){
    a = _mm_xor_si128(a,_mm_set1_epi32(0x80000000));
    b = _mm_xor_si128(b,_mm_set1_epi32(0x80000000));
    __m128i t = _mm_cmplt_epi32(a,b);
    __m128i u = _mm_cmpgt_epi32(a,b);
    __m128i z = _mm_or_si128(t,_mm_shuffle_epi32(t,177));
    z = _mm_andnot_si128(_mm_shuffle_epi32(u,245),z);
    return _mm_cvtsi128_si32(z) & 1;
}

我刚刚更新了“小于”代码。但是不确定是否正确。 - Mysticial
我在VS2010上写了一个“小于”情况的快速测试这里。可能需要一些调整才能在其他编译器上运行。它对于0x00000000 < 0x00000001失败了(除非我在测试中犯了错误)。 - jalf
它对我有效。你的链接显示它甚至没有编译。或者是我漏掉了什么? - Mysticial
嘿嘿,是的,开始看起来很丑陋(正如预料的那样)。不过,我已经将这些值存储在SSE寄存器中(装载 / 存储必须是原子的,这意味着我不能只加载一对32位无符号整数),所以转换为uint64_t需要额外的转换。不过,这可能是值得的。谢谢你的尝试。我觉得这次比我的MMX尝试要长,所以它似乎是一次净改进 :) - jalf
在这种情况下,只需将__m128i d = _mm_sub_epi64(a, b)与溢出检测器_mm_and_si128(_mm_xor_si128(a, b), _mm_xor_si128(d, a))进行异或运算,然后填充符号位以创建0或-1。 - plasmacel
显示剩余4条评论

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