如何知道变量是在寄存器中,还是在堆栈上?

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

本文关键字:堆栈 寄存器 何知道 变量      更新时间:2023-10-16

我正在阅读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...
 }

那么它说

假设一个典型的c++实现有寄存器和堆栈,寄存器和参数被写入堆栈之前调用g(),然后从堆栈内部读取参数g()并再次读取以恢复寄存器,而g()返回到f()。但这是很多不必要的阅读和写作,尤其是在编译器能够为变量x使用寄存器的情况下,yz:每个变量可以被写入两次(作为寄存器和也可以作为参数)并读取两次(在g()和to中使用时)在返回f()期间恢复寄存器)。

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

  1. 对于计算机对驻留在主内存中的一些数据进行一些操作,是否必须首先将数据加载到某些寄存器,然后CPU才能对数据进行操作?(我知道这个问题与c++没有特别的关系,但是理解它将有助于理解c++是如何工作的。)
  2. 我认为f()是一个功能在某种程度上与g(x, y, z)是一个功能相同。为什么x, y, z在调用g()之前都在寄存器中,而g()中传递的参数都在堆栈上?
  3. 如何知道x, y, z的声明使它们存储在寄存器中?g()内部的数据存储在哪里,寄存器还是堆栈?

p

当答案都很好的时候,选择一个可以接受的答案是非常困难的。我想是由@MatsPeterson, @TheodorosChatzigiannakis和@superultranova提供的。我个人更喜欢@Potatoswatter的问题,因为答案提供了一些指导。

别把这段话看得太认真了。它似乎做了过多的假设,然后进入过多的细节,这是不能真正概括的。

但是,你的问题非常好。

  1. 对于计算机对驻留在主内存中的一些数据进行一些操作,是否必须首先将数据加载到某些寄存器,然后CPU才能对数据进行操作?(我知道这个问题与c++没有特别的关系,但是理解它将有助于理解c++是如何工作的。)
或多或少,所有东西都需要加载到寄存器中。大多数计算机都是围绕一条数据路径(em)、一条连接寄存器、算术电路和存储器层次结构顶层的总线来组织的。通常,在数据路径上广播的任何内容都是用寄存器标识的。

你可能还记得RISC和CISC的争论。其中一个要点是,如果存储器不允许直接与算术电路连接,计算机设计可以简单得多。

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

