在一个始终为false的for循环中,布尔条件是否会被优化掉?

6

I have the following situation

bool user_set_flag;

getFlagFromUser(&user_set_flag);

while(1){

    if(user_set_flag){
        //do some computation and output

    }


    //do other computation
}

变量user_set_flag在代码中仅被设置一次,在程序的最开始,它是用户选择要使用程序做什么的关键。例如,如果用户选择了user_set_flag=false,那么编译器将按照这种方式编译代码,if(user_set_flag)语句将只会被检查一次,还是会一直被检查呢?我是否可以向编译器提供提示,例如将bool设置为const?
我之所以问这个问题,是因为我的应用程序对时间非常敏感,它能够尽可能快地处理帧。始终为false的分支应该能够在运行时确定吗?

9
我相信你可以找到其他需要优化的地方,而不仅限于一个 if 语句。 - Smashery
你的问题的答案取决于编译器。编译器不一定需要将其优化掉。但如果它是一个可以在编译时确定的常量,大多数编译器都会将其优化掉。 - Charles Salvia
2
编译器无法优化用户提供的值。那需要一台时间机器。 - MSalters
7个回答

14
首先,处理器具有称为分支预测的功能。在循环运行几次后,处理器将能够注意到您的if语句总是按一定方向执行。(它甚至可以注意到常规模式,如true false true false。)然后它将猜测执行该分支,只要它能够正确预测,if语句的额外成本就几乎被消除了。如果您认为用户更有可能选择true而不是false,您甚至可以告诉gcc编译器(gcc特定扩展)。
但是,您在其中一个评论中提到了一个'更加复杂的bool序列'。我认为处理器可能没有足够的内存来模式匹配所有这些跳转--当它回到第一个if语句时,它对该跳转的了解已经从其内存中移除。但我们可以在这里帮助它...
编译器有将循环和if语句转换为其认为更优化的形式的能力。例如,它可能会将您的代码转换为schnaader给出的形式,这被称为循环展开。您可以通过进行基于性能指导的优化(PGO)来帮助编译器了解热点位置。(注意:在gcc中,-funswitch-loops只在-O3时打开。)
您应该在指令级别上对代码进行分析(VTune是一个很好的工具),以查看if语句是否真正成为瓶颈。如果它们确实是,并且通过查看生成的汇编代码,您认为编译器尽管使用PGO仍然错误,那么您可以尝试自己提取if语句。也许模板化的代码会使它更方便。
template<bool B> void innerLoop() {
    for (int i=0; i<10000; i++) {
        if (B) {
            // some stuff..
        } else {
            // some other stuff..
        }
    }
}
if (user_set_flag) innerLoop<true>();
else innerLoop<false>();

2
关于分支预测的观点很好,人们经常忘记它,但它对这些事情有非常巨大的影响。 - Pavel Minaev
如果user_set_flag可以被确定为常量(局部于函数,没有传递给循环调用之前的任何句柄并且在循环内没有修改),我仍然期望编译器在这里进行优化,然后我期望编译器能够生成2个版本的循环,如果它可以显著提高速度...(即如果减少指令数量是值得的)。 - Matthieu M.
是的,我说过它可能会……但万一它没有,我们可以通过使其更明确来鼓励它。 - int3
这完全取决于一些东西另一些东西实际上是什么。它们必须几乎不存在才会有任何影响。 - Mike Dunlavey

6

我认为无法进一步优化这个代码。编译器很聪明,它知道在循环执行期间user_set_flag的值不会改变,并会生成最有效的机器码。

这有点像对编译器进行猜测。除非你真的真的非常清楚自己在做什么,否则最好坚持使用最简单的解决方案。

作为一次练习,请尝试使用if (true)if(user_set_flag)两种方式来测试(计时)执行时间。我的猜测是执行时间不会有任何区别。


5
另一种选择是:
if(user_set_flag){
    while(1){
      ComputationAndOutput();
      OtherComputation();
    }
} else {
    while(1){
      OtherComputation();
    }
}

但正如Smashery所说,这只是微小的优化,不会像其他你可以做的优化那样显著提高程序速度。


是的,我在考虑这样的事情 - 但我也会想知道是否调用函数所需的额外努力是否值得。 - Smashery
那么,在这种情况下,您可以复制代码而不是使用函数 - 但再次强调,所有这些只会使您的代码稍微快一点,并且并不真正值得。 - schnaader
例如,调用函数或评估“if”可能需要1-3个CPU周期,我相信您在循环中执行的其余代码将浪费数千个CPU周期,因此通过这些优化加快速度将少于1%。 - schnaader
1
@chnaader:如果你有一个非常复杂的布尔序列,只需评估一次并对运行时间造成重大影响,该怎么办? - ldog
那么优化可能会有用,但我怀疑这种情况并不常见(如果是的话,那么你的编码技术存在错误,这不是优化的问题,而是写好代码的问题)。 - schnaader

4

从技术上讲,编译器有可能优化这种情况。

例如:

#include <cstdio>

int main(int argc, char* [])
{
    while (true)
    {
        if (argc == 1) {
            puts("one");
        }
        puts("some more");
    }
}

主函数被编译为(G++ -O3):

    cmpl    $1, 8(%ebp)
    je  L9
    .p2align 4,,15
L2:
    movl    $LC1, (%esp)
    call    _puts
    jmp L2
L9:
    movl    $LC0, (%esp)
    call    _puts
    movl    $LC1, (%esp)
    call    _puts
    movl    $LC0, (%esp)
    call    _puts
    movl    $LC1, (%esp)
    call    _puts
    jmp L9

如您所见,条件仅被评估一次以确定要运行哪个循环。而且它已经展开了一些真实的分支 :)

我得出结论,没有理由担心这些微观优化,除非您确定编译器无法优化不断重复评估不变布尔值(例如,如果它是全局的,编译器如何知道它不会被函数调用修改)并且确实是瓶颈。


++ 我总是会给新手点赞。你说得对,没有必要担心这些。每当优化一个循环时,都要查看循环的内容。在这种情况下,"_puts"将消耗比循环开销多得多的时间,因此即使循环未经过优化,您也永远不会注意到。 - Mike Dunlavey

1

你说用户实际上有一个可以将此标志设置为truefalse的设置。这意味着它是可以在运行时更改的东西。也就是说,它不能被优化掉(通常情况下)。

一般来说,编译器只能在编译时知道并进行"优化"。也就是说:当你点击编辑器菜单中的"构建"选项时。如果它可以改变,那么它通常无法被优化掉。

然而,如果你对循环内部使用的一个汇编指令感到困扰,你可以很容易地(嗯,取决于你没有展示的部分)自己优化它。将if语句放在循环外部,这样它只会在调用函数时执行一次。


0

如果您在编译时知道标志的值,可以添加编译标志以不包括 if 语句,如下:

while(1){
   #ifdef user_set_flag
    {
        //do some computation and output

    }
   #endif


    //do other computation
}

0
如果你真的想要尽可能快的速度,那么你需要进行积极的性能调优。所以不要试图猜测编译器为优化程序所做的工作。
这是胆怯的表现。
相反,要主动出击。 这里向你展示如何做到。

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