可能每个人都使用某种优化开关(在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;
}
//编译器会跳过整个循环,因为它没有可见的副作用吗?
也许你可以想到其他的例子。
答案 0 :(得分:6)
如果您想知道编译器的功能,最好的办法是查看编译器文档。对于优化,您可以查看LLVM's Analysis and Transform Passes例如。
1)sin(3.141592)//它将在编译时进行评估吗?
可能。 IEEE浮点计算有非常精确的语义。如果你顺便在运行时更改处理器标志,这可能会令人惊讶。
2)int a = 0; a = exp(18),cos(1.57),2;
取决于:
exp
和cos
是否内嵌对于从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
计算已在编译时解决exp
和cos
已完全剥离。如果您想深入研究编译器优化,我建议您:
答案 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]。