在 SSE2 上进行无符号 64 位比较的最有效方法是啥?
Posted
技术标签:
【中文标题】在 SSE2 上进行无符号 64 位比较的最有效方法是啥?【英文标题】:What is the most efficient way to do unsigned 64 bit comparison on SSE2?在 SSE2 上进行无符号 64 位比较的最有效方法是什么? 【发布时间】:2020-12-24 17:30:59 【问题描述】:PCMPGTQ 在 SSE2 上不存在,并且本身不适用于无符号整数。我们的目标是为无符号 64 位比较提供向后兼容的解决方案,以便我们可以将它们包含到 WebAssembly SIMD 标准中。
这是 ARMv7+NEON 的姊妹问题: What is the most efficient way to do SIMD unsigned 64 bit comparison (CMHS) on ARMv7 with NEON?
并且与已为 SSE2 和 Neon 回答的签名比较变体的问题有关:
How to simulate pcmpgtq on sse2?
What is the most efficient way to support CMGT with 64bit signed comparisons on ARMv7a with Neon?
【问题讨论】:
如果您找到了有符号数的解决方案:您可以反转两个无符号值的高位(第 63 位),然后执行有符号比较。 您可能想要设计这个原语,以便它可以使用 AVX512VLVPCMPUQ k1 k2, xmm2, xmm3/m128/m64bcst, imm8
(felixcloutier.com/x86/vpcmpq:vpcmpuq)(如果可用)。比较的是掩码寄存器,而不是向量。但是,如果 SSE4.2 可用,您将异或翻转 pcmpgtq
的两个输入的符号。显然可以使用 SSE2 来实现,并且一些硬件相当有效; SSE2 的低效率水平真的决定了你是否选择将它包含在 WebAssembly 中吗?
@PeterCordes 是用 xor 翻转 63 位还是用唯一的方法减去两个输入上的数字?或者是否有另一种比较方法适用于无符号数,但与我们上次的做法不同?
@njuffa 我正在寻找 SSE2 的方法,而不仅仅是输入的 XOR。
如果你有pcmpgtq
,那么翻转两者的高位几乎肯定是最好的。仅使用 SSE2、IDK,可能有比构建更多步骤来支持现有 pcmpgtq 仿真更聪明的方法。我的观点是,只要 WebAssembly 的用户明白它可能并非在所有地方都高效,像这样一个天真的起点是完全可以的。 (除非世界上零硬件可以在一条指令中完成,即使有或没有 SVE 的 AArch64 也不行;尽管即使您的模型需要 AVX512VL 将结果返回到向量中,也可以进行优化)
【参考方案1】:
译自 Hacker's Delight:
static
__m128i sse2_cmpgt_epu64(__m128i a, __m128i b)
__m128i r = _mm_andnot_si128(_mm_xor_si128(b, a), _mm_sub_epi64(b, a));
r = _mm_or_si128(r, _mm_andnot_si128(b, a));
return _mm_shuffle_epi32(_mm_srai_epi32(r, 31), _MM_SHUFFLE(3,3,1,1));
概念:如果混合“符号”(无符号 MSB)则返回 a
否则返回 b - a
(MSB(a) ^ MSB(b)) ? a : b - a; // result in MSB
这是有道理的:
如果a
的MSB 已设置,而b
未设置,则a
在上面是无符号的(所以MSB(a) 是我们的结果)
如果设置了b
的MSB 而a
没有设置,则a
下面是无符号的(所以MSB(a) 是我们的结果)
如果它们的 MSB 相同,则它们的值在无符号范围的相同一半内,因此 b-a
实际上是 63 位减法。 MSB 将取消,b-a
的 MSB 将等于“借”输出,它告诉您a
是否严格高于b
。 (就像标量 sub
的 CF 标志。jb
是 jc
)。所以 MSB(b-a) 是我们的结果。
请注意,SIMD andnot/and/or 是一个位混合,但我们只关心 MSB。我们用 srai -> shuffle_epi32 广播它,丢弃低位的垃圾。 (或使用 SSE3,movshdup
,如@Soont 的回答中所述。)
与有符号比较不同:
(MSB(a) ^ MSB(b)) ? ~a : b - a; // result in MSB
如果符号混合,那么~a
的符号当然也是b
的符号。
【讨论】:
C 中的(a ^ b) ?
是对整个结果非零的逻辑测试。使用_mm_xor
结果作为混合控件将改为位混合,而不是在两个整体结果中的任何一个之间进行选择。这就是 Hacker's Delight 中这个符号的含义吗?无论如何,你确定这行得通吗,并且你不需要先对零pcmpeqq
(或等效项)?
哦对了,你在选择后广播符号位,所以你只关心混合最高位。而您的a^b
条件实际上是MSB(a) ^ MSB(b)
,而不是整数在所有位位置是否相等,从而使整个 64 位 xor == 0。这 不是表达式的含义用 C 表示。
已编辑以包含对其工作方式/原因的更详细说明。【参考方案2】:
给你。
__m128i cmpgt_epu64_sse2( __m128i a, __m128i b )
// Compare uint32_t lanes for a > b and a < b
const __m128i signBits = _mm_set1_epi32( 0x80000000 );
a = _mm_xor_si128( a, signBits );
b = _mm_xor_si128( b, signBits );
__m128i gt = _mm_cmpgt_epi32( a, b );
__m128i lt = _mm_cmpgt_epi32( b, a );
// It's too long to explain why, but the result we're after is equal to ( gt > lt ) for uint64_t lanes of these vectors.
// Unlike the source numbers, lt and gt vectors contain a single bit of information per 32-bit lane.
// This way it's much easier to compare them with SSE2.
// Clear the highest bit to avoid overflows of _mm_sub_epi64.
// _mm_srli_epi32 by any number of bits in [ 1 .. 31 ] would work too, only slightly slower.
gt = _mm_andnot_si128( signBits, gt );
lt = _mm_andnot_si128( signBits, lt );
// Subtract 64-bit integers; we're after the sign bit of the result.
// ( gt > lt ) is equal to extractSignBit( lt - gt )
// The above is only true when ( lt - gt ) does not overflow, that's why we can't use it on the source numbers.
__m128i res = _mm_sub_epi64( lt, gt );
// Arithmetic shift to broadcast the sign bit into higher halves of uint64_t lanes
res = _mm_srai_epi32( res, 31 );
// Broadcast higher 32-bit lanes into the final result.
return _mm_shuffle_epi32( res, _MM_SHUFFLE( 3, 3, 1, 1 ) );
Here’s a test app.
如果 SSE3 可用,movshdup
也是一个不错的选择,而不是 pshufd
(_mm_shuffle_epi32) 将 srai 结果复制到每个元素中的低位 dword。 (或者如果下一次使用是 movmskpd
或其他仅取决于每个 qword 的高位部分的东西,则将其优化掉。
例如,在 Conroe/Merom 上(第一代 Core 2、SSSE3 和大多数 SIMD 执行单元都是 128 位宽,但随机播放单元有限制),pshufd
是 2 微秒,3 周期延迟(flt->int领域)。 movshdup
只有 1 uop,1 个周期延迟,因为它的硬连线 shuffle 仅在每个 64 位一半的寄存器中。 movshdup
在“SIMD-int”域中运行,因此它不会在整数移位和接下来执行的任何整数操作之间造成任何额外的绕过延迟,这与 pshufd
不同。 (https://agner.org/optimize/)
如果您是 JITing,则只能在没有 SSE4.2 的 CPU 上使用它,这意味着在 Nehalem 之前是 Intel,在 Bulldozer 之前是 AMD。请注意,在其中一些 CPU 上,psubq
(_mm_sub_epi64
) 比较窄的 psub
稍慢,但它仍然是最佳选择。
为了完整起见,这里是 SSSE3 版本(与 SSE3 不太一样),以恒定负载为代价节省了一些指令。确定它是快还是慢的唯一方法 - 在旧电脑上测试。
__m128i cmpgt_epu64_ssse3( __m128i a, __m128i b )
// Compare uint32_t lanes for a > b and a < b
const __m128i signBits = _mm_set1_epi32( 0x80000000 );
a = _mm_xor_si128( a, signBits );
b = _mm_xor_si128( b, signBits );
__m128i gt = _mm_cmpgt_epi32( a, b );
__m128i lt = _mm_cmpgt_epi32( b, a );
// Shuffle bytes making two pairs of equal uint32_t values to compare.
// Each uint32_t combines two bytes from lower and higher parts of the vectors.
const __m128i shuffleIndices = _mm_setr_epi8(
0, 4, -1, -1,
0, 4, -1, -1,
8, 12, -1, -1,
8, 12, -1, -1 );
gt = _mm_shuffle_epi8( gt, shuffleIndices );
lt = _mm_shuffle_epi8( lt, shuffleIndices );
// Make the result
return _mm_cmpgt_epi32( gt, lt );
【讨论】:
这只是How to simulate pcmpgtq on sse2? 的符号位翻转了吗?我猜不完全;你正在翻转每个 32 位块的符号位,而不是 64 位。(比较的 64 位有符号必须将每个 qword 的低半部分范围移动到无符号符号。)所以有与仅翻转高位并提供给带符号的比较相比,这是一些节省,编译器可能会或可能不会为我们优化。 psrai res,31 => pshufd 可能比 srli / psubq 更好。pshufd
是一个复制和洗牌,你不需要一个零来替代。 (OTOH,一些没有 SSE4.2 的旧 CPU 的随机播放单元很慢,如 Fastest way to do horizontal SSE vector sum (or other reduction) 中所述,这使得 pshufb 变慢。但一些旧 CPU 的 psubq、IIRC 也很慢;必须检查一下。)
@PeterCordes 好主意,已更新。 pshufd
是所有最新 CPU 中的 1 条延迟指令。在 Skylake 和 Ryzen 之前,32 位算术移位 psrad
不是,它有 2 个周期的延迟。但是,这样做仍然有意义,因为我写的关于psrad
的内容同样适用于psrlq
,并且psrad
保存了pxor
使用的pxor
指令_mm_setzero_si128
记住这个问题的真正目标是当 SSE4.2 不可用时的 JIT 回退,否则你会执行 2x pxor / pcmpgtq。鉴于 WebAssembly 是 JITed,因此不可能在 Sandybridge-family 或 Bulldozer / Zen 上使用它。 (除非其他人在没有动态调度的情况下使用它进行提前编译,或者有人错误地配置了 VM 以不将 SSE4 广告给来宾......)
另外,为了完整起见,这里是SSSE3版本,可能会快一些,不知道是不是这样。 gist.github.com/Const-me/5999328a743128a86f7f5a93d07f2463以上是关于在 SSE2 上进行无符号 64 位比较的最有效方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章