C语言中关键字的作用是什么register
?我读过它用于优化,但在任何标准中都没有明确定义。它仍然相关吗?如果是,你什么时候使用它?
19 回答
这是对编译器的一个提示,即该变量将被大量使用,并且您建议尽可能将其保存在处理器寄存器中。
大多数现代编译器会自动执行此操作,并且比我们人类更擅长挑选它们。
我很惊讶没有人提到你不能获取寄存器变量的地址,即使编译器决定将变量保存在内存中而不是寄存器中。
所以使用register
你什么都不会(无论如何编译器会自己决定将变量放在哪里)并失去&
运算符 - 没有理由使用它。
它告诉编译器尝试使用 CPU 寄存器而不是 RAM 来存储变量。寄存器位于 CPU 中,访问速度比 RAM 快得多。但这只是对编译器的建议,可能不会贯彻执行。
我知道这个问题是关于 C 的,但是 C++ 的同一个问题作为这个问题的完全重复而被关闭。因此,这个答案可能不适用于 C。
C++11 标准的最新草案N3485在 7.1.1/3 中这样说:
说明
register
符是对实现的提示,即如此声明的变量将被大量使用。[注意:提示可以被忽略,并且在大多数实现中,如果获取变量的地址,它将被忽略。这种用法已被弃用... —尾注]
在 C++ 中(但不是在 C 中),标准并未声明您不能获取声明的变量的地址register
;但是,由于在整个生命周期中存储在 CPU 寄存器中的变量没有与之关联的内存位置,因此尝试获取其地址将是无效的,并且编译器将忽略register
关键字以允许获取地址。
至少 15 年以来它一直没有相关性,因为优化人员对此做出了比你更好的决策。即使它是相关的,它在具有大量寄存器的 CPU 架构(如 SPARC 或 M68000)上比在 Intel 上更有意义,因为它缺乏寄存器,其中大部分由编译器保留用于自己的目的。
我读过它用于优化,但在任何标准中都没有明确定义。
事实上,它是由 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只是一个抽象。最终,它从您那里提取的是汇编语言。汇编是 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:做大量数学运算的短期变量。不要一次声明太多。
您正在弄乱编译器复杂的图形着色算法。这用于寄存器分配。嗯,大部分。它作为对编译器的提示——这是真的。但不要完全忽略,因为您不允许获取寄存器变量的地址(请记住,编译器现在取决于您的怜悯,将尝试采取不同的行动)。这在某种程度上告诉你不要使用它。
这个关键字被使用了很久很久。当只有很少的寄存器可以用食指计算它们时。
但是,正如我所说,弃用并不意味着您不能使用它。
只是一个小演示(没有任何实际目的)用于比较:当删除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;
}
我已经使用以下代码在 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
七十年代,在 C 语言最开始的时候,就引入了 register 关键字,以便程序员给编译器提示,告诉它这个变量会经常使用,明智的做法是将其值保存在处理器的内部寄存器之一中。
如今,优化器比程序员更有效地确定更有可能保存到寄存器中的变量,并且优化器并不总是考虑程序员的提示。
很多人错误地建议不要使用 register 关键字。
让我们看看为什么!
register 关键字有一个相关的副作用:您不能引用(获取地址)寄存器类型变量。
建议其他人不要使用寄存器的人错误地将此作为附加论点。
但是,知道不能获取寄存器变量的地址这一简单事实允许编译器(及其优化器)知道该变量的值不能通过指针间接修改。
当在指令流的某个点,一个寄存器变量的值被分配到处理器的寄存器中,并且该寄存器没有被使用,因为获取另一个变量的值,编译器知道它不需要重新加载该寄存器中变量的值。这允许避免昂贵的无用内存访问。
做你自己的测试,你会在最内部的循环中获得显着的性能改进。
寄存器会通知编译器,编码器认为该变量将被写入/读取到足以证明其存储在可供变量使用的少数寄存器之一中是合理的。从寄存器读取/写入通常更快,并且可能需要更小的操作码集。
如今,这不是很有用,因为大多数编译器的优化器在确定是否应将寄存器用于该变量以及使用多长时间方面都比您更好。
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 次内存读取(尽管实际上,如果在 中完成计算,这可能已经优化为 0 R/W esi
,这就是使用 C++ 发生的情况const register
)。不使用register
会导致 2 次写入和 1 次读取(尽管读取时会发生存储到加载转发)。这是因为该值必须存在并直接在堆栈上更新,因此可以通过地址(指针)读取正确的值。register
没有这个要求,不能指向。const
并且register
基本上volatile
与使用相反volatile
将覆盖文件和块范围的 const 优化以及块范围的register
优化。const register
并且register
将产生相同的输出,因为 const 在块范围内对 C 没有任何作用,因此仅register
适用于优化。
在铿锵声,register
被忽略,但const
优化仍然发生。
在受支持的 C 编译器上,它会尝试优化代码,以便将变量的值保存在实际的处理器寄存器中。
register
启用全局寄存器分配优化(/Oe 编译器标志)时,Microsoft 的 Visual C++ 编译器会忽略该关键字。
请参阅MSDN 上的注册关键字。
Register 关键字告诉编译器将特定变量存储在 CPU 寄存器中,以便可以快速访问它。从程序员的角度来看,寄存器关键字用于程序中大量使用的变量,以便编译器可以加速代码。虽然这取决于编译器是将变量保存在 CPU 寄存器还是主存储器中。
寄存器指示编译器通过将该特定变量存储在寄存器中然后存储在内存中来优化此代码。这是对编译器的请求,编译器可能会也可能不会考虑这个请求。如果您的某些变量被非常频繁地访问,您可以使用此工具。例如:循环。
另一件事是,如果您将变量声明为寄存器,那么您将无法获取其地址,因为它未存储在内存中。它在 CPU 寄存器中分配。
register 关键字是对编译器的请求,将指定的变量存储在处理器的寄存器而不是内存中,作为提高速度的一种方式,主要是因为它将被大量使用。编译器可能会忽略该请求。