当使用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中讨论的一元和运算符还是隐含地使用(通过使用Unary&Operator)或隐含(通过将数组名称转换为指针,如6.3.2.1所述)。因此,唯一可以应用于使用存储类规范寄存器声明的数组的操作员是大小。

除非您在8位AVR或图片上闲逛,否则编译器可能会嘲笑您认为您最了解并忽略您的请求。即使在他们上,我也认为我知道了几次,并找到了欺骗编译器的方法(带有一些内联ASM),但是我的代码爆炸了,因为它必须按摩大量其他数据才能解决我的固执。

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

注意:我的意思是:“当地人”:标量变量,存储类自动(或“寄存器”),这些变量从未用作'&'的操作数。编译器有时也可以将自动结构,工会或数组分解为单个“本地”变量。

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

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分达到11岁,并继续前进。为了使此工作起作用,编译器无法保留 任何事物 在语句之间的寄存器中,除了分配给特定寄存器的变量外;在那种情况下,调试器被告知这一点。而且它不能试图“知道”变量的值,因为调试器可以改变它们。换句话说,如果您想在调试时更改本地变量,则需要1985年的情况。

现代编译器通常会编译如下:

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

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

  a = b*c + 2*k;

变成类似

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

(3)编译器然后采取所有这些操作,并寻找常见的子表达式等。由于每个新寄存器仅写一次,因此在保持正确性的同时重新排列它们是相当容易的。我什至不会开始循环分析。

(4)编译器然后尝试将所有这些虚拟寄存器映射到实际寄存器中以生成代码。由于每个虚拟寄存器的寿命有限,因此只需要在上述中重复使用实际寄存器 - 只有在生成“ a”的添加之前,才需要将其保存在与“ A”相同的寄存器中。如果寄存器没有足够。在装载机的机器上,只有寄存器中的值才能在计算中使用,第二种策略可以很好地设计。

从以上,这应该清楚:很容易确定映射到的虚拟寄存器 factor 与常数“ 8”相同,因此所有乘以 factor 是乘以8。即使 factor 稍后修改,这是一个“新”变量,它不会影响 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”将与Inital“ ACC”相同。因此,“ ACC0 = ACC”不需要寄存器副本。另一点:“结果”被分配给两次,并且由于第二个作业忽略了上一个值,因此代码中基本上有两个不同的“结果”变量,并且很容易通过分析确定。

所有这一切的含义是:如果代码更易于遵循代码,请不要犹豫使用其他当地人将复杂表达式分解为较小的表达式。基本上有零运行时间罚款,因为优化器无论如何都会看到同样的东西。

如果您有兴趣了解更多信息,则可以从这里开始: 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);
        }
    }
}

这看起来可能完全无害。但是,如果您担心执行速度,请考虑以下操作:编译器正在通过 hwget_hw, ,然后呼叫 generate_func. 。让我们假设编译器对这些功能中的内容一无所知(这是一般情况)。编译器 必须 假设呼叫 generate_func 可能正在改变 h 或者 w. 。这是对指针传递给的完全合法使用 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建议使用“寄存器”关键字作为标记密钥变量的一种抑制使用的方式 & 在它们上,例如,其他维护代码的人(不会影响生成的代码,因为编译器可以确定缺乏 & 没有它)。就我而言,我总是在申请之前仔细考虑 & 在代码的时间关键时期区域中的任何本地标量,我在我的角度使用“寄存器”来强制执行这是有点密码,但是我可以看到这一点(不幸的是,它在C ++中不起作用,因为编译器只会只是忽略“寄存器”)。

顺便说一下,就代码效率而言,获得函数返回两个值的最佳方法是与一个结构:

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;
    ...

这看起来可能很麻烦(编写很麻烦),但是比以前的示例会生成更干净的代码。实际上,“结构HW”将在两个寄存器中返回(无论如何在大多数现代ABIS上)。由于使用“ HWVAL”结构的方式,优化器将有效地将其分解为两个“当地人” hwval.hhwval.w, ,然后确定这些等同于 hw - 所以 hwval 代码本质上将消失。无需传递指针,没有函数通过指针修改另一个函数的变量。就像拥有两个不同的标量返回值一样。现在在C ++ 11中更容易做到 - std::tiestd::tuple, ,您可以使用此方法的详细性(并且不必编写结构定义)。

您的第二个例子在C中无效。因此,您可以很好地看到 register 关键字会更改某些内容(在C中)。只是为此目的,抑制变量的地址。因此,只是不要口头以“寄存器”的名字“注册”,这是一个错误的名称,而是坚持其定义。

C ++似乎忽略了 register, 好吧,他们必须有自己的理由,但是我发现再次找到这些微妙的差异之一,其中一个有效的代码对另一个有效的代码无效,这让我感到很难过。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top