c ++:编译器能否优化此代码段?

时间:2013-09-08 07:09:07

标签: c++ performance optimization compiler-construction compiler-optimization

void foo(const int constant)
{
    for(int i = 0; i < 1000000; i++) {
        // do stuff
        if(constant < 10) {              // Condition is tested million times :(
            // inner loop stuff
        }
    }
}

对于外循环的每次执行,都会检查“常量”的值。但是,常量永远不会改变,因此浪费了大量的CPU时间来测试条件常数&lt; 10?一遍又一遍地。人类会在前几次传球后意识到常数永远不变,并且智能地避免一遍又一遍地检查它。编译器是否注意到这一点并对其进行智能优化,或者重复if循环是否不可避免?

就个人而言,我认为这个问题是不可避免的。即使编译器在外部循环之前进行比较并设置某种布尔变量“skip_inner_stuff”,仍然必须为外部for循环的每次传递检查此变量。

您对此事有何看法?是否有更有效的方法来编写上述代码段以避免此问题?

7 个答案:

答案 0 :(得分:6)

您描述的优化也称为loop unswitching。多年来它一直是优化编译器的标准部分 - 但是如果你想确保你的编译器执行它,请编译一些优化级别的示例代码(例如gcc中的-O2)并检查生成的代码。

然而,在编译器无法证明一段代码在整个循环中是不变的情况下 - 例如对编译时无法使用的外部函数的调用 - 实际上,手动将代码提升到循环外部可以提高性能。

答案 1 :(得分:3)

编译器可以优化代码,但你不能指望它会对你的代码产生魔术。

优化很大程度上取决于您的代码和代码的使用情况。例如,如果您使用foo,请执行以下操作:

foo(12345);

编译器可以非常优化代码。即使它可以在编译时计算结果。

但如果你这样使用它:

int k;
cin >> k;
foo(k);

在这种情况下,它无法摆脱内部if(该值在运行时提供)。

我用MinGW / GCC-4.8.0写了一个示例代码:

void foo(const int constant)
{
    int x = 0;
    for (int i = 0; i < 1000000; i++)
    {
        x++;
        if (constant < 10)
        {
            x--;
        }
    }
    cout << x << endl;
}

int main()
{
    int k;
    cin >> k;
    foo(k);
}

让我们看一下生成汇编代码:

004015E1  MOV EAX,0F4240                 // i = 1000000
004015E6  MOV EBP,ESP
004015E8  XOR EDX,EDX
004015EA  PUSH ESI
004015EB  PUSH EBX
004015EC  SUB ESP,10
004015EF  MOV EBX,DWORD PTR SS:[EBP+8]
004015F2  XOR ECX,ECX                    // set ECX to 0
004015F4  CMP EBX,0A                     // if constant < 10
          ^^^^^^^^^^
004015F7  SETGE CL                       // then set ECX to 1
004015FA  ADD EDX,ECX                    // add ECX to i
004015FC  SUB EAX,1                      // i--
004015FF  JNE SHORT 004015F2             // loop if i is not zero

正如您所见,代码中存在内部if。请参阅CMP EBX,0A

我再次重复,这在很大程度上取决于带有循环的线条。

答案 2 :(得分:2)

其他人已经介绍了相关的编译器优化:循环非开关,它将测试移出循环并提供两个独立的循环体;和代码内联在某些情况下会为编译器提供constant的实际值,以便它可以删除测试,并无条件地执行'inner loop stuff'或完全删除它。

另外请注意,除了编译器所做的任何事情之外,现代CPU设计实际上做的事情类似于“人类在最初的几次传递之后才会意识到,常数永远不会改变”。它被称为动态分支预测

关键是检查一个整数是非常便宜的,甚至采取分支可以非常便宜。什么是潜在的昂贵是错误预测的分支。现代CPU使用各种策略来猜测分支将采用哪种方式,但所有这些策略都将很快开始正确地预测连续一百万次的分支。

我不知道的是,现代CPU是否足够聪明,发现constant是一个循环不变量并且在微码中执行完全循环非切换。但假设正确的分支预测,无论如何,循环非开关可能是一个小的优化。编译器所针对的处理器系列越具体,它就越了解其分支预测器的质量,编译器就越有可能确定循环非开关的额外好处是否值得代码膨胀。

当然还有最小的CPU,编译器必须提供所有的聪明才智。 PC中的CPU不是其中之一。

答案 3 :(得分:1)

您可以手动优化它:

void foo(const int constant)
{
    if (constant < 10) {
        for(int i = 0; i < 1000000; i++) {
            // do stuff

           // inner loop stuff here
        }
    } else {
        for(int i = 0; i < 1000000; i++) {
            // do stuff

            // NO inner loop stuff here
        }
    }
}

我不知道大多数编译器是否会做这样的事情,但看起来并不是太长了。

答案 4 :(得分:1)

一个好的编译器可以优化它。

编译器根据成本分析进行优化。因此,一个好的编译器应该估计每个备选方案的成本(有和没有提升),并选择哪个更便宜。

这意味着如果内部部分中的代码很大,则可能不值得优化,因为这可能导致指令缓存废弃。另一方面,如果价格便宜,则可以悬挂。

如果它在探查器中显示,因为它尚未优化,编译器搞砸了。

答案 5 :(得分:0)

一个好的编译器会优化它(当启用优化时)。

如果使用GCC,您可以

  • 使用

    生成优化和汇编代码进行编译
    gcc -Wall -O2 -fverbose-asm -S source.c
    

    然后查看(使用某个编辑器或类似less的寻呼机)到生成的汇编代码source.s

  • 要求GCC转储很多(数百个!)中间文件并查看其中的中间gimple表示

    gcc -Wall -O2 -fdump-tree-all -c source.c
    
  • 使用MELT及其probe在gimple中以交互方式查看。

养成始终从-Wall gcc询问所有警告的习惯(如果编译C ++代码,则g++

实际上,这种优化(“循环不变代码提升”,正如另一个答案所解释的那样)是必不可少的,因为这种中间代码经常发生,例如:在函数内联之后....(想象一下对foo的几次调用已内联...)

答案 6 :(得分:0)

实际上所有现代编译器都进行优化,如果您认为编译器不应该进行此优化,则应遵循此优化,您应该将变量设置为“volatile”。