在 C 中是否有一种无分支技术来计算两个无符号整数之间的绝对差?例如,给定变量 a 和 b,我希望值 2 用于 a=3、b=5 或 b=3、a=5 的情况。理想情况下,我还希望能够使用 SSE 寄存器对计算进行矢量化。
8 回答
有几种方法可以做到,我只提一种:
SSE4
- 使用
PMINUD
和PMAXUD
分隔寄存器#1 中的较大值和寄存器#2 中的较小值。 - 减去它们。
MMX/SSE2
- 翻转两个值的符号位,因为下一条指令只接受有符号整数比较。
PCMPGTD
. 将此结果用作掩码。- 计算两者的
(a-b)
结果(b-a)
- 用于
POR ( PAND ( mask, a-b ), PANDN ( mask, b-a ) )
选择绝对差的正确值。
来自tommesani.com,解决此问题的一种方法是使用两次饱和无符号减法。
由于饱和减法永远不会低于 0,因此您计算: r1 = (ab).saturating r2 = (ba).saturating
如果 a 大于 b,则 r1 将包含答案,而 r2 将为 0,反之亦然 b>a。将两个部分结果进行或运算将产生所需的结果。
根据VTUNE 用户手册,PSUBUSB/PSUBUSW 可用于 128 位寄存器,因此您应该能够通过这种方式获得大量并行化。
max(i,j) - min(i,j)
(i>j)*(i-j) + (j>i)*(j-i)
您当然可以使用 SSE 寄存器,但编译器可能会为您执行此操作
上交所2:
似乎与 Phernost 的第二个函数的速度差不多。有时 GCC 将其安排得更快,有时会慢一些。
__m128i big = _mm_set_epi32( INT_MIN, INT_MIN, INT_MIN, INT_MIN );
a = _mm_add_epi32( a, big ); // re-center the variables: send 0 to INT_MIN,
b = _mm_add_epi32( b, big ); // INT_MAX to -1, etc.
__m128i diff = _mm_sub_epi32( a, b ); // get signed difference
__m128i mask = _mm_cmpgt_epi32( b, a ); // mask: need to negate difference?
mask = _mm_andnot_si128( big, mask ); // mask = 0x7ffff... if negating
diff = _mm_xor_si128( diff, mask ); // 1's complement except MSB
diff = _mm_sub_epi32( diff, mask ); // add 1 and restore MSB
SSSE3:
比以前稍微快一点。有很多变化取决于如何声明循环之外的东西。(例如,a
使b
volatile
事情变得更快!这似乎是对调度的随机影响。)但这始终是最快的一个周期左右。
__m128i big = _mm_set_epi32( INT_MIN, INT_MIN, INT_MIN, INT_MIN );
a = _mm_add_epi32( a, big ); // re-center the variables: send 0 to INT_MIN,
b = _mm_add_epi32( b, big ); // INT_MAX to -1, etc.
__m128i diff = _mm_sub_epi32( b, a ); // get reverse signed difference
__m128i mask = _mm_cmpgt_epi32( b, a ); // mask: need to negate difference?
mask = _mm_xor_si128( mask, big ); // mask cannot be 0 for PSIGND insn
diff = _mm_sign_epi32( diff, mask ); // negate diff if needed
SSE4(谢谢 rwong):
不能测试这个。
__m128i diff = _mm_sub_epi32( _mm_max_epu32( a, b ), _mm_min_epu32( a, b ) );
计算差值并返回绝对值
__m128i diff = _mm_sub_epi32(a, b);
__m128i mask = _mm_xor_si128(diff, a);
mask = _mm_xor_si128(mask, b);
mask = _mm_srai_epi32(mask, 31);
diff = _mm_xor_si128(diff, mask);
mask = _mm_srli_epi32(mask, 31);
diff = _mm_add_epi32(diff, mask);
与使用有符号比较操作相比,这需要更少的操作,并产生更少的寄存器压力。
与以前相同的寄存器压力、2 个更多的操作、更好的依赖链分支和合并、用于 uops 解码的指令配对以及单独的单元利用率。虽然这需要加载,但可能超出缓存。在这个之后我没有想法了。
__m128i mask, diff;
diff = _mm_set1_epi32(-1<<31); // dependency branch after
a = _mm_add_epi32(a, diff); // arithmetic sign flip
b = _mm_xor_si128(b, diff); // bitwise sign flip parallel with 'add' unit
diff = _mm_xor_si128(a, b); // reduce uops, instruction already decoded
mask = _mm_cmpgt_epi32(b, a); // parallel with xor
mask = _mm_and_si128(mask, diff); // dependency merge, branch after
a = _mm_xor_si128(a, mask); // if 2 'bit' units in CPU, parallel with next
b = _mm_xor_si128(b, mask); // reduce uops, instruction already decoded
diff = _mm_sub_epi32(a, b); // result
在 Core2Duo 上为每个版本计时 200 万次迭代后,差异是无法估量的。所以选择更容易理解的。
以下一项或多项可能会导致无分支代码,具体取决于机器和编译器,因为条件表达式都非常简单。
我还没有通过所有的 sse 答案,但可能以下一些在矢量代码中表示;当然,以下所有内容都是可矢量化的(如果您以无符号比较开头,或者通过首先切换 msb 来伪造它。)。我认为对这个问题有一些实用的标量答案会很有帮助。
unsigned udiff( unsigned a, unsigned b )
{
unsigned result = a-b; // ok if a<b;
if(a <b ) result = -result;
return result;
}
unsigned udiff( unsigned a, unsigned b )
{
unsigned n =(a<b)? (unsigned)-1 : 0u;
unsigned result = a-b;
return (result^n)-n; // 'result' if n = 0; '-result' if n = 0xFFFFFFFF
}
unsigned udiff( unsigned a, unsigned b )
{
unsigned axb = a^b;
if( a < b ) axb = 0;
return (axb^b) - (axb^a); // a-b, or b-a
}
这将适用于 x86_64(或任何 64 位临时工基本上是免费的)
unsigned udiff( unsigned a, unsigned b )
{
unsigned n= (unsigned)(
(long long)((unsigned long long)a - (unsigned long long)b)>>32
); // same n as 2nd example
unsigned result = a-b;
return (result^n)-n; // 'result' if n = 0; '-result' if n = 0xFFFFFFFF
}
试试这个(假设第 2 个补码,根据您要求 SSE 的事实来判断,这是可以的):
int d = a-b;
int ad = ((d >> 30) | 1) * d;
解释:符号位(第 31 位)向下传播到第 1 位。的| 1 部分确保乘数为 1 或 -1。现代 CPU 上的乘法运算速度很快。
嗯……很简单……
int diff = abs( a - b );
易于矢量化(使用 SSE3 作为):
__m128i sseDiff = _mm_abs_epi32( _mm_sub_epi32( a, b ) );
a 和 b 是无符号整数。考虑 a=0 和 b=0xffffffff。正确的绝对差是 0xffffffff,但您的解决方案将给出 1。