编译器决定放入寄存器的变量的运算符地址会怎样?

What happens with address of operator for variables that the compiler decides to put in registers?

我知道在 C 中,如果用 register 关键字显式指定变量,则不能在其上使用 & 运算符,这对我来说很有意义,因为没有这样的作为始终保存在寄存器中的变量的“地址”。

我的问题是,如果编译器自行决定将变量存储在寄存器中而不是将其溢出,那么在代码执行期间 & 运算符会发生什么情况?

我可以想到编译器可能会尝试处理此问题的两种方式:

  1. 尝试模仿 & 的行为,但这看起来很毛茸茸,我不知道如何严格有效地做到这一点。
  2. 如果 & 运算符与此变量一起使用,则始终溢出该变量。

在这种情况下,C 是采用其中一种方法还是采用其他方法?

变量是否在硬件寄存器中实现这一事实必须对用户完全透明。由编译器来实现这一点,以便始终可以通过指针访问当前值。

当用户决定使用关键字 register 声明变量时,情况会发生变化。那么 & 运算符的使用就被简单地禁止了。

首先请注意,非register变量在优化过程中发生的情况超出了C语言的范围。还有另一种选择:从机器代码中完全删除变量。

My question is, if the compiler decides on its own to store a variable in register rather than spilling it, then what happens with the & operator during code execution?

如果编译器发现存在 & 运算符,则它不太可能将变量放入寄存器中。事实上,我曾经使用过的每个真实世界的编译器都会将这样的变量放在堆栈或静态存储内存中,从而使其可寻址。

在解析 C 代码时尽快生成程序集的流式编译器(例如 tinycc)根本不会将变量放入寄存器,除非这些变量具有 register 存储 class 说明符。另一方面,构建抽象语法树的非流式编译器只有在看到整个块后才能决定是否将某些内容放入寄存器中。然后它可以确定永远不需要变量的地址(或者所有通过变量地址访问变量都可以优化为直接寄存器访问)。

由于 C 无法在运行时在其静态提供的代码的上下文中解释动态输入的 C 代码片段(没有 _Eval(read_string()) 用户可以输入 printf("%p\n",(void*)&some_local);),所以没有任何惊喜在运行时获取地址。 C 编译器完成一个块后,它知道其中的每个局部变量将如何使用。

我不是编译器专家,但我的印象是恰恰相反。首先,编译器会尝试优化代码。如果在优化之后,不需要变量的地址,那么它是一个候选者,可以放在寄存器中(或者可能完全优化不存在)。

当然,如果 & 运算符根本没有应用于变量,那么它肯定是一个候选者。但即使 &x 确实出现在源代码中,对地址的需求在优化后可能会消失。

作为一个简单的例子,如果我们有

int x = 7;
foo(*&x);

编译器可以看出 *&x 完全等同于 x,因此可以将代码视为只是 foo(x)。如果x的地址没有被取到别的地方,那么它就根本不需要地址了,可以放到一个寄存器中。

现在您可以想象将这种分析扩展到更复杂的代码。

    int x = foo1(), y = foo2();
    int *p;
    p = cond ? &x : &y;
    return *p;

Try it on godbolt

从概念上讲,这可以依次重写为

return *(cond ? &x : &y);
return cond ? *&x : *&y;
return cond ? x : y;

现在 x,y 不再需要地址,p 根本不再需要存在。

换句话说,编译器不会尝试“模拟”& 运算符;相反,它会尝试重组代码,以便根本不需要它。

最常见的不可能的情况是变量的地址被传递给另一个函数。

int x;
foo(&x);

除非 foo 可以内联或可以使用其他类型的过程间分析,否则编译器确实必须将某些内容的地址传递给 foo,因此 x 有存在于记忆中,至少在那一刻。当然,编译器可以选择立即将其移入寄存器,如果不再需要它的地址,则在函数的其余部分将其保留在那里;变量是存在于内存中还是存在于寄存器中的问题不需要一直固定。