优化开关 - 他们真正做了什么?

时间:2012-09-07 11:05:50

标签: c++ visual-c++ gcc clang

可能每个人都使用某种优化开关(在gcc的情况下,最常见的是 -O2 我相信)。

但是gcc(和其他编译器,如VS,Clang)真的在这些选项的存在下做了什么?

当然没有明确的答案,因为它很大程度上取决于平台,编译器版本等。 但是,如果可能的话,我想收集一套“经验法则”。 我什么时候应该考虑加速代码的一些技巧?什么时候应该把工作留给编译器?

例如,编译器会走多远(有点艺术......) 案例,针对不同的优化级别:

1)sin(3.141592) //它会在编译时进行评估,还是应该考虑一个查找表来加速计算?

2)int a = 0; a = exp(18), cos(1.57), 2; //编译器是否会评估exp和cos,尽管不需要,因为表达式的值等于2?

3)

for (size_t i = 0; i < 10; ++i) {
  int a = 10 + i;
}

//编译器会跳过整个循环,因为它没有可见的副作用吗?

也许你可以想到其他的例子。

3 个答案:

答案 0 :(得分:6)

如果您想知道编译器的功能,最好的办法是查看编译器文档。对于优化,您可以查看LLVM's Analysis and Transform Passes例如。

  

1)sin(3.141592)//它将在编译时进行评估吗?

可能。 IEEE浮点计算有非常精确的语义。如果你顺便在运行时更改处理器标志,这可能会令人惊讶。

  

2)int a = 0; a = exp(18),cos(1.57),2;

取决于:

  • 函数expcos是否内嵌
  • 如果不是,是否正确注释(因此编译器知道它们没有副作用)

对于从C或C ++标准库中获取的函数,应正确识别/注释它们。

至于计算的消除:

  • -adce:积极的死代码消除
  • -dce:死码消除
  • -die:消除死亡指令
  • -dse:Dead Store Elimination

编译器喜欢找到无用的代码:)

  

3)

实际上与2)相似。不使用商店的结果,表达没有副作用。

  • -loop-deletion:删除死循环

对于决赛:什么不能让编译器进行测试?

#include <math.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
  double d = sin(3.141592);
  printf("%f", d);

  int a = 0; a = (exp(18), cos(1.57), 2); /* need parentheses here */
  printf("%d", a);

  for (size_t i = 0; i < 10; ++i) {
    int a = 10 + i;
  }

  return 0;
}

Clang在编译期间尝试提供帮助:

12814_0.c:8:28: warning: expression result unused [-Wunused-value]
  int a = 0; a = (exp(18), cos(1.57), 2);
                           ^~~ ~~~~
12814_0.c:12:9: warning: unused variable 'a' [-Wunused-variable]
    int a = 10 + i;
        ^

发出的代码(LLVM IR):

@.str = private unnamed_addr constant [3 x i8] c"%f\00", align 1
@.str1 = private unnamed_addr constant [3 x i8] c"%d\00", align 1

define i32 @main(i32 %argc, i8** nocapture %argv) nounwind uwtable {
  %1 = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([3 x i8]* @.str, i64 0, i64 0), double 0x3EA5EE4B2791A46F) nounwind
  %2 = tail call i32 (i8*, ...)* @printf(i8* getelementptr inbounds ([3 x i8]* @.str1, i64 0, i64 0), i32 2) nounwind
  ret i32 0
}

我们注意到:

  • 正如预测sin计算已在编译时解决
  • 正如预测的那样expcos已完全剥离。
  • 正如预测的那样,循环也被剥离了。

如果您想深入研究编译器优化,我建议您:

  • 学会阅读IR(这非常简单,非常容易组装)
  • 使用LLVM试用页面测试您的假设

答案 1 :(得分:1)

编译器有许多优化过程。每个优化过程都负责许多小优化。例如,您可能有一个在编译时计算算术表达式的传递(例如,您可以将5MB表示为5 *(1024 * 1024)而不会受到惩罚)。另一个传递函数。另一个搜索无法访问的代码并将其杀死。等等。

然后,编译器的开发人员决定他们希望以哪种顺序执行这些传递。例如,假设您有以下代码:

int foo(int a, int b) {
  return a + b;
}

void bar() {
  if (foo(1, 2) > 5)
    std::cout << "foo is large\n";
}

如果您在此处运行死代码消除,则不会发生任何事情。同样,如果你运行表达式减少,没有任何反应。但是内联可能会认为foo足够小以便内联,所以它用函数体替换了bar中的调用,替换了参数:

void bar() {
  if (1 + 2 > 5)
    std::cout << "foo is large\n";
}

如果你现在运行表达式 ,它将首先确定1 + 2是3,然后决定3&gt; 5是假的。所以你得到:

void bar() {
  if (false)
    std::cout << "foo is large\n";
}

现在 死代码消除会看到if(false)并将其删除,结果是:

void bar() {
}

但现在吧突然非常微小,之前它更大更复杂。因此,如果再次运行内联器,它将能够内联到其调用者。这可能会暴露更多的优化机会,等等。

对于编译器开发人员来说,这是编译时和生成的代码质量之间的权衡。他们根据启发式测试,测试和经验决定运行一系列优化器。但由于一种尺寸并不适合所有尺寸,因此它们会暴露一些旋钮来调整它。 gcc和clang的主要旋钮是-O选项系列。 -O1运行一个简短的优化器列表; -O3运行一个包含更昂贵的优化器的更长的列表,并且更频繁地重复传递。

除了决定运行哪些优化器之外,选项还可以调整各种传递所使用的内部启发式算法。例如,内联器通常有很多参数来决定何时值得内联函数。传递-O3,只要有可能提高性能,这些参数将更倾向于内联函数; pass -Os,这些参数只会导致非常小的函数(或者可以证明只有一次调用的函数)被内联,因为其他任何东西都会增加可执行文件的大小。

答案 2 :(得分:0)

编译器会执行您无法想到的所有优化。特别是C ++编译器。

他们做的事情包括展开循环,使函数内联,消除死代码,用一个替换多个指令等等。

我可以给出的建议是:在C / C ++编译器中,您可以相信他们将执行很多优化。

看看[1]。

[1] http://en.wikipedia.org/wiki/Compiler_optimization