2

当我有一个像这样的循环时:

for (int i = 0; i < SlowVariable; i++)
{
   //
}

我知道在 VB6SlowVariable中,循环的每次迭代都会访问它,从而使以下效率更高:

int cnt = SlowVariable;

for (int i = 0; i < cnt; i++)
{
   //
}

我需要在 GCC 中进行相同的优化吗?还是SlowVariable只评估一次?

4

6 回答 6

5

这被称为“提升”SlowVariabele循环。

只有当编译器能够证明 的值SlowVariabele每次都相同并且评估SlowVariabele没有副作用时,编译器才能做到这一点。

因此,例如考虑以下代码(为了举例,我假设由于某种原因通过指针访问是“慢”的):

void foo1(int *SlowVariabele, int *result) {
    for (int i = 0; i < *SlowVariabele; ++i) {
       --*result;
    }
}

编译器不能(通常)提升,因为它知道它将被调用result == SlowVariabele,因此 的值*SlowVariabele在循环期间会发生变化。

另一方面:

void foo2(int *result) {
    int val = 12;
    int *SlowVariabele = &val;
    for (int i = 0; i < *SlowVariabele; ++i) {
       --*result;
    }
}

现在至少在原则上,编译器可以知道val循环中永远不会发生变化,因此它可以提升。它是否真的这样做取决于优化器的积极程度以及它对函数的分析有多好,但我希望任何认真的编译器都能做到这一点。

类似地,如果foo1使用编译器可以确定(在调用站点)不相等的指针调用,并且如果调用是内联的,那么编译器可以提升。这就是restrict

void foo3(int *restrict SlowVariabele, int *restrict result) {
    for (int i = 0; i < *SlowVariabele; ++i) {
       --*result;
    }
}

restrict(在 C99 中引入)意味着“你不能用result == SlowVariabele”调用这个函数,并允许编译器提升。

相似地:

void foo4(int *SlowVariabele, float *result) {
    for (int i = 0; i < *SlowVariabele; ++i) {
       --*result;
    }
}

严格的别名规则意味着SlowVariable并且result不能引用相同的位置(或者程序无论如何都有未定义的行为),因此编译器可以再次提升。

于 2013-01-31T13:09:10.550 回答
2

通常,变量不能慢(或快),除非它们被映射到某种奇怪的内存(在这种情况下你通常想要声明它们volatile)。

但实际上,使用局部变量会为优化创造更多机会,而且效果可能非常明显。只有当编译器能够证明在循环中调用的任何函数都不能读取写入该全局变量时,编译器才能自行“缓存”全局变量。当您在循环中调用外部函数时,编译器可能无法证明这样的事情。

于 2013-01-31T12:38:09.997 回答
1

这取决于编译器如何优化,例如这里:

#include <stdio.h>

int main(int argc, char **argv)
{
    unsigned int i;
    unsigned int z = 10;

    for( i = 0 ; i < z ; i++ )
        printf("%d\n", i);

    return 0;
}

如果您使用 编译它gcc example.c -o example,结果代码将是:

 0x0040138c <+0>:     push   ebp
 0x0040138d <+1>:     mov    ebp,esp
 0x0040138f <+3>:     and    esp,0xfffffff0
 0x00401392 <+6>:     sub    esp,0x20
 0x00401395 <+9>:     call   0x4018f4 <__main>
 0x0040139a <+14>:    mov    DWORD PTR [esp+0x18],0xa
 0x004013a2 <+22>:    mov    DWORD PTR [esp+0x1c],0x0
 0x004013aa <+30>:    jmp    0x4013c4 <main+56>
 0x004013ac <+32>:    mov    eax,DWORD PTR [esp+0x1c]
 0x004013b0 <+36>:    mov    DWORD PTR [esp+0x4],eax
 0x004013b4 <+40>:    mov    DWORD PTR [esp],0x403064
 0x004013bb <+47>:    call   0x401b2c <printf>
 0x004013c0 <+52>:    inc    DWORD PTR [esp+0x1c]
 0x004013c4 <+56>:    mov    eax,DWORD PTR [esp+0x1c]  ; (1) 
 0x004013c8 <+60>:    cmp    eax,DWORD PTR [esp+0x18]  ; (2)
 0x004013cc <+64>:    jb     0x4013ac <main+32>
 0x004013ce <+66>:    mov    eax,0x0
 0x004013d3 <+71>:    leave
 0x004013d4 <+72>:    ret
 0x004013d5 <+73>:    nop
 0x004013d6 <+74>:    nop
 0x004013d7 <+75>:    nop
  1. 的值i将从堆栈中电影化到eax.
  2. 然后 CPU 将eax或与堆栈中i的 的值进行比较。z

