C语言中的"register"关键字是什么意思?

316
在C语言中,“register”关键字是什么作用?我看过它被用于优化,但在任何标准中都没有明确定义。它还有用吗?如果有的话,在什么情况下会使用它?
在C语言中,“register”关键字可以提示编译器将变量存储在CPU寄存器中,以便更快地访问它们并提高程序效率。然而,现代编译器通常可以根据上下文自行选择最佳存储位置,因此“register”关键字很少使用。

56
register 关键字在 C 语言中有什么作用?(原文提示被忽略) - bestsss
27
并不完全忽略。尝试获取“register”变量的地址。 - qrdl
5
你正在阅读的那段代码很老 - Colonel Panic
19个回答

391

这是向编译器提供的一个提示,表明变量将被频繁使用,并建议尽可能将其保存在处理器寄存器中。

大多数现代编译器会自动执行此操作,并且比我们人类更擅长选择它们。


23
我尝试过在注册表中进行调整以改进我的ACM提交,有时真的很有效。但你必须小心,因为选择不当会降低性能。 - ypnos
93
不使用'register'的一个好理由是,你不能取得声明为'register'的变量的地址。 - Adam Rosenfield
27
注意,一些/许多编译器会完全忽略register关键字(这是完全合法的)。 - Euro Micelli
6
ypnos说:实际上,解决 ACM ICPC 问题的速度更依赖于算法选择,而不是微小的优化。使用 C 语言而不是 Java,在5秒的时间限制内通常足以得到正确的解决方案。 - Joey
74
@Euro:你可能已经知道了,但为明确起见,编译器需要防止获取“register”变量的地址;这是“register”关键字的唯一强制效果。即使这样做可以足以提高优化效果,因为它变得非常容易告诉变量只能在此函数中修改。 - Dale Hagglund
显示剩余5条评论

81

我很惊讶没有人提到过,即使编译器决定将变量存储在内存中而不是寄存器中,也不能获取寄存器变量的地址。

因此,使用 register 没有任何好处(无论如何编译器都会决定变量放在哪里),同时失去了 & 运算符 - 没有理由使用它。


116
实际上是有原因的。仅仅因为你无法获取变量的地址,就会产生一些优化机会:编译器可以证明该变量不会被别名引用。 - Alexandre C.
13
编译器在证明非平凡情况下没有别名发生时通常效果不佳,因此即使编译器没有将其放入寄存器中,register 对于此仍然很有用。 - Miles Rout
2
@AlexandreC,Miles,编译器在检查变量是否被取出时完全没有问题。因此,无论其他关于检测别名的困难如何,重申这一点对你毫无益处。当K+R首次创建C语言时,事先知道不会使用&确实很有用,因为该编译器实际上是在看到声明之后,在查看以下代码之前做出寄存器分配决策的。这就是为什么有禁止使用的规定。'register'关键字现在基本上已经过时了。 - greggo
37
按照这种逻辑,“const”也是无用的,因为它并没有给你带来任何好处,你只是失去了改变变量的能力。未来可能需要确保没有人会轻易获取该变量的地址,那么可以使用“register”。但我从未有过使用“register”的理由。 - Tor Klingberg
没有理由使用它,这就是为什么他们将其添加到语言中的原因。 - Kröw

44

该语句告诉编译器尝试使用CPU寄存器而不是RAM来存储变量。寄存器位于CPU中,访问速度比RAM快得多。但这只是对编译器的建议,它可能不一定遵循。


10
对于使用C++的人来说,值得补充的是,C++允许您获取寄存器变量的地址。 - Will
5
但是编译器很可能会因此忽略该关键字。请参阅我的回答。 - bwDraco
是的,似乎在C++中'register'只是一个安慰剂,它只是允许将C代码编译为C++。而且,禁止&var并允许按引用传递或const引用传递并没有太多意义,如果没有按引用传递,你就会严重破坏C++。 - greggo
1
“如果没有传递引用,你就严重破坏了C++。” 我并没有这样做。我第一次看到它的时候就是这样的 ;) - user426

29

我知道这个问题涉及到C语言,但是同样关于C++的问题被关闭了,认为是这个问题的一个精确重复。因此,这个答案可能不适用于C。


C++11标准的最新草案N3485在7.1.1/3中指出:

register限定符是向实现者提示所声明的变量将会被频繁使用。[ 注意: 如果变量的地址被取出,则该提示可以被忽略,在大多数实现中,如果变量的地址被取出,它也将被忽略。这种用法已经过时... —end note ]

在C++中(但不是C),标准没有规定您不能取一个被声明为register的变量的地址;然而,由于存储在CPU寄存器中的变量在其生命周期内没有与之关联的内存位置,尝试取其地址将是无效的,编译器将忽略register关键字以允许取其地址。


