在C中“注册”关键字?


272

register关键字用C语言做什么?我已经读过它用于优化,但是在任何标准中都没有明确定义。它仍然相关,如果有,您什么时候使用它?


41
register关键字在C中起什么作用?被忽略:)
bestsss 2011年

18
@bestsss没有被完全忽略。尝试获取register变量的地址。
qrdl 2012年

4
您正在读取的代码是旧的 youtube.com/watch?v=ibF36Yyeehw#t=1827
Panic Panic 2014年

Answers:


340

向编译器提示该变量将被大量使用,如果可能,建议将其保存在处理器寄存器中。

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


17
好吧,我尝试过注册以调整我的ACM提交,有时确实有帮助。但是您确实必须小心,因为选择不当会降低性能。
ypnos

80
使用“寄存器”的充分理由:您不能使用声明为“寄存器”的变量的地址
Adam Rosenfield

22
请注意,一些/许多编译器将完全忽略register关键字(这是完全合法的)。
Euro Micelli 2009年

4
ypnos:实际上,解决ACM ICPC问题的速度很大程度上取决于算法的选择,而不是取决于这种微优化。5秒的时间限制通常足以解决问题,特别是使用C而不是Java时。
乔伊(Joey)

65
@Euro:您可能知道这一点,但是为了明确起见,需要编译器来防止获取register变量的地址;这是关键字的唯一强制性效果register。即使这样也足以改善优化,因为告诉变量只能在此函数中进行修改变得微不足道。
戴尔·哈格隆德

69

我很惊讶没有人提到您不能接受寄存器变量的地址,即使编译器决定将变量保留在内存中而不是寄存器中。

因此,使用register您不会赢得任何收益(无论如何,编译器会自行决定将变量放在何处)而&不会丢失运算符-没有理由使用它。


94
实际上是有原因的。不能获取变量地址的事实这一事实带来了一些优化机会:编译器可以证明变量不会被别名。
Alexandre C.

8
众所周知,编译器在证明非平凡情况下不会发生别名时非常糟糕,因此register即使编译器未将其放入寄存器中,它也很有用。
Miles Rout 2014年

2
@AlexandreC,Miles,编译器可以很好地检查&是否在任何地方使用了变量。因此,无论在检测锯齿方面有其他困难,重述都无济于事。当K + R首次创建C时,事先知道&将不被使用确实是有用的,因为该编译器实际上是在查看以下代码之前根据看到该声明来确定寄存器分配的。这就是为什么实行禁令的原因。'register'关键字现在已经过时了。
greggo,2015年

25
通过这种逻辑const也是无用的,因为它不会给您带来任何好处,只会失去更改变量的能力。register可以用来确保将来没有人不假思索地获取变量的地址。我从来没有理由要使用register
Tor Klingberg,2015年

34

它告诉编译器尝试使用CPU寄存器而不是RAM来存储变量。寄存器位于CPU中,并且比RAM访问要快得多。但这只是对编译器的建议,可能不会遵循。


8
值得添加给使用C ++的人们,C ++使您可以获取寄存器变量的地址
2012年

5
@Will:...但是编译器最终可能会忽略该关键字。看我的答案。
bwDraco

是的,似乎“寄存器”在C ++中是一个安慰剂,它只用于允许C代码被编译为C ++。而且,禁止&var允许通过引用或const引用传递它也没有多大意义,并且如果没有通过引用传递,您就严重破坏了C ++。
greggo,2015年

22

我知道这个问题与C有关,但是C ++的同一问题已被关闭,因为它与该问题完全相同。因此,此答案可能不适用于C。


C ++ 11标准的最新草案N3485在7.1.1 / 3中这样说:

一个register说明符是一种提示,如此声明的变量将被频繁使用的实现。[ 注意:提示可以忽略,在大多数实现中,如果使用变量的地址,则将忽略提示。不建议使用此用法...-—尾注 ]

在C ++中(但不是在C),该标准没有规定你不能把一个变量的地址声明register; 但是,由于在CPU寄存器的整个生命周期中存储的变量都没有与之关联的存储位置,因此尝试获取其地址将无效,并且编译器将忽略register关键字以允许获取该地址。


17

