是否有任何聪明的位技巧可以检测是否有少数整数(例如3或4)具有特定值?
直截了当的
bool test(int a, int b, int c, int d)
{
// The compiler will pretty likely optimize it to (a == d | b == d | c == d)
return (a == d || b == d || c == d);
}
test(int, int, int, int):
cmp ecx, esi
sete al
cmp ecx, edx
sete dl
or eax, edx
cmp edi, ecx
sete dl
or eax, edx
ret
这些sete
指令的延迟时间比我想要的要高,所以我宁愿按位使用(&
,|
,^
,~
)东西和单一比较。
答案 0 :(得分:4)
我发现的唯一解决方案是:
int s1 = ((a-d) >> 31) | ((d-a) >> 31);
int s2 = ((b-d) >> 31) | ((d-b) >> 31);
int s3 = ((c-d) >> 31) | ((d-c) >> 31);
int s = s1 & s2 & s3;
return (s & 1) == 0;
替代变体:
int s1 = (a-d) | (d-a);
int s2 = (b-d) | (d-b);
int s3 = (c-d) | (d-c);
int s = (s1 & s2 & s3);
return (s & 0x80000000) == 0;
两者都被翻译成:
mov eax, ecx
sub eax, edi
sub edi, ecx
or edi, eax
mov eax, ecx
sub eax, esi
sub esi, ecx
or esi, eax
and esi, edi
mov eax, edx
sub eax, ecx
sub ecx, edx
or ecx, eax
test esi, ecx
setns al
ret
有较少的sete指令,但显然更多的是mov / sub。
更新:正如BeeOnRope @建议的那样 - 将输入变量转换为无符号
是有意义的答案 1 :(得分:2)
这不是一个完整的技巧。任何零都会产生零乘积,从而得到零结果。否定0产生1.不处理溢出。
bool test(int a, int b, int c, int d)
{
return !((a^d)*(b^d)*(c^d));
}
gcc 7.1 -O3
输出。 (d
在ecx
中,其他输入在其他整数注册中开始。)
xor edi, ecx
xor esi, ecx
xor edx, ecx
imul edi, esi
imul edx, edi
test edx, edx
sete al
ret
它可能比Core2或Nehalem上的原始版本更快,其中partial-register stalls是个问题。 imul r32,r32
在Core2 / Nehalem(以及后来的Intel CPU)上有3c延迟,每个时钟吞吐量为1,因此该序列从输入到第二imul
结果有7个周期延迟,另外2个周期test
/ sete
的延迟。如果此序列在多个独立输入上运行,则吞吐量应该相当不错。
使用64位乘法可避免第一次乘法时出现溢出问题,但如果总数为>= 2**64
,则第二次乘法仍会溢出。它仍将是英特尔Nehalem和Sandybridge家族以及AMD Ryzen的相同表现。但是在较旧的CPU上它会更慢。
在x86 asm中,使用全乘法单操作数mul
指令(64x64b => 128b)进行第二次乘法将避免溢出,并且可以检查结果是否为全零或不是or rax,rdx
。我们可以在GNU C中为64位目标(__int128
可用)编写
bool test_mulwide(unsigned a, unsigned b, unsigned c, unsigned d)
{
unsigned __int128 mul1 = (a^d)*(unsigned long long)(b^d);
return !(mul1*(c^d));
}
和gcc / clang确实发出了我们希望的asm(每个都有一些无用的mov
指令):
# gcc -O3 for x86-64 SysV ABI
mov eax, esi
xor edi, ecx
xor eax, ecx
xor ecx, edx # zero-extends
imul rax, rdi
mul rcx # 64 bit inputs (rax implicit), 128b output in rdx:rax
mov rsi, rax # this is useless
or rsi, rdx
sete al
ret
这应该与现代x86-64上可以溢出的简单版本一样快。 (mul r64
仍然只有3c延迟,但在英特尔Sandybridge系列上只有2 uops而不是imul r64,r64
的1,而且不会产生高半部分。)
它仍然可能比原始版本的clang setcc
/ or
输出更糟糕,原始版本使用8位or
指令来避免读取32-写低字节后的位寄存器(即没有部分寄存器停止)。
使用两个编译器on the Godbolt compiler explorer查看两个来源。 (还包括:@BeeOnRope's ^
/ &
version that risks false positives,无论是否支持全面检查。)