19
我看过了一些关于优化的资料,但是并没有任何标准明确定义它。实际上,C标准已经明确定义了它。引用N1570 draft第6.7.1节第6段(其他版本有相同措辞):“使用存储类别说明符register声明标识符来表示访问对象应尽可能快。这样的建议在多大程度上有效是由实现定义的。”不能对使用register定义的对象应用一元运算符&,也不能在外部声明中使用register。还有一些其他(相当晦涩的)规则是特定于使用register修饰的对象的。
  • 使用register定义数组对象具有未定义的行为。
    更正:使用register定义数组对象是合法的,但您不能对这样的对象进行任何有用的操作(索引到数组需要获取其初始元素的地址)。
  • _Alignas说明符(C11中的新说明符)可能无法应用于此类对象。
  • 如果传递给va_start宏的参数名称被标记为register,则其行为是未定义的。

还可能有其他一些,请下载标准草案并搜索"register"以了解更多信息。

作为其名称所示,register 的原始含义是要求将对象存储在 CPU 寄存器中。但随着优化编译器的改进,这变得不再有用。现代版本的 C 标准不再引用 CPU 寄存器,因为它们不再(需要)假定存在这样的东西(有一些架构不使用寄存器)。普遍认为,将 register 应用于对象声明更可能会恶化生成的代码,因为它干扰了编译器自己的寄存器分配。仍然可能有一些情况是有用的(比如,如果您确实知道一个变量被访问的频率有多高,并且您的知识比现代优化编译器能够推断的更好)。 register 的主要切实影响是防止尝试获取对象的地址。这并不特别有用作为优化提示,因为它只能应用于局部变量,并且优化编译器可以自行看到这样的对象的地址没有被获取。

这个程序的行为是否根据C标准真的是未定义的?在C++中它是否被定义良好?我认为在C++中它是被定义良好的。 - Destructor
@Destructor:为什么会是未定义的呢?如果您认为有register限定的数组对象,那就错了。 - Keith Thompson
哦,对不起,我忘记在主函数中的数组声明中写上寄存器关键字了。在C++中这是明确定义的吗? - Destructor
我之前关于定义register数组对象的理解是错误的;请查看我的回答中更新的第一条要点。定义这样的对象是合法的,但你不能对其进行任何操作。如果在 你的示例 中将 register 添加到 s 的定义中,则该程序在 C 中是非法的(违反了约束)。C++ 对 register 没有相同的限制,因此该程序将是有效的 C++(但使用 register 是没有意义的)。 - Keith Thompson
@KeithThompson:如果可以合法地获取register关键字变量的地址,那么它可能会有用处,但仅限于在获取其地址时将变量复制到临时变量中,且在下一个序列点从临时变量重新加载时不会影响语义的情况下。这将允许编译器假定该变量可以安全地保留在寄存器中,跨所有指针访问,只要在获取其地址的任何位置刷新即可。 - supercat

18

在至少15年的时间里,它已经不再相关了,因为优化器可以比你更好地做出这方面的决策。即使在它仍然相关的时候,在像SPARC或M68000这样具有大量寄存器的CPU架构上比在Intel上更加合理,后者只有很少的寄存器,大多数被编译器保留用于自身目的。


编译器在识别别名缺失方面仍然比开发人员差得多,因此减少重复间接引用(非常昂贵)并帮助本地缓存数据的实践仍然非常相关。 - user426

14

实际上,register告诉编译器该变量在程序中与其他任何东西(甚至是char)都没有别名。

这可以在现代编译器中的各种情况下被利用,并且可以帮助编译器在复杂代码中大有裨益——在简单代码中,编译器可以自行解决这个问题。

否则,它没有任何作用,也不用于寄存器分配。只要您的编译器足够现代化,指定它通常不会导致性能下降。


告诉编译器...不,它并不会。所有自动变量都具有该属性,除非您获取其地址并且以超出某些可分析用途的方式使用它。因此,编译器从代码中知道这一点,无论您是否使用寄存器关键字。恰好发生的是,“register”关键字使编写这样的结构非法,但如果您不使用关键字,并且在这种方式下获取地址,则编译器仍然知道它是安全的。这样的信息对于优化至关重要。 - greggo
1
@greggo:太遗憾了,register禁止获取地址,否则它可以让编译器知道即使变量的地址被传递给外部函数,编译器仍能应用寄存器优化的情况(该变量必须在特定调用中刷新到内存,但一旦函数返回,编译器可以再次将其视为从未获取过地址的变量)。 - supercat
1
@supercat 我认为与编译器进行这样的对话仍然是非常棘手的。如果这就是你想告诉编译器的内容,你可以通过将第一个变量复制到一个没有'&'的第二个变量中,然后再也不使用第一个变量来实现。 - greggo
1
@greggo:如果bar是一个register变量,编译器可以随意将foo(&bar);替换为int temp=bar; foo(&temp); bar=temp;,但在大多数其他情况下,取bar的地址是被禁止的,这似乎不是一个过于复杂的规则。如果变量本来就可以保留在寄存器中,那么替换会使代码更小。如果变量无论如何都需要保留在RAM中,替换会使代码更大。让编译器决定是否进行替换将在两种情况下都产生更好的代码。 - supercat
2
@greggo:允许在全局变量上使用“register”限定符,无论编译器是否允许获取地址,在循环中重复调用使用全局变量的内联函数时,可以实现一些不错的优化。我想不到其他任何方法来让该变量在循环迭代之间保持在寄存器中 - 你呢? - supercat