底线:让编译器做它该做的。

  • 我认为f()是一个函数,在某种程度上与g(x, y, z)是一个函数相同。为什么在调用g()之前的x, y, z在寄存器中,而在g()中传递的参数在堆栈中?
  • 每个平台都定义了C函数相互调用的方式。在寄存器中传递参数效率更高。但是,存在权衡,并且寄存器的总数是有限的。旧的abi往往为了简单而牺牲效率,并将它们全部放在堆栈上。

    底线:这个例子任意地假设了一个朴素的ABI。

  • 如何知道x, y, z的声明使它们存储在寄存器中?g()中的数据存储在哪里,寄存器还是堆栈?
  • 编译器倾向于为访问频率更高的值使用寄存器。本例中不需要使用堆栈。然而,访问频率较低的值将被放在堆栈上,以使更多的寄存器可用。

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

    底线:避免获取地址并随意传递/存储它们

    这完全取决于编译器(与处理器类型一起),变量是存储在内存中还是存储在寄存器中[或者在某些情况下存储在多个寄存器中](以及您给编译器的选项,假设它有决定这些事情的选项-大多数"好的"编译器都有)。例如,LLVM/Clang编译器使用名为"mem2reg"的特定优化通道,将变量从内存移动到寄存器。这样做的决定是基于变量的使用方式—例如,如果您在某个点获取变量的地址,则需要将其存储在内存中。

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

    同样,至少在具有可移植性的编译器中,也会有一个为实际目标生成机器码的阶段,其中包含特定于目标的优化,这再次可以将变量从内存移动到寄存器。

    如果不了解特定编译器的工作原理,就不可能确定代码中的变量是在寄存器中还是在内存中。人们可以猜测,但这种猜测就像猜测其他"可预测的事情"一样,比如看着窗外猜测几小时后是否会下雨——这取决于你住在哪里,这可能是一个完全随机的猜测,也可能是相当可预测的——一些热带国家,你可以根据每天下午下雨的时间来设定你的手表,在其他国家,很少下雨,在一些国家,比如英国,除了"现在这里没有下雨"之外,你无法确定。

    回答实际问题:

    1. 这取决于处理器。适当的RISC处理器,如ARM, MIPS, 29K等,除了加载和存储类型指令外,没有使用内存操作数的指令。如果你需要添加两个值,你需要将值加载到寄存器中,并在那些寄存器上使用add操作。有些,如x86和68K允许两个操作数中的一个是内存操作数,例如PDP-11和VAX有"完全自由",无论你的操作数是在内存还是寄存器中,你都可以使用相同的指令,只是不同的寻址模式为不同的操作数。
    2. 你原来的前提是错误的——不能保证g的参数在堆栈上。这只是众多选择之一。许多abi(应用程序二进制接口,又名"调用约定")使用寄存器作为函数的前几个参数。因此,它再次取决于编译器(在某种程度上)和编译器针对的处理器(远远超过哪个编译器),无论参数是在内存中还是在寄存器中。
    3. 再一次,这是编译器做出的决定——这取决于处理器有多少寄存器,它们是可用的,如果为x, yz"释放"一些寄存器的成本是多少——范围从"完全没有成本"到"相当多"——再次取决于处理器模型和ABI。

    对于计算机对驻留在主存中的一些数据进行一些操作,是否必须首先将数据加载到某些寄存器中,然后CPU才能对数据进行操作?

    即使这个陈述也不总是正确的。这可能适用于你将要使用的所有平台,但肯定会有另一种根本不使用处理器寄存器的架构。

    您的x86_64计算机却可以。

    我认为f()是一个函数,就像g(x, y, z)是一个函数一样。为什么在调用g()之前的x, y, z在寄存器中,而在g()中传递的参数在堆栈中?

    如何知道x, y, z的声明使它们存储在寄存器中?g()中的数据存储在哪里,寄存器还是堆栈?

    对于编译代码的任何编译器和系统,

    这两个问题都不可能有唯一的答案。它们甚至不能被认为是理所当然的,因为g的参数可能不在堆栈上,这一切都取决于我将在下面解释的几个概念。

    首先,你应该知道所谓的调用约定,它定义了函数参数是如何传递的(例如,在堆栈上压入,放在寄存器中,或者两者的混合)。这不是c++标准强制的,调用约定是ABI的一部分,这是一个关于低级机器代码程序问题的更广泛的主题。

    其次,寄存器分配(即在任何给定的时间,哪些变量实际上被加载到寄存器中)是一个复杂的任务,也是一个np完全问题。编译器会尽力利用他们所拥有的信息。通常,访问频率较低的变量放在堆栈中,而访问频率较高的变量保存在寄存器中。因此,部分Where the data inside g() is stored, register or stack?不能一次性回答,因为它取决于许多因素,包括寄存器压力。

    更不用说编译器优化,它甚至可以消除一些变量的需要。

    最后你链接的问题已经说了

    当然,您的经验可能会有所不同,并且有无数的变量超出了这个特定的FAQ的范围,但上面的例子可以作为过程集成中可能发生的各种事情的示例。

    。你张贴的段落做了一些假设来设置一个例子。这些只是假设,你应该这样对待它们。

    作为一个小补充:关于inline对函数的好处,我建议看看这个答案:https://stackoverflow.com/a/145952/1938163

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

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

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

    关于你的第一个问题,是的,非加载/存储指令在寄存器上操作。

    关于你的第2个问题,如果我们假设参数在堆栈上传递,那么我们必须将寄存器写入堆栈,否则g()将无法访问数据,因为g()中的代码不"知道"参数在哪个寄存器中。

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

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

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

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

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

    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。

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

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

    你的编译器将应用转换到你的代码,以获得一些东西,可能是性能,可能是更低的代码大小,它应用成本函数来估计这种收益,所以你通常只能看到结果反汇编单元。

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

    这是为了提高性能而设计的,因为处理器更容易(也更快)在寄存器中寻址和操作数据。架构寄存器的大小是有限的,所以不能把所有东西都放到寄存器中。即使你"提示"你的编译器把它放在寄存器中,最终,操作系统可能会管理它在寄存器外,在主存中,如果可用的寄存器是满的。

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

    对于计算机对驻留在主存中的一些数据进行一些操作,是否必须首先将数据加载到某些寄存器中,然后CPU才能对数据进行操作?

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

    如何知道x, y, z的声明使它们存储在寄存器中?g()中的数据存储在哪里,寄存器还是堆栈?

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

    但这远不是一个普遍的真理:它取决于(复杂的)编译器的内部工作(其细节在该段中被手工描述)。

    我认为f()是一个函数,就像g(x, y, z)是一个函数一样。为什么在调用g()之前的x, y, z在寄存器中,而在g()中传递的参数在堆栈中?

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

    所有的调用约定都有一些开销。这种开销的一个来源是参数传递。许多调用约定试图通过更倾向于通过寄存器传递参数来减少这种情况,但是由于CPU寄存器的数量有限(与堆栈的空间相比),它们最终会在多个参数之后推入堆栈。

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