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循环的每次传递检查此变量。
您对此事有何看法?是否有更有效的方法来编写上述代码段以避免此问题?
答案 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
养成始终从-Wall
gcc
询问所有警告的习惯(如果编译C ++代码,则g++
。
foo
的几次调用已内联...)
答案 6 :(得分:0)
实际上所有现代编译器都进行优化,如果您认为编译器不应该进行此优化,则应遵循此优化,您应该将变量设置为“volatile”。