【问题标题】:Comparison operators on booleans trick布尔技巧上的比较运算符
【发布时间】:2016-10-16 10:22:46
【问题描述】:

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

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

P == Q 是双条件,

P != Q是排他析取,

P < Q 是逆非蕴涵,

P > Q 是非暗示,

P <= Q 隐含,

P >= Q是逆蕴涵。

我的问题是:

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

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

【问题讨论】:

  • 演员阵容怎么了?为什么不P < QP > QP <= QP >= Q
  • 哦,我不知道 , = 也可以用于布尔值。谢谢。
  • "这个技巧会提高性能吗?"相比什么增加?
  • 速度。不是很明显吗?
  • 我的意思是与仅使用 &&||! 的代码相比?

标签: c++ boolean comparison-operators boolean-operations


【解决方案1】:

这个技巧会提高性能吗?

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

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

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

不是作为一种技巧,而是因为它可以说导致代码更容易理解(不是任何特定语言):

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

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

【讨论】:

  • 针对比较运算符相对于逻辑运算符的劣势,我在 ISO C++ 提案中发布了一个优化提案。
  • 祝你好运,但老实说,我不希望它发生太多事情:您建议在语言规则中添加一个特殊的例外,这可能会改变现有代码的行为,并且没有给出任何令人信服的用例。你觉得P < Q!P && Q 有什么好处?
【解决方案2】:

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

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

编辑:另一个答案说得很好:通过修剪不必要的部分可以更快地评估逻辑表达式。为了演示,我重写了代码以使用对返回 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() 总共被调用了两次,因为编译器不知道它们是否总是返回相同的结果。如果是这种情况,则应通过将评估结果保存在自己的变量中来重写代码:

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

【讨论】:

  • 嗯,确实。对于第一个示例,GCC 有一个旧的missed-optimization bug report,尽管现在生成的代码已经比当时好得多。这并不像“指令越少越快”那么简单,因此可能值得检查两个版本的实际指令成本,但除非它们完全一样快,否则其中一个或另一个会错过优化。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-02-22
  • 1970-01-01
  • 2017-05-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多