所有这些都发生在每一轮。

如果您使用 优化代码gcc -O2 example.c -o example,结果将是:

 0x00401b70 <+0>:     push   ebp
 0x00401b71 <+1>:     mov    ebp,esp
 0x00401b73 <+3>:     push   ebx
 0x00401b74 <+4>:     and    esp,0xfffffff0
 0x00401b77 <+7>:     sub    esp,0x10
 0x00401b7a <+10>:    call   0x4018a8 <__main>
 0x00401b7f <+15>:    xor    ebx,ebx
 0x00401b81 <+17>:    lea    esi,[esi+0x0]
 0x00401b84 <+20>:    mov    DWORD PTR [esp+0x4],ebx
 0x00401b88 <+24>:    mov    DWORD PTR [esp],0x403064
 0x00401b8f <+31>:    call   0x401ae0 <printf>
 0x00401b94 <+36>:    inc    ebx
 0x00401b95 <+37>:    cmp    ebx,0xa                     ; (1)
 0x00401b98 <+40>:    jne    0x401b84 <main+20>
 0x00401b9a <+42>:    xor    eax,eax
 0x00401b9c <+44>:    mov    ebx,DWORD PTR [ebp-0x4]
 0x00401b9f <+47>:    leave
 0x00401ba0 <+48>:    ret
 0x00401ba1 <+49>:    nop
 0x00401ba2 <+50>:    nop
 0x00401ba3 <+51>:    nop
  1. 编译器知道检查 的值是没有意义的z,所以它将代码修改为类似 的内容for( i = 0 ; i < 10 ; i++ )

如果编译器不知道z此代码中 like 的值:

#include <stdio.h>

void loop(unsigned int z) {
    unsigned int i;
    for( i = 0 ; i < z ; i++ )
        printf("%d\n", i);
}

int main(int argc, char **argv)
{
    unsigned int z = 10;

    loop(z);

    return 0;
}

结果将是:

0x0040138c <+0>:     push   esi
0x0040138d <+1>:     push   ebx
0x0040138e <+2>:     sub    esp,0x14
0x00401391 <+5>:     mov    esi,DWORD PTR [esp+0x20] ; (1)
0x00401395 <+9>:     test   esi,esi
0x00401397 <+11>:    je     0x4013b1 <loop+37>
0x00401399 <+13>:    xor    ebx,ebx                  ; (2)
0x0040139b <+15>:    nop
0x0040139c <+16>:    mov    DWORD PTR [esp+0x4],ebx
0x004013a0 <+20>:    mov    DWORD PTR [esp],0x403064
0x004013a7 <+27>:    call   0x401b0c <printf>
0x004013ac <+32>:    inc    ebx
0x004013ad <+33>:    cmp    ebx,esi
0x004013af <+35>:    jne    0x40139c <loop+16>
0x004013b1 <+37>:    add    esp,0x14
0x004013b4 <+40>:    pop    ebx
0x004013b5 <+41>:    pop    esi
0x004013b6 <+42>:    ret
0x004013b7 <+43>:    nop
  1. z最终会出现在一些未使用的寄存器esi中,寄存器是最快的存储分类。
  2. 没有局部变量i,在栈上,编译器用来ebx存储 的值i,也是寄存器。

毕竟,这取决于编译器和您使用的优化选项,但是,在所有情况下,C 仍然比 VB 快得多,快得多。

于 2013-01-31T14:06:58.360 回答
0

这取决于您的编译器,但我相信如果 SlowVariable 的值是恒定的,大多数当代编译器都会为您优化。

于 2013-01-31T12:35:01.800 回答
0

“它”(语言)没有说。当然,它必须表现得好像每次都评估变量一样。

优化编译器可以做很多聪明的事情,所以最好将这些微优化留给编译器。

如果您要手动进行优化,请务必分析(=测量)并阅读生成的代码。

于 2013-01-31T12:35:23.547 回答
0

实际上它取决于“SlowVariable”和编译器的行为。如果你的慢变量是易变的,编译器不会做任何努力来缓存它,因为关键字 volatile 不允许它。如果它不是“易失的”,那么编译器很可能会通过将其加载到寄存器中来优化对该变量的连续访问。

于 2013-01-31T12:36:03.397 回答