与不使用if的测试相比,if语句的效率如何?(C++)

21 c c++ if-statement micro-optimization

我需要一个程序来获取两个数字中较小的一个,我想知道是否使用标准"如果x小于y"

int a, b, low;
if (a < b) low = a;
else low = b;
Run Code Online (Sandbox Code Playgroud)

或多或少效率高于此:

int a, b, low;
low = b + ((a - b) & ((a - b) >> 31));
Run Code Online (Sandbox Code Playgroud)

(或者放在int delta = a - b顶部并随之重新放置实例的变化a - b).

我只是想知道哪一个更有效(或者如果差异太小而不相关),以及if-else语句与一般的替代方案的效率.

dan*_*ben 26

(免责声明:以下内容涉及非常低级别的优化,这些优化通常不是必需的.如果您继续阅读,您放弃了投诉计算机速度快的权利,并且没有任何理由担心这类事情.)

消除if语句的一个优点是可以避免分支预测惩罚.

当分支不容易预测时,分支预测罚分通常只是一个问题.当几乎总是采用/不采用分支时,容易预测分支,或者它遵循简单的模式.例如,循环语句中的分支每次都被取出,除了最后一个,因此很容易预测.但是,如果你有像这样的代码

a = random() % 10
if (a < 5)
  print "Less"
else
  print "Greater"
Run Code Online (Sandbox Code Playgroud)

然后,这个分支不容易预测,并且经常会产生与清除缓存和回滚在分支的错误部分中执行的指令相关的预测损失.

避免这种惩罚的一种方法是使用ternary(?:)运算符.在简单的情况下,编译器将生成条件移动指令而不是分支.

所以

int a, b, low;
if (a < b) low = a;
else low = b;
Run Code Online (Sandbox Code Playgroud)

int a, b, low;
low = (a < b) ? a : b
Run Code Online (Sandbox Code Playgroud)

在第二种情况下,不需要分支指令.此外,它比您的比特笨重的实现更清晰,更易读.

当然,这是一种微优化,不太可能对您的代码产生重大影响.

  • 最后,一个对过早优化没有贬低的答案.谢谢. (22认同)
  • 在`-O1`和更高版本中,gcc为if语句和min()函数的三元运算符生成相同的代码,在两种情况下都使用cmovg指令.在`-O0`中,它为if语句使用分支和标签,为三元运算符使用cmovle. (13认同)
  • @Justicle - 对于过早优化没有喋喋不休的问题是你最终会得到一个隐含的建议(特别是那些刚刚学习的人)应该编写像'low = b +((a - b)&((a)的代码 - b)>> 31))`无处不在,因为有人说"它更快".事实上,绝大多数时候这样做是错误的. (11认同)

Bil*_*ard 9

简单的答案:一个条件跳转比两个减法,一个加法,一个按位和一个移位操作组合起来更有效. 我已经在这一点上受到了充分的教育(见评论),我甚至不再自信地说它通常更有效率.

务实的答案:无论哪种方式,你都没有为程序员弄清楚第二个例子正在做什么所花费的额外CPU周期.程序的可读性首先,效率第二.

  • 对于某些处理器,您的简单答案是错误的 (2认同)
  • @Bill:许多处理器都有一个长指令管道,只要有错误预测的分支就必须刷新,大概需要10或20个周期.在这种情况下,分支可能会被错误预测一半的时间,因此条件版本可能需要平均5或10个周期,而波浪形版本需要4或5.(当然,其他处理器有条件指令,短管道以及其他避免错误预测的方法,然后条件版本会更快). (2认同)
  • 在许多游戏控制台中使用的有序PowerPC处理器中,不可预测的分支是20个周期的气泡,而*正确*预测的分支是5个循环的气泡.由于双调度,x +((y-x)&(a >> 31))是3个周期.浮点数的情况甚至更为极端,其中条件移动的吞吐量为1/1周期,而浮点比较上的分支可以是*40*循环气泡. (2认同)

Tha*_*tos 8

在gcc 4.3.4,amd64(core 2 duo)上编译,Linux:

int foo1(int a, int b)
{
    int low;
    if (a < b) low = a;
    else low = b;
    return low;
}

int foo2(int a, int b)
{
    int low;
    low = b + ((a - b) & ((a - b) >> 31));
    return low;
}
Run Code Online (Sandbox Code Playgroud)

我明白了:

foo1:
    cmpl    %edi, %esi
    cmovle  %esi, %edi
    movl    %edi, %eax
    ret

foo2:
    subl    %esi, %edi
    movl    %edi, %eax
    sarl    $31,  %eax
    andl    %edi, %eax
    addl    %esi, %eax
    ret
Run Code Online (Sandbox Code Playgroud)

...我非常肯定不会计算分支预测,因为代码不会跳转.此外,非if语句版本更长2个指令.我想我会继续编码,让编译器做它的工作.


Blu*_*eft 7

最大的问题是你的第二个例子不适用于64位机器.

然而,即使忽略了这一点,现代编译器也足够聪明,可以在每种情况下考虑无分支预测,并比较估计的速度.所以,你的第二个例子很可能实际上更慢

if语句和使用三元运算符之间没有区别,因为即使是大多数愚蠢的编译器都足够聪明,可以识别这种特殊情况.


[编辑]因为我认为这是一个非常有趣的话题,所以我写了一篇博文.

  • 是的,但是当它决定时,编译器是错误的.我已经计划了两条路.我的工作包括将更多工作塞进16.6毫秒,而不是竞争产品.一般来说,我看过编译器会发出许多次优的代码序列.它们并不完美. (6认同)
  • 我有时会这样做,但通常更容易满足编译器中途并以这样的方式编写代码,从而产生我想要的代码序列; 特别是内在函数就是一个例子.与内联汇编相比,它更容易与其他C++代码混合.这是嵌入式世界的常见做法; 部分工作是学习编译器将为特定输入发出的内容. (5认同)
  • 我已经看过MSVC和GCC的汇编输出了,它们似乎都不够聪明,无法在我想要的时间内发出无分支条件移动. (3认同)
  • 在实践中我写了一个`isel(a,b,c)`函数,它与`return a> = 0具有相同的效果?b:c`.我们只是用它.(它被命名为`fsel`内在函数,这是硬件的本机浮点条件移动.)如果编译器足够聪明,可以为`?:`发出正确的代码,那会更好.但是我们没有没有智能编译器,只有GCC. (3认同)

Cub*_*bbi 7

与任何低级优化一样,在目标CPU /板设置上进行测试.

在我的编译器(x86_64上的gcc 4.5.1)上,第一个例子变为

cmpl    %ebx, %eax
cmovle  %eax, %esi
Run Code Online (Sandbox Code Playgroud)

第二个例子变成了

subl    %eax, %ebx
movl    %ebx, %edx
sarl    $31, %edx
andl    %ebx, %edx
leal    (%rdx,%rax), %esi
Run Code Online (Sandbox Code Playgroud)

不确定第一个在所有情况下是否更快,但我敢打赌它是.