布尔技巧的比较运算符

Comparison operators on booleans trick

在C++中,逻辑运算符&&||!都有,分别对应合取、析取、取反,分别.

但我注意到比较运算符==!=<><=>=可用于布尔值也是如此!鉴于 PQ 是布尔值:

P == Q 是双条件 ,

P != Q 是排他性析取 ,

P < Q 是逆向非蕴涵 ,

P > Q 是非蕴涵 ,

P <= Q是蕴涵,

P >= Q是逆蕴涵.

我的问题是:

  1. 程序会通过这个技巧执行得更好吗?

  2. 是否有任何使用此技巧的示例代码(任何语言)?

Will the performance increase by this trick?

在任何有好处的处理器上,当 PQ 是简单变量时,这将足够简单,您应该期望编译器已经使用它而不需要任何源代码重写。

但请记住,P < Q 通常比 !P && Q 有明显的 缺点 :它需要评估 Q,当如果 P 的计算结果为 true,则结果已知。这同样适用于所有其他关系运算符。

Is there any example code using this trick (in any language)?

不是把戏,而是因为它可以使代码更容易理解(不是任何特定语言):

if ((a == null) != (b == null))
  throw "a and b must either both be null, or both be non-null";

可以写成^。哪个更容易阅读,见仁见智。

实际上我认为它可能会使代码更快。这是第一个函数的代码:

bool biconditional(bool a, bool b)
{
    return (a && b) || (!a && !b);
}

bool biconditional_trick(bool a, bool b)
{
    return a == b;
}

以及生成的程序集:

biconditional(bool, bool):
        mov     eax, esi
        xor     eax, 1
        xor     eax, edi
        ret
biconditional_trick(bool, bool):
        cmp     dil, sil
        sete    al
        ret

我使用了来自 Compiler Explorer 的 gcc 5.3 和标志 -O3 -Wall -fno-verbose-asm -march=haswell

很明显,您可以减少 1 条指令。有趣的是 gcc 没有做这个优化。您可能想给他们发一封电子邮件并询问原因:https://gcc.gnu.org/lists.html

编辑:另一个答案提出了一个很好的观点:通过修剪不必要的部分可以更快地评估逻辑表达式。为了演示,我重写了代码以使用对 return bool 而不是 bool 参数的函数的调用:

bool fa();
bool fb();

bool biconditional_with_function()
{
    return (fa() && fb()) || (!fa() && !fb());
}

bool biconditional_with_function_trick()
{
    return fa() == fb();
}

这是程序集:

biconditional_with_function():
        sub     rsp, 8
        call    fa()
        test    al, al
        je      .L7
        call    fb()
        test    al, al
        jne     .L10
.L7:
        call    fa()
        mov     edx, eax
        xor     eax, eax
        test    dl, dl
        je      .L14
.L10:
        add     rsp, 8
        ret
.L14:
        call    fb()
        add     rsp, 8
        xor     eax, 1
        ret
biconditional_with_function_trick():
        push    rbx
        call    fa()
        mov     ebx, eax
        call    fb()
        cmp     bl, al
        pop     rbx
        sete    al
        ret

您可以看到,如果前半部分为真,则为 biconditional_with_function 生成的代码使用跳转跳过表达式的后半部分。有趣的是,当计算后半部分时,fa()fb() 被调用两次,因为编译器不知道它们是否总是 return 相同的结果。如果是这种情况,则应通过将评估结果保存在自己的变量中来重写代码:

bool biconditional_with_function_rewritten()
{
    bool a = fa();
    bool b = fb();
    return (a && b) || (!a && !b);
}

和程序集:

biconditional_with_function_rewritten():
        push    rbx
        call    fa()
        mov     ebx, eax
        call    fb()
        xor     eax, 1
        xor     eax, ebx
        pop     rbx
        ret

我们可以看到它们几乎相同,只剩下 1 个指令差异,使 "trick" 方法略有优势。

对于相反的非蕴涵,我们可以看到 GCC 在使用逻辑运算符时确实会避免计算第二个运算符,但在使用 < 运算符时不会:

bool fa();
bool fb();

bool converse_nonimplication()
{
    return !fa() && fb();
}

bool converse_nonimplication_trick()
{
    return fa() < fb();
}

程序集:

converse_nonimplication():
        sub     rsp, 8
        call    fa()
        test    al, al
        je      .L5
        xor     eax, eax
        add     rsp, 8
        ret
.L5:
        add     rsp, 8
        jmp     fb()
converse_nonimplication_trick():
        push    rbx
        call    fa()
        mov     ebx, eax
        call    fb()
        cmp     al, bl
        pop     rbx
        seta    al
        ret