gcc / g ++可以告诉我何时忽略了我的注册?

使用gcc / g ++编译C / C ++代码时,如果它忽略了我的寄存器,它可以告诉我吗? 例如,在此代码中

int main() { register int j; int k; for(k = 0; k < 1000; k++) for(j = 0; j < 32000; j++) ; return 0; } 

j将用作寄存器,但在此代码中

 int main() { register int j; int k; for(k = 0; k < 1000; k++) for(j = 0; j < 32000; j++) ; int * a = &j; return 0; } 

j将是一个正常变量。 它能告诉我使用寄存器的变量是否真的存储在CPU寄存器中?

你可以公平地假设GCC忽略了register关键字,除了可能在-O0 。 但是,它不应该以某种方式产生影响,如果你处于这样的深度,你应该已经阅读了汇编代码。

以下是有关此主题的信息主题: http : //gcc.gnu.org/ml/gcc/2010-05/msg00098.html 。 回到过去, register确实帮助编译器将一个变量分配到寄存器中,但是今天的寄存器分配可以在没有提示的情况下自动完成。 该关键字在C中继续有两个用途:

  1. 在C中,它会阻止您获取变量的地址。 由于寄存器没有地址,因此这种限制可以帮助简单的C编译器。 (简单的C ++编译器不存在。)
  2. register对象不能声明为restrict 。 因为restrict属于地址,它们的交集是没有意义的。 (C ++还没有restrict ,无论如何,这个规则有点微不足道。)

对于C ++,自C ++ 11以来,该关键字已被弃用,并建议从2017年的标准修订版中删除 。

一些编译器在参数声明上使用了register来确定函数的调用约定,ABI允许混合的基于堆栈和寄存器的参数。 这似乎是不一致的,它往往会出现像register("A1")这样的扩展语法,我不知道是否还在使用这样的编译器。

关于现代编译和优化技术, register注释根本没有任何意义。 在你的第二个程序中,你取j的地址,寄存器没有地址,但是一个相同的本地或静态变量可以很好地存储在它的生命周期中的两个不同的存储单元中,或者有时存储在存储器中,有时存储在寄存器中,或者根本不存在。 实际上,优化编译器会将嵌套循环编译为空,因为它们没有任何效果,只是将它们的最终值分配给kj 。 然后省略这些分配,因为剩下的代码不使用这些值。

您无法在C中获取寄存器的地址,而且编译器完全可以忽略您; C99标准,第6.7.1节(pdf) :

