当我有一个像这样的循环时:
for (int i = 0; i < SlowVariable; i++)
{
//
}
我知道在 VB6SlowVariable
中,循环的每次迭代都会访问它,从而使以下效率更高:
int cnt = SlowVariable;
for (int i = 0; i < cnt; i++)
{
//
}
我需要在 GCC 中进行相同的优化吗?还是SlowVariable
只评估一次?
当我有一个像这样的循环时:
for (int i = 0; i < SlowVariable; i++)
{
//
}
我知道在 VB6SlowVariable
中,循环的每次迭代都会访问它,从而使以下效率更高:
int cnt = SlowVariable;
for (int i = 0; i < cnt; i++)
{
//
}
我需要在 GCC 中进行相同的优化吗?还是SlowVariable
只评估一次?
这被称为“提升”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
不能引用相同的位置(或者程序无论如何都有未定义的行为),因此编译器可以再次提升。
通常,变量不能慢(或快),除非它们被映射到某种奇怪的内存(在这种情况下你通常想要声明它们volatile
)。
但实际上,使用局部变量会为优化创造更多机会,而且效果可能非常明显。只有当编译器能够证明在循环中调用的任何函数都不能读取或写入该全局变量时,编译器才能自行“缓存”全局变量。当您在循环中调用外部函数时,编译器可能无法证明这样的事情。
这取决于编译器如何优化,例如这里:
#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
i
将从堆栈中电影化到eax
.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
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
z
最终会出现在一些未使用的寄存器esi
中,寄存器是最快的存储分类。i
,在栈上,编译器用来ebx
存储 的值i
,也是寄存器。毕竟,这取决于编译器和您使用的优化选项,但是,在所有情况下,C 仍然比 VB 快得多,快得多。
这取决于您的编译器,但我相信如果 SlowVariable 的值是恒定的,大多数当代编译器都会为您优化。
“它”(语言)没有说。当然,它必须表现得好像每次都评估变量一样。
优化编译器可以做很多聪明的事情,所以最好将这些微优化留给编译器。
如果您要手动进行优化,请务必分析(=测量)并阅读生成的代码。
实际上它取决于“SlowVariable”和编译器的行为。如果你的慢变量是易变的,编译器不会做任何努力来缓存它,因为关键字 volatile 不允许它。如果它不是“易失的”,那么编译器很可能会通过将其加载到寄存器中来优化对该变量的连续访问。