至少有15年没有相关性了,因为优化程序会对此做出更好的决策。即使具有相关性,它在具有很多寄存器(如SPARC或M68000)的CPU体系结构上比在英特尔公司(其寄存器数量很少)所具有的意义要大得多,其中大多数寄存器是由编译器出于自身目的保留的。


13

实际上,register告诉编译器该变量不会与程序中的其他任何内容(甚至不是char的别名)一起别名。

现代编译器可以在各种情况下利用这一点,并且可以在复杂代码中为编译器提供相当大的帮助-在简单代码中,编译器可以自行解决。

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


“告诉编译器。”不,不是。所有自动变量都具有该属性,除非您获取其地址以超出某些可分析用途的方式使用它。因此,无论您是否使用register关键字,编译器都从代码中知道这一点。碰巧'register'关键字使编写这样的结构非法,但是如果您不使用该关键字并且以这种方式获取地址,则编译器仍然知道它是安全的。这些信息对于优化至关重要。
greggo,2015年

1
@greggo:太糟糕register了,根本禁止使用该地址,因为否则可能使编译器知道尽管将变量的地址传递给外部函数,编译器仍能够应用寄存器优化的情况很有用(变量必须会被刷新到该特定调用的内存,但是一旦函数返回,编译器便可以再次将其视为一个变量,其地址从未被占用过。
超级猫

我认为@supercat与编译器的对话仍然非常棘手。如果那是您要告诉编译器的内容,则可以通过将第一个变量复制到第二个没有'&'的变量来完成,然后再也不使用第一个。
greggo '16

1
@greggo:说如果barregister变量,则编译器可以随意将替换foo(&bar);int temp=bar; foo(&temp); bar=temp;,但是bar在大多数其他情况下,禁止使用的地址似乎并不是一个过于复杂的规则。如果可以将变量保存在寄存器中,则替换将使代码更小。如果仍然需要将该变量保留在RAM中,则替换将使代码变大。在这两种情况下,都不必考虑是否要由编译器来替代,这将导致更好的代码。
超级猫

1
@greggo:允许register对全局变量进行限定,无论编译器是否允许使用地址,在循环中重复调用使用全局变量的内联函数的情况下,都可以进行一些不错的优化。我想不出任何其他方法来让该变量在循环迭代之间保存在寄存器中,可以吗?
超级猫

13

我已经读过它用于优化,但是在任何标准中都没有明确定义。

事实上,它由C标准明确规定。引用N1570草案第6.7.1节第6段(其他版本具有相同的措词):

使用存储类说明符声明对象的标识符register表明,对对象的访问应尽可能快。此类建议有效的程度由实施定义。

一元运算&符可能不会应用于用定义的对象register,也register可能不会在外部声明中使用。

还有一些其他(相当模糊的)规则专门针对register-qualified对象:

  • 用定义数组对象register具有未定义的行为。
    更正:使用定义一个数组对象是合法的register,但是您不能对此类对象做任何有用的事情(索引到数组中需要使用其初始元素的地址)。
  • _Alignas说明符(在C11新)可以不被应用到这样一个对象。
  • 如果传递给va_start宏的参数名称是register-qualified,则行为是不确定的。

可能还有其他几个。下载标准草案,并在需要时搜索“注册”。

顾名思义,的原始含义register是要求将对象存储在CPU寄存器中。但是随着优化编译器方面的改进,此功能已不再有用。C标准的现代版本不引用CPU寄存器,因为它们不再(需要)假定存在这种情况(有些体系结构不使用寄存器)。普遍的看法是,应用于register对象声明更可能会使生成的代码恶化,因为它会干扰编译器自己的寄存器分配。在某些情况下,它仍然很有用(例如,如果您确实确实知道将多久访问一次变量,并且您的知识要比现代优化编译器可以弄清楚的要好)。

的主要实际效果register是,它可以防止尝试获取对象的地址。这对于作为优化提示不是特别有用,因为它只能应用于局部变量,并且优化编译器可以自己看到未使用该对象的地址。


那么根据C标准,程序的行为真的不确定吗?在C ++中定义是否正确?我认为它在C ++中定义良好。
毁灭者

@Destructor:为什么它是未定义的?register如果您正在考虑的话,则没有限定的数组对象。
基思·汤普森

抱歉,我忘了在main()的数组声明中写register关键字。在C ++中定义是否正确?
毁灭者

我定义register数组对象是错误的。请参阅我的答案中更新的第一个项目符号点。定义这样的对象是合法的,但是您不能对此做任何事情。如果在示例中添加register的定义,则该程序在C中是非法的(违反约束)。C++对并没有相同的限制,因此该程序将是有效的C ++(但使用毫无意义)。sregisterregister
基思·汤普森

@KeithThompson:register如果可以合法获取此类变量的地址,则该关键字可以起到有用的作用,但是仅在语义不受影响的情况下,通过将变量的地址复制到临时变量并将其从临时变量中重新加载,语义不会受到影响。在下一个序列点。这将允许编译器假定变量可以安全地保存在所有指针访问的寄存器中,只要在其地址所在的任何位置刷新该变量即可。
超级猫

9

讲故事的时间!

C作为一种语言,是计算机的抽象。它允许您根据计算机的功能来做事,即操纵内存,做数学运算,打印事情等。

但是C只是一个抽象。最终,它从您身上提取的是汇编语言。汇编是CPU读取的语言,如果您使用汇编语言,则将根据CPU来执行操作。CPU做什么?基本上,它从内存读取,进行数学运算并写入内存。CPU不仅对内存中的数字进行数学运算。首先,必须将一个数字从一个存储器移到另一个CPU内部的存储器,称为寄存器。一旦完成对该数字的所需处理,就可以将其移回普通系统内存。为什么要完全使用系统内存?寄存器数量有限。在现代处理器中,您只能得到大约一百个字节,而更受欢迎的较旧处理器则受到更大的限制(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

您搞砸了编译器的复杂图形着色算法。这用于寄存器分配。好吧,主要是。它是对编译器的提示-是的。但是请不要完全忽略它,因为不允许您使用寄存器变量的地址(请记住,编译器,请您放心,将尝试以不同的方式进行操作)。在某种程度上告诉您不要使用它。

关键字使用了很久很久了。当只有很少的寄存器可以使用食指将它们全部计数时。

但是,正如我所说,已弃用并不意味着您无法使用它。


13
一些较旧的硬件具有比现代Intel机器更多的寄存器。寄存器数与年龄无关,与CPU体系结构无关。
我的正确观点

2
@JUSTMYcorrectOPINION的确,X86基本上总共有六个,最多只能有1个或2个用于“注册”。实际上,由于已将大量代码写入或移植到贫乏寄存器的机器中,我怀疑这对'register'关键字成为安慰剂的作用很大-在没有寄存器的情况下提示寄存器是没有意义的。距今已有4年多的时间了,幸运的是x86_64已将其提高到14,ARM现在也是一件大事。
greggo,2015年

4

进行一些比较(没有任何实际用途)的演示: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;
}

2
使用gcc 4.8.4和-O3,我没有区别。没有-O3和40000次迭代,我在1.5s的总时间内可能减少了50毫秒,但是我没有运行足够的时间来知道这是否具有统计学意义。
zstewart

CLANG 5.0没什么区别,平台是AMD64。(我已经检查了ASM输出。)
ern0

4

我已经使用以下代码在QNX 6.5.0下测试了register关键字:

#include <stdlib.h>
#include <stdio.h>
#include <inttypes.h>
#include <sys/neutrino.h>
#include <sys/syspage.h>

int main(int argc, char *argv[]) {
    uint64_t cps, cycle1, cycle2, ncycles;
    double sec;
    register int a=0, b = 1, c = 3, i;

    cycle1 = ClockCycles();

    for(i = 0; i < 100000000; i++)
        a = ((a + b + c) * c) / 2;

    cycle2 = ClockCycles();
    ncycles = cycle2 - cycle1;
    printf("%lld cycles elapsed\n", ncycles);

    cps = SYSPAGE_ENTRY(qtime) -> cycles_per_sec;
    printf("This system has %lld cycles per second\n", cps);
    sec = (double)ncycles/cps;
    printf("The cycles in seconds is %f\n", sec);

    return EXIT_SUCCESS;
}

我得到以下结果:

-> 807679611个周期已过去

->该系统每秒具有3300830000个周期

->以秒为单位的周期为〜0.244600

现在没有注册诠释:

int a=0, b = 1, c = 3, i;

我有:

->经过了1421694077个周期

->该系统每秒具有3300830000个周期

->以秒为单位的周期为〜0.430700


2

寄存器将通知编译器编码器认为该变量将被足够地写入/读取,以证明其存储在少数可供变量使用的寄存器之一中。从寄存器读/写通常更快,并且可能需要更小的操作码集。

如今,这并不是很有用,因为大多数编译器的优化器在确定是否应将寄存器用于该变量以及使用多长时间方面比您更好。


2

七十年代,在C语言的最开始,引入了register关键字,以便允许程序员向编译器提供提示,告诉编译器该变量将经常使用,并且应该明智地使用它。将其值保存在处理器的内部寄存器之一中。

如今,优化器比程序员更有效地确定更可能保留在寄存器中的变量,并且优化器并不总是将程序员的提示考虑在内。

许多人错误地建议不要使用register关键字。

让我们看看为什么!

register关键字具有相关的副作用:您不能引用(获取地址)寄存器类型变量。

建议他人不要使用寄存器的人错误地将此作为附加论点。

但是,知道您不能使用寄存器变量的地址这一简单事实,使编译器(及其优化器)知道不能通过指针间接修改此变量的值。

当在指令流的某个点上,将寄存器变量的值分配到处理器的寄存器中,并且自从获取另一个变量的值以来未使用该寄存器时,编译器知道它不需要重新加载该寄存器中变量的值。这可以避免昂贵的无用的内存访问。

进行自己的测试,您将在大多数内部循环中获得显着的性能改进。

c_register_side_effect_performance_boost



1

register启用全局寄存器分配优化(/ Oe编译器标志)后,Microsoft的Visual C ++编译器将忽略该关键字。

请参阅在MSDN上注册关键字


1

Register关键字告诉编译器将特定变量存储在CPU寄存器中,以便可以快速访问它。从程序员的角度来看,register关键字用于程序中大量使用的变量,以便编译器可以加速代码。尽管将变量保留在CPU寄存器还是主存储器中取决于编译器。


0

寄存器指示编译器通过将该特定变量存储在寄存器中然后存储在内存中来优化此代码。这是对编译器的请求,编译器可能会也可能不会考虑此请求。如果某些变量经常被访问,则可以使用此功能。例如:循环。

还有一件事是,如果将变量声明为寄存器,则由于其未存储在内存中,因此无法获取其地址。它在CPU寄存器中获得分配。


0

gcc 9.3 asm输出,不使用优化标志(此答案中的所有内容均指没有优化标志的标准编译):

#include <stdio.h>
int main(void) {
  int i = 3;
  i++;
  printf("%d", i);
  return 0;
}
.LC0:
        .string "%d"
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 3
        add     DWORD PTR [rbp-4], 1
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, eax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        mov     eax, 0
        leave 
        ret
#include <stdio.h>
int main(void) {
  register int i = 3;
  i++;
  printf("%d", i);
  return 0;
}
.LC0:
        .string "%d"
main:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 8
        mov     ebx, 3
        add     ebx, 1
        mov     esi, ebx
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        add     rsp, 8
        pop     rbx
        pop     rbp
        ret

这将强制ebx用于计算,这意味着需要将其推入堆栈并在函数结束时恢复,因为已保存了被调用者。register产生更多的代码行,并进行1次内存写入和1次内存读取(尽管实际上,如果esi使用C ++ 完成计算,则可以将其优化为0 R / Ws const register)。不使用将register导致2次写入和1次读取(尽管读取时将发生存储到负载转发)。这是因为该值必须存在并直接在堆栈上更新,以便可以通过地址(指针)读取正确的值。register没有此要求,因此无法指出。constregister是基本上相对的volatile,并使用volatile将覆盖文件和块范围内的const优化,以及块范围内的const优化registerconst register并且register会产生相同的输出,因为const在块范围内对C不执行任何操作,因此仅register应用优化。

在clang上,将register被忽略,但const仍会进行优化。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.