如何知道变量在寄存器中或堆栈中?

How is it known that variables are in registers, or on stack?

我正在 isocpp FAQ 阅读关于 inline 的问题,代码为

void f()
{
  int x = /*...*/;
  int y = /*...*/;
  int z = /*...*/;
  // ...code that uses x, y and z...
  g(x, y, z);
  // ...more code that uses x, y and z...
 }

然后它说

Assuming a typical C++ implementation that has registers and a stack, the registers and parameters get written to the stack just before the call to g(), then the parameters get read from the stack inside g() and read again to restore the registers while g() returns to f(). But that’s a lot of unnecessary reading and writing, especially in cases when the compiler is able to use registers for variables x, y and z: each variable could get written twice (as a register and also as a parameter) and read twice (when used within g() and to restore the registers during the return to f()).

我很难理解上面的段落。我试着列出我的问题如下:

  1. 计算机要对驻留在主存中的某些数据进行运算,是否必须先将数据加载到某些寄存器中,然后CPU才能对数据进行运算? (我知道这个问题与 C++ 没有特别的关系,但了解这一点将有助于理解 C++ 的工作原理。)
  2. 我认为 f() 是一个函数,与 g(x, y, z) 是一个函数一样。为什么调用g()之前的x, y, z在寄存器中,g()传入的参数在栈中?
  3. 如何知道 x, y, z 的声明使它们存储在寄存器中? g()里面的数据存放在哪里,寄存器还是栈?

PS

我认为当答案都非常好时很难选择一个可接受的答案(例如,@MatsPeterson、@TheodorosChatzigiannakis 和@superultranova 提供的答案)。我个人更喜欢@Potatoswatter 的那个,因为答案提供了一些指导方针。

如果不查看汇编语言,您将无法知道变量是在寄存器、堆栈、堆、全局内存中还是其他地方。 变量是一个抽象概念。编译器可以选择使用寄存器或其他内存,只要不改变执行

还有一条规则影响了这个话题。如果您获取变量的地址并将其存储到指针中,则该变量可能不会放入寄存器中,因为寄存器没有地址。

变量存储也可能取决于编译器的优化设置。由于简化,变量可能会消失。不改变值的变量可以作为常量放入可执行文件中。

For a computer to do some operations on some data which are residing in the main memory, is it true that the data must be loaded to some registers first then the CPU can operate on the data?

这取决于体系结构及其提供的指令集。但在实践中,是的——这是典型的情况。

How is it known that the declarations for x, y, z make them stored in the registers? Where the data inside g() is stored, register or stack?

假设编译器不消除局部变量,它更愿意将它们放在寄存器中,因为寄存器比堆栈(驻留在主内存或缓存中)更快。

但这远非普遍真理:它取决于编译器的(复杂的)内部工作原理(其详细信息已在该段中进行了说明)。

I think f() is a function in a way the same as g(x, y, z) is a function. How come x, y, z before calling g() are in the registers, and the parameters passed in g() are on the stack?

即使我们假设变量实际上存储在寄存器中,当您调用函数时,calling convention 也会启动。这是描述如何调用函数的约定,其中传递参数,谁清理堆栈,保留哪些寄存器。

所有调用约定都有某种开销。这种开销的一个来源是参数传递。许多调用约定试图通过更喜欢通过寄存器传递参数来减少这种情况,但由于 CPU 寄存器的数量有限(与堆栈的 space 相比),它们最终会退回到通过寄存器传递多个参数后的堆栈。

您问题中的段落假定调用约定通过堆栈传递所有内容,并且基于该假设,它试图告诉您的是,如果我们能够 "copy" (在编译时)调用者内部被调用函数的主体(而不是发出对该函数的调用)。这在逻辑上会产生相同的结果,但会消除函数调用的运行时成本。

别把那段话当回事。好像是在做过多的假设,然后进入过多的细节,这确实不能一概而论。

但是,你的问题很好

  1. For a computer to do some operations on some data which are residing in the main memory, is it true that the data must be loaded to some registers first then the CPU can operate on the data? (I know this question is not particularly related to C++, but understanding this will be helpful to understand how C++ works.)

或多或少,一切都需要加载到寄存器中。大多数计算机都是围绕 数据路径 、连接寄存器、算术电路和内存层次结构顶层的总线组织的。通常,数据路径上广播的任何内容都由寄存器标识。

您可能还记得 RISC 与 CISC 的大辩论。其中一个关键点是,如果不允许内存直接连接到运算电路,计算机设计可以简单得多。

在现代计算机中,有架构寄存器,它们是像变量一样的编程结构,还有物理寄存器,它们是实际的电路。编译器在根据体系结构寄存器生成程序时会做很多繁重的工作来跟踪物理寄存器。对于像 x86 这样的 CISC 指令集,这可能涉及生成将内存中的操作数直接发送到算术运算的指令。但在幕后,它一直是寄存器。

