检测某些整数是否具有特定值的位技巧

时间:2017-07-24 21:06:39

标签: c++ performance optimization x86 bit-manipulation

是否有任何聪明的位技巧可以检测是否有少数整数(例如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);
}

in GCC compiles to

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指令的延迟时间比我想要的要高,所以我宁愿按位使用(&|^~ )东西和单一比较。

2 个答案:

答案 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输出。 (decx中,其他输入在其他整数注册中开始。)

    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,无论是否支持全面检查。)