“register"关键字在C?

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

132214 次浏览

它告诉编译器尝试使用CPU寄存器,而不是RAM来存储变量。寄存器在CPU中,访问速度比RAM快得多。但这只是给编译器的一个建议,它可能不会执行到底。

至少15年来,它已经不相关了,因为优化者在这方面做出的决定比你更好。即使它是相关的,它在具有大量寄存器的CPU架构(如SPARC或M68000)上也比在缺乏寄存器的Intel上更有意义,因为大多数寄存器由编译器保留用于自己的目的。

这是对编译器的一个提示,表明该变量将被大量使用,并且建议尽可能将它保存在处理器寄存器中。

大多数现代编译器都是自动的,并且比我们人类更擅长选择它们。

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

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

在支持的C编译器上,它尝试优化代码,以便变量的值保存在实际的处理器寄存器中。

You are messing with the compiler's sophisticated graph-coloring algorithm. 这用于寄存器分配。嗯,主要是。它对编译器起到了提示作用——这是真的。但不能完全忽略它,因为您不允许使用寄存器变量的地址(请记住,编译器现在将尝试不同的操作)。这在某种程度上是在告诉你不要使用它。

这个关键字是很久很久以前用的。当时只有那么几个寄存器能用你的食指数出来。

但是,正如我所说,deprecated并不意味着你不能使用它。

我很惊讶,没有人提到,你不能采取寄存器变量的地址,即使编译器决定保持变量在内存而不是在寄存器。

所以使用register你什么都得不到(反正编译器会自己决定把变量放在哪里),并且失去&操作符——没有理由使用它。

实际上,寄存器告诉编译器该变量不与 程序中的任何其他内容(甚至不包括char)

这可以被现代编译器在各种情况下利用,并可以在复杂代码中帮助编译器-在简单代码中,编译器可以自己解决这个问题。

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

当启用全局寄存器分配优化(/Oe编译器标志)时,Microsoft的Visual c++编译器会忽略register关键字。

参见MSDN上的注册关键字

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

寄存器指示编译器通过将特定变量存储在寄存器中然后存储在内存中来优化这段代码。它是对编译器的请求,编译器可能考虑也可能不考虑这个请求。 你可以在某些变量被频繁访问的情况下使用这个工具。 例如:循环。

还有一件事是,如果你把一个变量声明为寄存器,那么你就不能得到它的地址,因为它没有存储在内存中。它在CPU寄存器中得到分配。

我知道这个问题是关于C语言的,但是同样的c++问题作为这个问题的完全复制而结束了。因此,这个答案可能不适用于C。


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

register说明符是对实现的一个提示,即这样声明的变量将被大量使用。[注意:提示可以被忽略,在大多数实现中,如果变量的地址被获取,它将被忽略。这种用法已被弃用…端注]

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

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

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

带有storage-class的对象标识符的声明 说明符register建议对对象的访问同样快 越好。这些建议的有效程度是 实现定义。< / p >

一元&操作符不能应用于用register定义的对象,并且register不能在外部声明中使用。

还有其他一些特定于__abc0限定对象的规则(相当模糊):

    <李> register定义数组对象具有未定义的行为。 < br > 使用register定义一个数组对象是合法的,但你不能对这样的对象做任何有用的事情(索引数组需要取其初始元素的地址)。
  • _Alignas说明符(C11中新增)不能应用于这样的对象。
  • 如果传递给va_start宏的参数名是__abc1限定的,则行为是未定义的。

可能还有其他一些;下载一份标准草案,如果感兴趣,可以搜索“register”。

顾名思义,register原始含义是要求将对象存储在CPU寄存器中。但是随着优化编译器的改进,这已经变得不那么有用了。现代版本的C标准没有提到CPU寄存器,因为它们不再(需要)假设存在这样的东西(有些架构不使用寄存器)。通常的观点是,对对象声明应用register更有可能恶化生成的代码,因为它会干扰编译器自己的寄存器分配。在一些情况下,它仍然是有用的(例如,如果您确实知道变量被访问的频率,并且您的知识比现代优化编译器所能计算的更好)。

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

在70年代,在C语言的最开始,register关键字的引入是为了允许程序员给编译器提示,告诉它变量将被经常使用,并且明智的做法是将它的值保存在处理器的一个内部寄存器中。

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

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

让我们看看为什么!

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

建议别人不要使用寄存器的人错误地将此作为附加参数。

然而,知道不能获取寄存器变量的地址这一简单事实,允许编译器(及其优化器)知道该变量的值不能通过指针间接修改。

当在指令流的某一点上,一个寄存器变量在处理器的寄存器中赋值,并且该寄存器此后没有被用于获取另一个变量的值时,编译器知道它不需要在该寄存器中重新加载变量的值。 这允许避免昂贵的无用的内存访问

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

c_register_side_effect_performance_boost

只是一个小演示(没有任何现实世界的目的)进行比较:当在每个变量之前删除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;
}

Storytime !

作为一种语言,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:短期变量,做很多数学运算。不要一次申报太多。

我在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:

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

我有:

-> 1421694077周期已经过

->系统周期为3300830000次/秒

->周期为~0.430700秒

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中进行计算,这可以优化为0 R/ w,这就是使用c++的register4所发生的情况)。不使用register会导致2次写入和1次读取(尽管store to load forwarding会在读取时发生)。这是因为该值必须直接在堆栈上显示和更新,以便通过地址(指针)读取正确的值。register没有这个要求,不能被指向。constregister基本上与volatile相反,使用volatile将覆盖文件和块范围内的const优化以及块范围内的register优化。const registerregister将产生相同的输出,因为const在块范围内对C不做任何操作,因此只有register优化适用。

在clang时,register被忽略,但仍会发生const优化。

register关键字是向编译器请求将指定的变量存储在处理器的寄存器中而不是内存中,以提高速度,主要是因为它将被大量使用。编译器可能会忽略该请求。