13

故事时间!

C语言是计算机的抽象。它允许您做一些与计算机相关的事情,如操作内存、进行数学运算、打印等。

但C只是一个抽象。最终,它从那里提取的是汇编语言。汇编语言是CPU读取的语言,如果您使用它,您将按照CPU的方式执行操作。CPU做什么?基本上是从内存中读取数据,进行数学运算,并将结果写回内存。CPU不会直接对内存中的数字进行数学运算。首先,您需要将数字从内存移动到CPU内部称为寄存器的内存中。完成对此数字所需的任何操作后,您可以将其移回常规系统内存。为什么要使用系统内存?寄存器数量有限。现代处理器只有约100个字节,而早期流行的处理器甚至更加奇葩(6502只有3个8位寄存器供您自由使用)。因此,您平均的数学运算看起来像:

load first number from memory
load second number from memory
add the two
store answer into memory

很多操作不涉及数学。加载和存储操作可能占用了处理时间的一半。C语言是计算机的抽象,使程序员不必担心寄存器的使用和操作,因为寄存器的数量和类型因计算机而异,C语言将寄存器分配的责任完全交给编译器。除一个例外。

当你声明一个变量register时,你在告诉编译器:“嗨,我打算经常使用这个变量并且/或者它的生命周期短暂。如果我是你,我会尝试将其保存在寄存器中。” 当C标准表示编译器不必实际执行任何操作时,那是因为C标准不知道你正在编译的计算机是什么,它可能像上面所述的6502一样,需要使用所有3个寄存器才能运行,并且没有多余的寄存器可以保存您的数字。但是,当它说您不能获得地址时,那是因为寄存器没有地址。他们是处理器的手。由于编译器不必给您地址,并且它根本不能有地址,因此现在有几种优化方法开放给编译器。例如,它可以始终将数字保留在寄存器中。它不必担心在计算机内存中的存储位置(除了需要再次获取它之外)。它甚至可以将其转换为另一变量,给另一个处理器,给它一个可以改变的位置等。

tl; dr:寿命短的变量执行很多数学操作。不要一次声明太多。


5

这是一个小的演示(没有实际用途),来进行比较:当在每个变量之前删除register关键字时,这段代码在我的i7上(GCC)需要3.41秒,使用register相同的代码只需要0.7秒。

#include <stdio.h>

int main(int argc, char** argv) {

     register int numIterations = 20000;    

     register int i=0;
     unsigned long val=0;

    for (i; i<numIterations+1; i++)
    {
        register int j=0;
        for (j;j<i;j++) 
        {
            val=j+i;
        }
    }
    printf("%d", val);
    return 0;
}

4
使用gcc 4.8.4和-O3编译时,我没有看到任何差异。如果不加-O3选项并且进行40000次迭代,可能有50毫秒左右的时间减少,但我没有运行足够多次来确定这是否具有统计学意义。 - zstewart
没有任何区别,使用的平台是AMD64,我已经检查了汇编输出。 - ern0

5
你正在干扰编译器复杂的图着色算法。该算法用于寄存器分配,或多或少是作为编译器的提示使用。但并非完全被忽略,因为你不能获取寄存器变量的地址(请记住编译器现在由你支配,将尝试以不同的方式运行)。这在某种程度上告诉你不要使用它。
这个关键字早在很久以前就被使用了。当时只有那么几个寄存器,可以用手指头数清楚。
但是,正如我所说,弃用并不意味着你不能使用它。

13
一些较旧的硬件拥有比现代英特尔机器更多的寄存器。寄存器数量与年龄无关,与CPU架构有关。 - JUST MY correct OPINION
2
@JUSTMYcorrectOPINION 实际上,X86基本上只有六个寄存器可用,最多只能留出1或2个用于“专用寄存器”。事实上,由于已经编写或移植了大量代码到一个寄存器较少的机器上,我怀疑这对“register”关键字成为安慰剂做出了很大贡献 - 当没有寄存器时,暗示寄存器是没有意义的。现在我们已经过去了4年多,值得庆幸的是,x86_64已经增加到14个寄存器,而ARM也成为了一个重要的东西。 - greggo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接