C/C++编译器会对这个if语句进行优化吗?

3

我有这样的代码,但我觉得它有点难以阅读:

// code1
if( (expensiveOperation1() && otherOperation() && foo()) 
     || (expensiveOperation2() && bar() && baz()) {
  // do something
}

我将其修改为以下内容,以使其更易于阅读:
// code2
const bool expr1 = expensiveOperation1() && otherOperation() && foo();
const bool expr2 = expensiveOperation2() && bar() && baz();
if(expr1 || expr2){
   // one of the conditions met
}

但是我现在应该关注效率吗?

我的意思是,在code1中,如果第一个联结从句已经被满足,那么它甚至不会去看第二个从句,因为已经清楚该语句为真。

但是在我更易读的例子中,必须计算cond1cond2。或者编译器是否足够聪明,如果expr2没有在其他地方使用,就将我的code2改成code1


我正在使用gcc/g++(准确地说是g++ v3.4)。 - Frank
7个回答

24

我认为它不应该,因为如果任何一个函数具有副作用,它们就不是逻辑上等价的。

但以下代码会等价,而且还能够为测试函数提供描述性名称,使代码更易于自我解释:

// code3
inline bool combinedOp1()
{
    return expensiveOperation1() && otherOperation() && foo();
}

inline bool combinedOp2()
{
    return expensiveOperation2() && bar() && baz();
}

然后按以下方式调用:

if (combinedOp1() || combinedOp2())
{
    // do something
}

由于惰性求值,最好不要有任何副作用。 - tvanfosson
同意,如果它优化掉了这个操作,我会感到惊讶。当然,不应该有任何副作用,但编译器并不知道这一点。它必须假设最坏的情况。如果你昂贵操作的定义是可见的,它可能会进行优化,但这不是一个安全的赌注。虽然如此,我还是喜欢这个解决方案。 - jalf
嗯,你的意思是说,在某些情况下,即使combinedOp1()已经为true,这也会执行combinedOp2()吗?有人能确认这绝对是这种情况吗?因为如果是这样的话,那么这不应该是被接受的答案。 - Frank
1
不,那不是他的意思。如果combinedOp1()为真,则保证永远不会调用combinedOp2()。 - John Carter

20
也许可以,但为什么不把第二个检查包含在第一个检查中呢?
// code3
bool expr = expensiveOperation1() && otherOperation() && foo();
expr = expr || (expensiveOperation2() && bar() && baz());
if(expr){
   // one of the conditions met
}
更好的方法是,将每个列表中最便宜的检查放在首位,利用惰性求值跳过昂贵的操作。

很好 - 这是那种简单的事情,可能在我一生中都不会想到。 - Michael Burr
不错,除了expr被声明为const,所以第二行无法编译 :-) 我猜你的意思是“const bool expr1 = ...; const bool expr2 = expr1 || (...)" - SCFrench
剪切/粘贴错误。我可能不会将其设置为const并重复使用它。 - tvanfosson
1
一个缺点是更难找到描述性的名称。在dehmann的版本中,expr1和expr2变量可以被命名以表达子条件。 - Tomas
1
@tvanfosson - dehmann提出“code2”的原因是为了使code1更易读。毫无疑问,通过为expr1和expr2选择清晰、独特的名称来实现这一点,这可能是您解决方案的缺点。它不必如此,所以我不会给你负评,只是想指出这一点。 - Tomas
显示剩余2条评论

4

通常情况下,编译器不会重新排列&&和||的顺序,以防条件具有副作用。一些非常聪明的编译器可能能够静态地验证它们的独立性,但这将是罕见的。

如果可能的话,请重新排列您的条件,使得廉价操作先执行,这样就可以避免执行昂贵的操作。


2
这里的顶级答案都回答了“不应该”和“可能”!这不是一个明确的答案,拜托!
如果你想知道你的编译器是否优化了这个小代码段,请使用“显示汇编输出”标志编译你的代码。在GCC上,该标志是“-S”。然后查看输出汇编,它将完全显示正在编译或未编译的内容。
然后,你可以将你的第一段代码与“therefromhere”的代码片段进行比较,并快速尝试众多代码更改,直到找到编译器最优化的代码(即最少的循环)。
听起来复杂且可怕,但实际上只需要大约5分钟的时间。我在这里做了一个例子:C中交换值的最快方法是什么?

-1 这是一种制造不可移植代码并在下一个编译器更新中出现错误的好方法。始终尝试理解语言所保证的内容,只有当行为似乎违反了这些保证时,您才应该开始分析编译器实现细节。 - Ben Voigt
1
我什么时候建议过“噢,当你查看编译器的输出汇编时……确保让你的C代码不可移植”?你一定很努力地把我的写作解释成“让你的代码不可移植”。只有傻瓜才会故意让他们的C代码不可移植。更大的傻瓜会在没有思考的情况下非故意地使他们的C代码不可移植。 - Trevor Boyd Smith

1

这个问题的答案当然取决于编译器。最确切的方法是查看编译器为该函数生成的汇编代码。大多数(或者说全部?)编译器都有一种方法可以做到这一点,例如 gcc 使用 -S 选项。如果出于某种奇怪的原因你的编译器没有这个选项,大多数调试器都可以显示函数的反汇编代码,或者还有其他工具可以做到这一点。


0

好的回答。

我只想补充一点,就是我不喜欢编译器在优化时过于激进,以至于重新排列我的代码。

我只希望编译器按照指示执行。

如果它能够比我更聪明,那么它也可以比自己更聪明。


0
编译器可以进行优化,如果它知道cond2中的函数(expensiveOperation2()、bar()和baz())是纯函数(即没有副作用)。如果它们是纯函数,确保编译器知道这一点的最简单方法是将它们变成内联函数。
虽然有可能编译器即使你不这样做也能够判断出来,但这种情况非常罕见,因为expensiveOperation2()可能会执行很多工作。
顺便说一下,如果这些函数是纯函数,你应该重新排列它们的顺序,以便在expensiveOperation2()之前运行bar()和baz()(在cond1的排序中也是如此)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接