实现可以将任何注册声明简单地视为自动声明。 但是,无论是否实际使用了可寻址存储,使用存储类说明符寄存器声明的对象的任何部分的地址都无法显式(通过使用6.5.3.2中讨论的一元和运算符)或隐式计算(通过将数组名称转换为指针,如6.3.2.1中所述。 因此,可以应用于使用存储类说明符寄存器声明的数组的唯一运算符是sizeof。

除非您正在研究8位AVR或PIC,否则编译器可能会嘲笑您认为自己最了解并忽略了您的请求。 即使在他们身上,我也认为我已经知道了几次并找到了欺骗编译器的方法(使用一些内联asm),但是我的代码爆炸了,因为它必须按下一堆其他数据来解决我的固执问题。

这个问题,以及一些答案,以及我见过的’register’关键字的其他几个讨论 – 似乎隐含地假设所有本地都被映射到特定寄存器或堆栈上的特定内存位置。 直到15 – 25年前,这通常都是正确的,如果你关闭优化就是如此,但在执行标准优化时根本不是这样。 现在,优化器将本地视为用于描述数据流的符号名称,而不是需要存储在特定位置的值。

注意:’locals’在这里我指的是:存储类auto(或’register’)的标量变量,它们从未用作’&’的操作数。 编译器有时也可以将自动结构,联合或数组分解为单独的“本地”变量。

为了说明这一点:假设我在函数的顶部写这个:

 int factor = 8; 

..然后, factor变量的唯一用途是乘以各种各样的东西:

 arr[i + factor*j] = arr[i - factor*k]; 

在这种情况下 – 如果你想要尝试 – 没有factor变量。 代码分析将显示factor始终为8,因此所有的变化将变为<<3 。 如果你在1985 C中做了同样的事情, factor会在堆栈上得到一个位置,并且会有多个,因为编译器基本上一次只处理一个语句而且不记得有关变量值的任何内容。 当时程序员更有可能使用#define factor 8在这种情况下获得更好的代码,同时保持可调factor

如果你使用-O0 (优化关闭) - 你确实会获得factor的变量。 例如,这将允许您跳过factor=8语句,然后使用调试器将factor更改为11,并继续。 为了使其工作,编译器不能在语句之间保留任何寄存器,除了分配给特定寄存器的变量; 在这种情况下,调试器会收到通知。 并且它无法尝试“了解”有关变量值的任何信息,因为调试器可以更改它们。 换句话说,如果要在调试时更改局部变量,则需要1985年的情况。

现代编译器通常编译如下函数:

(1)当在函数中多次分配局部变量时,编译器会创建变量的不同“版本”,以便每个变量仅在一个位置分配。 变量的所有“读取”都指特定版本。

(2)将这些本地人中的每一个分配给“虚拟”寄存器。 中间计算结果也分配了变量/寄存器; 所以

  a = b*c + 2*k; 

变得像

  t1 = b*c; t2 = 2; t3 = k*t2; a = t1 + t3; 

(3)然后编译器接受所有这些操作,并查找公共子表达式等。由于每个新寄存器只写一次,因此在保持正确性的同时重新排列它们要容易得多。 我甚至不会开始循环分析。

(4)然后编译器尝试将所有这些虚拟寄存器映射到实际寄存器中以生成代码。 由于每个虚拟寄存器的寿命有限,因此可以大量重用实际的寄存器 - 上面的“t1”仅在生成“a”的add之前需要,因此它可以保存在与“a”相同的寄存器中。 当没有足够的寄存器时,一些虚拟寄存器可以分配给存储器 - 或者 - 一个值可以保存在某个寄存器中,存储到存储器一段时间,然后加载回(可能)不同的寄存器中。 在加载存储机器上,只有寄存器中的值可以用于计算,第二种策略可以很好地适应这种情况。

从上面可以看出,这很容易:很容易确定映射到factor的虚拟寄存器与常数'8'相同,因此所有乘以factor的乘法都乘以8.即使factor被修改,也就是a 'new'变量并不影响factor先前使用。

另一个含义,如果你写

  vara = varb; 

..代码中可能存在或可能不存在相应的副本。 例如

 int *resultp= ... int acc = arr[0] + arr[1]; int acc0 = acc; // save this for later int more = func(resultp,3)+ func(resultp,-3); acc += more; // add some more stuff if( ...){ resultp = getptr(); resultp[0] = acc0; resultp[1] = acc; } 

在上面,acc的两个“版本”(初始和添加“更多”之后)可以在两个不同的寄存器中,然后'acc0'将与初始'acc'相同。 因此'acc0 = acc'不需要注册副本。 另一点:'resultp'被分配两次,并且由于第二个赋值忽略了前一个值,代码中基本上有两个不同的'resultp'变量,这很容易通过分析确定。

所有这一切的含义:如果使代码更容易理解,不要犹豫是否要使用额外的本地化中间体将复杂的表达式分解为更小的表达式。 对此,基本上没有运行时间惩罚,因为优化器无论如何都会看到同样的事情。

如果您有兴趣了解更多信息,可以从这里开始: http : //en.wikipedia.org/wiki/Static_single_assignment_form

这个答案的要点是(a)给出一些关于现代编译器如何工作的概念;(b)指出要求编译器,如果它是如此友好,将特定的局部变量放入寄存器 - 不真有意义。 优化器可以将每个“变量”视为几个变量,其中一些可能在循环中大量使用,而其他变量则不然。 一些变量将消失 - 例如通过保持不变; 或者,有时候,交换中使用的临时变量。 或者没有实际使用的计算。 编译器可以在代码的不同部分使用相同的寄存器,根据您正在编译的机器上的最佳实际情况。

暗示编译器关于哪些变量应该在寄存器中的概念假定每个局部变量映射到寄存器或存储器位置。 当Kernighan + Ritchie设计C语言时,情况确实如此,但事实并非如此。

关于你不能获取寄存器变量地址的限制:显然,没有办法实现获取寄存器中保存的变量的地址,但你可能会问 - 因为编译器可以自行忽略'寄存器' - 为什么这个规则到位了? 如果碰巧拿地址,为什么编译器不能忽略'寄存器'? (与C ++中的情况一样)。

同样,你必须回到旧的编译器。 原始K + R编译器将解析局部变量声明,然后立即决定是否将其分配给寄存器(如果是,则指定哪个寄存器)。 然后它将继续编译表达式,一次为每个语句发出汇编程序。 如果它后来发现你正在使用已经分配给寄存器的“寄存器”变量的地址,那么就没有办法处理它,因为那时分配通常是不可逆转的。 但是,可能会生成错误消息并停止编译。

最重要的是,“注册”似乎已经过时了:

  • C ++编译器完全忽略它
  • C编译器忽略它,除了强制执行关于&的限制&并且可能不在-O0忽略它,它实际上可以导致按请求分配。 在-O0你不关心代码速度。

因此,它现在基本上存在向后兼容性,并且可能基于某些实现仍然可以将其用于“提示”。 我从不使用它 - 我编写实时DSP代码,花费大量时间查看生成的代码并找到使其更快的方法。 有许多方法可以修改代码以使其运行更快,并且了解编译器的工作原理非常有用。 自从我上次发现在这些方面添加“注册”以来,已经很长时间了。


附录

我在上面排除了我对“本地人”的特殊定义,应用&应用的变量(这些当然包含在通常意义上的术语中)。

考虑以下代码:

 void somefunc() { int h,w; int i,j; extern int pitch; get_hw( &h,&w ); // get shape of array for( int i = 0; i < h; i++ ){ for( int j = 0; j < w; j++ ){ Arr[i*pitch + j] = generate_func(i,j); } } } 

这可能看起来完全无害。 但是如果你担心执行速度,请考虑这一点:编译器将hw的地址传递给get_hw ,然后调用generate_func 。 让我们假设编译器对这些函数中的内容一无所知(这是一般情况)。 编译器必须假定对generate_func的调用可能正在改变hw 。 这是传递给get_hw的指针的完全合法用法 - 你可以将它存储在某个地方,然后在以后使用它,只要包含h,w的范围仍在使用中,以读取或写入这些变量。

因此,编译器必须将hw存储在堆栈的内存中,并且无法预先确定循环将运行多长时间。 所以某些优化是不可能的,因此循环可能效率较低(在这个例子中,内部循环中有一个函数调用,所以它可能没有太大的区别,但考虑有一个函数的情况) 偶尔会在内循环中调用,具体取决于某些条件)。

这里的另一个问题是generate_func可以改变pitch ,因此每次都需要完成i*pitch ,而不是仅仅在i改变时。

它可以被记录为:

 void somefunc() { int h0,w0; int h,w; int i,j; extern int pitch; int apit = pitch; get_hw( &h0,&w0 ); // get shape of array h= h0; w= w0; for( int i = 0; i < h; i++ ){ for( int j = 0; j < w; j++ ){ Arr[i*apit + j] = generate_func(i,j); } } } 

现在变量apit,h,w都是我在上面定义的意义上的“安全”本地,并且编译器可以确定它们不会被任何函数调用更改。 假设我没有generate_func修改任何内容,代码将具有与以前相同的效果,但可能更有效。

Jens Gustedt建议使用'register'关键字作为标记关键变量的方式,以禁止在其上使用& ,例如由其他人维护代码(它不会影响生成的代码,因为编译器可以确定缺乏&没有它)。 就我而言,我总是在应用代码的时间关键区域中的任何本地标量之前仔细考虑,并且在我看来使用“注册”来强制执行此操作有点神秘,但我可以看到这一点(不幸的是它因为编译器只会忽略'register',所以在C ++中不起作用。

顺便提一下,在代码效率方面,让函数返回两个值的最好方法是使用struct:

 struct hw { // this is what get_hw returns int h,w; }; void somefunc() { int h,w; int i,j; struct hw hwval = get_hw(); // get shape of array h = hwval.h; w = hwval.w; ... 

这可能看起来很麻烦(编写起来很麻烦),但它会产生比前面的例子更清晰的代码。 'struct hw'实际上将在两个寄存器中返回(无论如何,在大多数现代ABI上)。 由于使用'hwval'结构的方式,优化器将有效地将其分解为两个'本地'' hwval.hhwval.w ,然后确定它们等同于hw - 所以hwval将基本上消失在代码中。 没有指针需要传递,没有函数通过指针修改另一个函数的变量; 它就像有两个不同的标量返回值。 这在C ++ 11中更容易实现 - 使用std::tiestd::tuple ,您可以使用此方法,而不需要编写结构定义。

你的第二个例子在C中是无效的。所以你很清楚register关键字改变了某些东西(在C中)。 它就是为了这个目的,禁止获取变量的地址。 因此,不要将其名称“注册”,这是一个用词不当,但坚持其定义。

那个C ++似乎忽略了register ,他们必须有他们的理由,但是我觉得再次找到其中一个有效代码对另一个无效的那些细微差别有点难过。