GCC缓存循环变量吗?

时间:2013-01-31 12:32:08

标签: c performance optimization gcc c99

当我有一个循环时:

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

我知道在VB6中,每次循环迭代都会访问SlowVariable,从而提高效率:

int cnt = SlowVariable;

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

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

6 个答案:

答案 0 :(得分:5)

这被称为&#34;吊装&#34; <{1}}离开了。

编译器只有在每次都能证明SlowVariabele的值相同且评估SlowVariabele没有副作用时才能这样做。

因此,例如,考虑以下代码(我假设为了示例,通过指针访问是&#34;慢&#34;由于某种原因):

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; } } (在C99中引入)表示&#34;您不能使用restrict&#34;来调用此函数,并允许编译器提升。

类似地:

result == SlowVariabele

严格的别名规则意味着void foo4(int *SlowVariabele, float *result) { for (int i = 0; i < *SlowVariabele; ++i) { --*result; } } SlowVariable不能引用相同的位置(或者程序还有未定义的行为),因此编译器也可以提升。

答案 1 :(得分:2)

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

但实际上,使用局部变量可以为优化创造更多机会,并且效果可能非常明显。只有当它能够证明在循环中调用的函数不能读取该全局变量时,编译器才能“缓存”全局变量。当你在循环中调用外部函数时,编译器可能无法证明这一点。

答案 2 :(得分: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会将eaxi与堆栈中z的值进行比较。
  3. 所有这一切都发生在每一轮。

    如果使用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++ )
    2. 如果编译器没有像这段代码那样知道z的值:

      #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的值,也注册。
      3. 毕竟,它取决于您使用的编译器和优化选项,但在所有情况下,C仍然比VB更快,更快。

答案 3 :(得分:0)

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

答案 4 :(得分:0)

“它”(语言)不说。它必须表现,就像每次评估变量一样。

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

如果您要通过手动路线进行优化,请务必 profile (= measure)并阅读生成的代码。

答案 5 :(得分:0)

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