8

对于编译器如何实际优化以及不同级别之间的区别(例如 gcc 的 -O2 与 -O3 ),我没有很多经验。因此,我不确定以下两个语句对于任意编译器是否等效:

for(i=0;i<10;++i){
variable1*variable2*gridpoint[i];
}

variable3=variable1*variable2;
for(i=0;i<10;++i){
variable3*gridpoint[i];
}

从处理时间的角度来看,只计算一次 variable1 和 variable2 的乘积是有意义的,因为它们在循环中不会改变。然而,这需要额外的内存,而且我不确定优化器对这种开销的影响有多大。如果你有一个来自纸/书的方程并且想要将它翻译成计算机可读的东西,第一个表达式是最容易阅读的,但第二个可能是最快的 - 特别是对于循环内有很多不变变量的更复杂的方程(我有一些非常讨厌的非线性微分方程,我希望在代码中可读)。如果我将变量声明为常量,这会发生任何变化吗?我希望我的问题对任意编译器有意义,因为我同时使用 gcc、Intel 和 Portland 编译器。

4

3 回答 3

4

对于任意编译器来说,很难充分回答这个问题。这段代码能做什么不仅取决于编译器,还取决于目标架构。我将尝试解释具有良好功能的生产编译器可以对这段代码做什么。

从处理时间的角度来看,只计算一次 variable1 和 variable2 的乘积是有意义的,因为它们在循环中不会改变。

你说的对。正如猫先生所指出的,这称为公共子表达式消除。因此,编译器可能会生成只计算一次表达式的代码(或者如果两个操作数的值一次都是常量,则甚至在编译时计算它)。

如果一个体面的编译器可以确定函数没有副作用,它也可以对函数执行子表达式消除。例如,如果函数体可用,GCC 可以分析函数,但也有pureconst属性可用于专门标记应接受此优化的函数(请参阅函数属性)。

鉴于没有副作用并且编译器能够确定它(在您的示例中,没有任何障碍),因此其中两个片段在这方面是等效的(我已经使用 clang 进行了检查 :-))。

然而,这需要额外的内存,我不确定优化器在多大程度上考虑了这种开销。

事实上,这不需要任何额外的内存。乘法在处理器寄存器中完成,结果也存储在寄存器中。这是一个消除大量代码并使用单个寄存器来存储结果的问题,这总是很棒(而且在寄存器分配方面肯定会让生活更轻松,尤其是在循环中)。因此,如果可以完成此优化,那么它将无需额外费用即可完成。

第一个表达式是最容易阅读的。

GCC 和 Clang 都将执行此优化。不过,我不确定其他编译器,所以你必须自己检查。但是很难想象任何没有进行子表达式消除的好的编译器。

如果我将变量声明为常量,这会发生任何变化吗?

它可能。这称为常量表达式——仅包含常量的表达式。可以在编译期间而不是在运行时评估常量表达式。因此,例如,如果您使用多个 A、B 和 C,其中 A 和 B 都是常量,编译器将A*B只针对该预先计算的值预先计算多个 C 表达式。如果编译器可以在编译时确定它的值并确保它没有被更改,那么编译器也可以使用非常量值来执行此操作。例如:

$ cat test.c
inline int foo(int a, int b)
{
    return a * b;
}

int main() {
    int a;
    int b;
    a = 1;
    b = 2;
    return foo(a, b);
}
$ clang -Wall -pedantic -O4 -o test ./test.c
$ otool -tv ./test
./test:
(__TEXT,__text) section
_main:
0000000100000f70    movl    $0x00000002,%eax
0000000100000f75    ret

在上述代码片段的情况下,还可以进行其他优化。以下是其中一些想到的:

第一个最明显的是循环展开。由于迭代次数在运行时是已知的,编译器可能会决定展开循环。是否应用此优化取决于架构(即,某些 CPU 可以“锁定循环”并比其展开版本更快地执行代码,这也通过使用更少的空间使代码对缓存更友好,避免额外的 µOP 融合阶段, ETC)。

第二个可以将速度提高 50 倍的优化是使用SIMD指令(SSE、AVX 等)。例如,GCC 非常擅长(如果不是更好的话,英特尔也必须如此)。我已经验证了以下功能:

