在C ++中“如果错误然后快速失败”的性能损失?

时间:2011-03-15 10:57:23

标签: c++ performance if-statement

两种写作方式if-else之间是否有任何性能差异(在C ++中),如下所示(逻辑等效代码)likely1 == likely2 == true路径likely1likely2在这里作为一些更精细的条件的占位符)?

// Case (1):
if (likely1) {
  Foo();
  if (likely2) {
    Bar();
  } else {
    Alarm(2);
  }
} else {
  Alarm(1);
}

VS

// Case (2):
if (!likely1) {
  Alarm(1);
  return;
}
Foo();
if (!likely2) {
  Alarm(2);
  return;
}
Bar();

我非常感谢有关尽可能多的编译器和平台的信息(但突出显示了gcc / x86)。

请注意我对 对这两种风格的可读性意见感兴趣,对于任何“过早优化”声明都不感兴趣。

编辑:换句话说,我想问两种风格是否在某些时候被编译器认为完全完全100%等效/透明(例如逐位)在特定编译器的某个点上等效AST,如果没有,那么有什么区别?对于任何(对“现代”和gcc)编译器的偏好,你知道。

而且,为了更清楚,我也不认为它会给我带来很大的性能提升,而且它通常会过早优化,但我感兴趣

6 个答案:

答案 0 :(得分:6)

它在很大程度上取决于编译器和优化设置。如果差异至关重要 - 同时实施两者,并分析装配或执行基准测试。

答案 1 :(得分:4)

我对特定平台没有答案,但我可以提出一些一般观点:

  • 在没有分支预测的非现代处理器上的传统答案是,第一种可能更有效,因为在通常情况下它需要更少的分支。但您似乎对现代编译器和处理器感兴趣。

  • 在现代处理器上,一般来说,短前向分支并不昂贵,而错误预测的分支可能很昂贵。 “昂贵的”当然是指几个周期

  • 除此之外,编译器有权订购基本块,但是如果它不改变逻辑,它就会喜欢。因此,当您编写if (blah) {foo();} else {bar();}时,编译器有权发出如下代码:

  evaluate condition blah
  jump_if_true else_label
  bar()
  jump endif_label
else_label:
  foo()
endif_label:

总的来说,gcc倾向于按照你编写它们的顺序发出的东西,其他一切都是平等的。有许多事情会使其他所有不相等,例如,如果你在函数的两个不同位置具有bar(); return的逻辑等价物,gcc可能会合并这些块,只发出一次调用bar()然后返回,从两个不同的地方跳跃或跌倒。

  • 分支预测有两种 - 静态和动态。静态意味着分支的CPU指令指定条件是否“可能”,以便CPU可以针对常见情况进行优化。编译器可能在某些平台上发出静态分支预测,如果您正在针对该平台进行优化,那么可能编写代码以考虑到这一点。您可以通过了解编译器如何处理各种控制结构或使用编译器扩展来考虑它。就个人而言,我认为它不足以概括编译器将会做什么。看看反汇编。

  • 动态分支预测意味着在热代码中,CPU将自行统计分支的可能性,并针对常见情况进行优化。现代处理器使用各种不同的动态分支预测技术:http://en.wikipedia.org/wiki/Branch_predictor。性能关键代码几乎热代码,只要动态分支预测策略有效,它就可以非常快速地优化热代码。可能存在某些混淆特定策略的病态情况,但一般情况下,你可以说任何处于紧张循环中的任何偏向于采取/未采取的情况,将在大多数情况下正确预测

  • 有时分支是否正确预测无关紧要,因为在某些情况下某些CPU在指令管道中包含两种可能性,而它正在等待条件被评估,并放弃不必要的选择。现代CPU变得复杂。然而,更简单的CPU设计可以避免分支成本,例如ARM上的条件指令。

  • 无论如何,对其他功能的调用将会扰乱所有这些猜测。因此在您的示例中可能存在细微差别,这些差异可能取决于Foo,Bar和Alarm中的实际代码。不幸的是,不可能区分显着和微不足道的差异,或者考虑到这些功能的细节,而不会进入你不感兴趣的“过早优化”指控。

  • 微优化尚未编写的代码几乎总是为时过早。很难预测名为Foo和Bar的函数的性能。据推测,问题的目的是辨别是否存在应该为编码风格提供信息的常见问题。答案是,由于动态分支预测,没有。在热门代码中,你的条件排列方式几乎没有什么区别,它确实产生差异,差异不像“在if条件下采取/不采用分支更快”。

  • 如果这个问题仅适用于单个程序,并且该代码被证明是热门的,那么当然可以对其进行测试,无需进行概括。

答案 2 :(得分:1)

它依赖于编译器。查看__builtin_expect上的gcc文档。您的编译器可能有类似的东西。请注意,您确实应该关注过早优化。

答案 3 :(得分:1)

答案很大程度上取决于“可能”的类型。如果它是一个整型常量表达式,编译器可以对其进行优化,两种情况都是等价的。否则,它将在运行时进行评估,并且无法进行太多优化。

因此,案例2通常比案例1更有效。

作为我使用的实时嵌入式系统的输入,您的“案例2”通常是安全和/或性能至关重要的代码的标准。安全关键型嵌入式系统的样式指南通常允许使用此语法,因此函数可以在出错时快速退出。

通常,样式指南会对“case 2”语法不满意,但如果

,则允许在一个函数中允许多个返回的异常

1)该功能需要快速退出并处理错误,或

2)如果函数末尾的单个返回导致 less 可读代码,这通常是各种协议和数据解析器的情况。

答案 4 :(得分:1)

如果您关注性能,我假设您正在使用配置文件引导优化。

如果您使用的是配置文件引导优化,则您提出的两种变体完全相同。

无论如何,您所询问的内容的性能完全被代码示例中不明显的性能特征所掩盖,所以我们真的无法回答这个问题。你必须测试两者的表现。

答案 5 :(得分:-1)

虽然我和其他所有人在一起,因为优化分支是没有意义的,如果没有分析并且实际上已经找到了瓶颈......如果有的话,优化可能的情况是有意义的。

正如他们的名字所示,

可能1和可能2都有可能。因此排除两者都可能的组合也可能可能最快:

if(likely1 && likely2)
{
    ... // happens most of the time
}else
{
    if(likely1)
        ...
    if(likely2)
        ...
    else if(!likely1 && !likely2) // happens almost never
        ...
}

请注意,第二个else可能不是必需的,一个不错的编译器会发现,如果前一个if子句不是真的,那么即使你没有明确告诉它,也不可能是真的。