编译器不运行代码(除非它为分析和更好的代码执行做了几轮),但它必须准备它 - 这包括如何保留程序定义的变量,是否使用快速高效的存储作为寄存器,或使用较慢(且更容易产生副作用)的内存。
最初,您的局部变量将简单地分配到堆栈帧上的位置(当然,您明确使用动态分配的内存除外)。如果您的函数分配了一个 int,您的编译器可能会告诉堆栈增加几个额外的字节,并使用该内存地址来存储该变量并将其作为操作数传递给您的代码对该变量执行的任何操作。
但是,由于内存速度较慢(即使在缓存时),并且操作它会对 CPU 造成更多限制,所以在稍后阶段,编译器可能会决定尝试将一些变量移动到寄存器中。这种分配是通过一个复杂的算法完成的,该算法试图选择最重用和延迟关键的变量,这些变量可以适合您的架构现有的逻辑寄存器数量(同时确认各种限制,例如一些指令要求操作数在此或该寄存器)。
还有另一个复杂情况 - 某些内存地址可能会以编译时未知的方式与外部指针混叠,在这种情况下,您无法将它们移动到寄存器中。编译器通常是一群非常谨慎的人,他们中的大多数会避免危险的优化(否则他们需要进行一些特殊的检查以避免讨厌的事情)。
毕竟,编译器仍然很有礼貌,可以让您建议哪个变量对您很重要和关键,以防他错过它,并且通过用register
关键字标记这些,您基本上是在要求他尝试为此进行优化如果有足够的寄存器可用并且不可能有别名,则可以通过使用寄存器来实现变量。
下面是一个小例子:以下面的代码为例,两次做同样的事情,但情况略有不同:
#include "stdio.h"
int j;
int main() {
int i;
for (i = 0; i < 100; ++i) {
printf ("i'm here to prevent the loop from being optimized\n");
}
for (j = 0; j < 100; ++j) {
printf ("me too\n");
}
}
请注意, i 是本地的, j 是全局的(因此编译器不知道在运行期间是否有其他人可以访问他)。
在 gcc 中使用 -O3 编译会为 main 生成以下代码:
0000000000400540 <main>:
400540: 53 push %rbx
400541: bf 88 06 40 00 mov $0x400688,%edi
400546: bb 01 00 00 00 mov $0x1,%ebx
40054b: e8 18 ff ff ff callq 400468 <puts@plt>
400550: bf 88 06 40 00 mov $0x400688,%edi
400555: 83 c3 01 add $0x1,%ebx # <-- i++
400558: e8 0b ff ff ff callq 400468 <puts@plt>
40055d: 83 fb 64 cmp $0x64,%ebx
400560: 75 ee jne 400550 <main+0x10>
400562: c7 05 80 04 10 00 00 movl $0x0,1049728(%rip) # 5009ec <j>
400569: 00 00 00
40056c: bf c0 06 40 00 mov $0x4006c0,%edi
400571: e8 f2 fe ff ff callq 400468 <puts@plt>
400576: 8b 05 70 04 10 00 mov 1049712(%rip),%eax # 5009ec <j> (loads j)
40057c: 83 c0 01 add $0x1,%eax # <-- j++
40057f: 83 f8 63 cmp $0x63,%eax
400582: 89 05 64 04 10 00 mov %eax,1049700(%rip) # 5009ec <j> (stores j back)
400588: 7e e2 jle 40056c <main+0x2c>
40058a: 5b pop %rbx
40058b: c3 retq
如您所见,第一个循环计数器位于 ebx 中,并在每次迭代时递增并与限制进行比较。
然而,第二个循环是危险的,gcc 决定通过内存传递索引计数器(每次迭代将其加载到 rax 中)。这个例子展示了你在使用寄存器时会有多好,以及有时你不能。