底线:让编译器做它的事情。

  1. I think f() is a function in a way the same as g(x, y, z) is a function. How come x, y, z before calling g() are in the registers, and the parameters passed in g() are on the stack?

每个平台都定义了C函数相互调用的方式。在寄存器中传递参数效率更高。但是,需要权衡取舍,寄存器的总数是有限的。较旧的 ABI 通常会为了简单而牺牲效率,并将它们全部放在堆栈上。

底线:该示例任意假设一个朴素的 ABI。

  1. How is it known that the declarations for x, y, z make them stored in the registers? Where the data inside g() is stored, register or stack?

编译器倾向于将寄存器用于更频繁访问的值。示例中的任何内容都不需要使用堆栈。但是,访问频率较低的值将放在堆栈上,以提供更多寄存器。

只有当您获取变量的地址时,例如通过 &x 或通过引用传递,并且该地址转义了内联器,编译器才需要使用内存而不是寄存器。

底线:避免随意获取地址和passing/storing它们。

简答:你不能。这完全取决于您的编译器和启用的优化功能。

编译器关注的是将您的程序转换为汇编,但它是如何完成的与您的编译器的工作方式紧密相关。 一些编译器允许您提示要注册的变量映射。 例如检查:https://gcc.gnu.org/onlinedocs/gcc/Global-Reg-Vars.html

您的编译器将对您的代码应用转换以获得某些东西,可能是性能,可能是更小的代码大小,并且它应用成本函数来估计这些收益,因此您通常只能看到反汇编编译后的结果单位.

变量几乎总是存储在主存中。很多时候,由于编译器优化,你声明的变量的值永远不会移动到主内存,但那些是你在你的方法中使用的中间变量,在调用任何其他方法之前不相关(即发生堆栈操作)。

这是设计使然 - 以提高性能,因为处理器更容易(并且更快)寻址和操作寄存器中的数据。建筑寄存器的大小有限,因此不能将所有内容都放入寄存器中。即使你 'hint' 你的编译器将它放在寄存器中,最终,如果可用寄存器已满,OS 可能会在主内存中的寄存器外管理它。

最有可能的是,一个变量将在主内存中,因为它在 near 执行中具有进一步的相关性,并且可能会在 CPU 时间的更长时间内保持依赖。一个变量在体系结构寄存器中,因为它与即将到来的机器指令相关,并且执行几乎立即但可能不会长期相关。

变量是存储在内存中还是寄存器中[或者在某些情况下不止一个寄存器](以及你给编译器的选项,假设它有决定这些事情的选项——大多数 "good" 编译器都有)。例如,LLVM/Clang 编译器使用称为 "mem2reg" 的特定优化过程,将变量从内存移动到寄存器。这样做的决定取决于变量的使用方式——例如,如果您在某个时候获取变量的地址,它需要在内存中。

其他编译器具有类似但不一定相同的功能。

此外,至少在具有某种可移植性的编译器中,还将有一个为实际目标生成机器代码的阶段,其中包含特定于目标的优化,它再次可以将变量从内存移动到登记。

[不了解特定编译器的工作原理]不可能确定代码中的变量是在寄存器中还是在内存中。可以猜测,但这样的猜测就像猜测其他 "kind of predictable things",就像看着 window 猜测几个小时后是否会下雨 - 取决于你住的地方,这可能是一个完全随机猜测,或者完全可以预测——一些热带国家,你可以根据每天下午下雨的时间来设置你的手表,在其他国家,很少下雨,而在一些国家,比如在英国,你无法知道某些超出 "right now it is [not] raining right here".

回答实际问题:

  1. 这取决于处理器。正确的 RISC 处理器,如 ARM、MIPS、29K 等,除了加载和存储类型指令外,没有使用内存操作数的指令。因此,如果需要将两个值相加,则需要将值加载到寄存器中,并对这些寄存器使用加法操作。有的,比如x86和68K允许两个操作数之一是内存操作数,又比如PDP-11和VAX有"full freedom",无论你的操作数是在内存还是寄存器,都可以使用相同的指令,只是不同操作数的不同寻址模式。
  2. 这里您的原始前提是错误的 - 不能保证 g 的参数在堆栈中。这只是众多选择之一。许多 ABI(应用程序二进制接口,也称为“调用约定”)使用寄存器作为函数的前几个参数。因此,这再次取决于编译器(在某种程度上)和编译器针对的处理器(远不止是哪个编译器)参数是在内存中还是在寄存器中。
  3. 同样,这是编译器做出的决定 - 它取决于处理器有多少寄存器,哪些可用,如果 "freeing" 一些寄存器用于 x,成本是多少,yz - 范围从 "no cost at all" 到 "quite a bit" - 同样取决于处理器型号和 ABI。

