2

嗨,我一直在阅读各种文档中的此类内容

register

告诉编译器将声明的变量存储在 CPU 寄存器中。

在标准 C 方言中,关键字 register 使用以下语法:

register data-definition;

register类型修饰符告诉编译器将声明的变量存储在 CPU 寄存器中(如果可能),以优化访问。例如,

register int i;

请注意,TIGCC 会在优化开启时自动将常用变量存储在 CPU 寄存器中,但关键字 register 将强制存储在即使优化关闭的寄存器中。但是,如果编译器断定在此位置没有足够的空闲寄存器可供使用,则可能会拒绝将数据存储在寄存器中的请求。

http://tigcc.ticalc.org/doc/keywords.html#register

我的观点不仅仅是关于注册。我的观点是为什么编译器会将变量存储在内存中。编译器业务只是编译并生成一个目标文件。在运行时发生实际的内存分配。为什么编译器会做这件事。我的意思是如果不通过编译文件本身来运行目标文件,内存分配是否会在 C 的情况下发生?

4

3 回答 3

6

编译器正在生成机器码,机器码用于运行你的程序。编译器决定它生成什么机器代码,因此决定在运行时将发生什么样的分配。它不会在您键入时执行它们,gcc foo.c但稍后,当您运行可执行文件时,运行的是 GCC 生成的代码。

这意味着编译器希望生成尽可能快的代码并在编译时做出尽可能多的决定,这包括如何分配事物。

于 2013-10-31T14:53:53.723 回答
2

编译器不运行代码(除非它为分析和更好的代码执行做了几轮),但它必须准备它 - 这包括如何保留程序定义的变量,是否使用快速高效的存储作为寄存器,或使用较慢(且更容易产生副作用)的内存。

最初,您的局部变量将简单地分配到堆栈帧上的位置(当然,您明确使用动态分配的内存除外)。如果您的函数分配了一个 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 中)。这个例子展示了你在使用寄存器时会有多好,以及有时你不能。

于 2013-10-31T15:06:51.593 回答
1

编译器需要将代码翻译成机器指令,并告诉计算机如何运行代码。这包括如何进行操作(例如将两个数字相乘)以及如何存储数据(堆栈、堆或寄存器)。

于 2013-10-31T14:59:31.107 回答