uint8_t dumb_checksum(const uint8_t *p, size_t size)
{
    uint8_t s = 0;
    size_t i;
    for (i = 0; i < size; ++i)
        s = (uint8_t)(s + p[i]);
    return s;
}

... 被转换为一个循环,其中每个步骤一次对 16 个值求和(即在 中_mm_add_epi8),并带有额外的代码处理对齐和奇数 (<16) 迭代计数。然而,在我上次检查时,Clang 完全失败了。因此,即使不知道迭代次数,GCC 也可能会以这种方式减少您的循环。

如果可以的话,我想建议你不要优化你的代码,除非你发现它是一个瓶颈。否则,您可能会浪费大量时间进行错误和过早的优化。

我希望这回答了你的问题。祝你好运!

于 2013-01-09T17:47:23.400 回答
3

是的,您可以指望编译器在执行子表达式消除方面做得很好,即使是通过循环。这可能会导致内存使用量略有增加,但是任何体面的编译器都会考虑所有这些,而且执行子表达式消除几乎总是一种胜利(因为我们正在谈论的内存是寄存器和一级缓存)。

这里有一些快速测试也可以向自己“证明”它。结果表明,您基本上不应该尝试在手动消除子表达式时智取编译器,只需自然编写代码并让编译器做它擅长的事情(例如弄清楚哪些表达式应该真正消除,哪些不应该给出目标架构和周边代码。)

稍后,如果您对代码的性能不满意,您应该对代码进行分析,看看哪些语句和表达式占用的时间最多,然后尝试弄清楚您是否可以重新组织代码以帮助编译器出来,但我会说绝大多数时候它不会像这样简单,它会做一些事情来减少缓存停顿(即更好地组织你的数据),消除冗余的程序间计算,以及类似的东西那。

(FTR 在以下代码中使用随机数只是确保编译器不会过于热衷于变量消除和循环展开)

编1:

#include <stdlib.h>
#include <time.h>

int main () {
    srandom(time(NULL));
    int i, ret = 0, a = random(), b = random(), values[10];
    int loop_end = random() % 5 + 1000000000;
    for (i=0; i < 10; ++i) { values[i] = random(); }

    for (i = 0; i < loop_end; ++i) {
        ret += a * b * values[i % 10];
    }

    return ret;
}

编2:

#include <stdlib.h>
#include <time.h>

int main () {
    srandom(time(NULL));
    int i, ret = 0, a = random(), b = random(), values[10];
    int loop_end = random() % 5 + 1000000000;
    for (i=0; i < 10; ++i) { values[i] = random(); }

    int c = a * b;
    for (i = 0; i < loop_end; ++i) {
        ret += c * values[i % 10];
    }

    return ret;
}

结果如下:

> gcc -O2 prog1.c -o prog1; time ./prog1  
./prog1  1.62s user 0.00s system 99% cpu 1.630 total

> gcc -O2 prog2.c -o prog2; time ./prog2
./prog2  1.63s user 0.00s system 99% cpu 1.636 total

(这里是测墙时间,所以不要注意0.01秒的差异,运行几次它们都在1.62-1.63秒的范围内,所以它们的速度是一样的)

有趣的是,在没有优化的情况下编译时 prog1 更快:

> gcc -O0 prog1.c -o prog1; time ./prog1  
./prog1  2.83s user 0.00s system 99% cpu 2.846 total

> gcc -O0 prog2.c -o prog2; time ./prog2 
./prog2  2.93s user 0.00s system 99% cpu 2.946 total

也很有趣,编译-O1提供了最好的性能..

gcc -O1 prog1.c -o prog1; time ./prog1 
./prog1  1.57s user 0.00s system 99% cpu 1.579 total

gcc -O1 prog2.c -o prog2; time ./prog2
./prog2  1.56s user 0.00s system 99% cpu 1.563 total

GCC 和 Intel 是很棒的编译器,并且非常聪明地处理这样的事情。我对 Portland 编译器没有任何经验,但这些都是编译器要做的非常基本的事情,所以如果它不能很好地处理这种情况,我会感到非常惊讶。

于 2013-01-09T17:18:06.743 回答
0

如果我是编译器,我会认识到这两个循环都没有左操作数,也没有任何副作用(除了设置i10),所以我会完全优化循环。

我并不是说这真的发生了。看起来它可能发生在您提供的代码中。

于 2013-01-09T17:43:09.727 回答