For a computer to do some operations on some data which are residing in the main memory, is it true that the data must be loaded to some registers first then the CPU can operate on the data?

甚至这个说法也不总是正确的。对于您将要使用的所有平台来说,这可能都是正确的,但肯定会有另一种架构根本不使用 processor registers

但是您的 x86_64 计算机可以。

I think f() is a function in a way the same as g(x, y, z) is a function. How come x, y, z before calling g() are in the registers, and the parameters passed in g() are on the stack?

How is it known that the declarations for x, y, z make them stored in the registers? Where the data inside g() is stored, register or stack?

对于您的代码将在其上编译的任何编译器和系统,这两个问题都无法得到唯一的回答。它们甚至不能被认为是理所当然的,因为 g 的参数可能不在堆栈上,这完全取决于我将在下面解释的几个概念。

首先你应该知道所谓的calling conventions which define, among the other things, how function parameters are passed (e.g. pushed on the stack, placed in registers, or a mix of both). This isn't enforced by the C++ standard and calling conventions are a part of the ABI,这是一个关于低级机器代码程序问题的更广泛的话题。

其次register allocation (i.e. which variables are actually loaded in a register at any given time) is a complex task and a NP-complete problem. Compilers try to do their best with the information they have. In general less frequently accessed variables are put on the stack while more frequently accessed variables are kept on registers. Thus the part Where the data inside g() is stored, register or stack? cannot be answered once-and-for-all since it depends on many factors including register pressure.

更不用说编译器优化,它甚至可能消除对某些变量的需求。

最后,您链接的问题已经说明了

Naturally your mileage may vary, and there are a zillion variables that are outside the scope of this particular FAQ, but the above serves as an example of the sorts of things that can happen with procedural integration.

即您发布的段落做了一些假设来设置示例。这些只是假设,您应该这样对待它们。

作为一个小补充:关于 inline 对函数的好处,我建议看一下这个答案:

关于您的第 1 个问题,是的,非 load/store 指令对寄存器进行操作。

关于你的 #2 问题,如果我们假设参数是在堆栈上传递的,那么我们必须将寄存器写入堆栈,否则 g() 将无法访问数据,因为g() 中的代码没有 "know" 注册参数。

关于您的第 3 个问题,我们不知道 x、y 和 z 肯定会存储在 f() 的寄存器中。可以使用 register 关键字,但这更像是一种建议。根据调用约定,假设编译器不做任何涉及参数传递的优化,您可以预测参数是在堆栈上还是在寄存器中。

您应该熟悉调用约定。调用约定处理将参数传递给函数的方式,通常涉及以指定顺序在堆栈上传递参数、将参数放入寄存器或两者的组合。

stdcallcdeclfastcall 是调用约定的一些示例。在参数传递方面,stdcall 和cdecl 是一样的,都是按从右到左的顺序将参数压入栈中。在这种情况下,如果 g()cdeclstdcall 调用者将按顺序推送 z,y,x:

mov eax, z
push eax
mov eax, x
push eax
mov eax, y
push eax
call g

在 64 位 fastcall 中,使用寄存器,微软使用 RCX、RDX、R8、R9(加上需要 4 个以上参数的函数的堆栈),linux 使用 RDI、RSI、RDX、RCX、R8 ,R9。要使用 MS 64 位 fastcall 调用 g(),可以执行以下操作(我们假设 zxy 不在寄存器中)

mov rcx, x
mov rdx, y
mov r8, z
call g

这就是人类(有时是编译器)编写汇编的方式。编译器会使用一些技巧来避免传递参数,因为它通常会减少指令数量并减少访问内存的时间。以下面的代码为例(我故意忽略了非易失性寄存器规则):

f:
xor rcx, rcx
mov rsi, x
mov r8, z
mov rdx y
call g
mov rcx, rax
ret

g:
mov rax, rsi
add rax, rcx
add rax, rdx
ret

出于说明目的,rcx 已在使用中,并且 x 已加载到 rsi 中。编译器可以编译 g,使其使用 rsi 而不是 rcx,因此在调用 g 时不必在两个寄存器之间交换值。编译器还可以内联 g,因为 f 和 g 共享用于 x、y 和 z 的同一组寄存器。在这种情况下,call g 指令将替换为 g 的内容,不包括 ret 指令。

f:
xor rcx, rcx
mov rsi, x
mov r8, z
mov rdx y
mov rax, rsi
add rax, rcx
add rax, rdx
mov rcx, rax
ret

这会更快,因为我们不必处理 call 指令,因为 g 已经内联到 f.