如何知道variables是在寄存器中还是在堆栈中?

我正在阅读这个关于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() 。 但是这是很多不必要的读写,特别是在编译器能够使用variablesxyz寄存器的情况下:每个variables可以被写两次(作为寄存器并且也作为参数)并被读两次(当在g()并在返回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多一点,因为答案提供了一些指导。

不要太认真地对待这一段。 这似乎是过度的假设,然后进入过度的细节,这是不能真正的概括。

但是,你的问题非常好。

  1. 为了使计算机对驻留在主存储器中的一些数据进行一些操作,数据必须首先被加载到某些寄存器,然后CPU才能对数据进行操作。 (我知道这个问题与C ++没有特别的关系,但是理解这个将有助于理解C ++是如何工作的。)

或多或less,一切都需要加载到寄存器。 大多数计算机是围绕着一个数据通路 ,一条连接寄存器,算术电路和存储器层次结构顶层的总线组织的。 通常,在数据path上广播的任何东西都用一个寄存器来标识。

您可能还记得伟大的RISC和CISC的辩论。 其中一个关键点是,如果不允许存储器直接连接到算术电路,那么计算机devise可以简单得多。

在现代计算机中,有一些架构寄存器 ,像variables一样是编程结构, 物理寄存器是实际的电路。 编译器在处理体系结构寄存器方面做了很多繁重工作,以便跟踪物理寄存器。 对于像x86这样的CISC指令集,这可能涉及到生成将内存中的操作数直接发送到算术运算的指令。 但在幕后,它一路注册。

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

  1. 我认为f()是一个函数,与g(x,y,z)是同一个函数。 在调用g()之前,x,y,z如何在寄存器中,并且在g()中传递的参数在堆栈中?

每个平台都定义了一个C函数相互调用的方法。 在寄存器中传递参数更有效。 但是,有一些权衡,寄存器的总数是有限的。 较早的ABI往往为了简单而牺牲效率,并把它们全部放在堆栈上。

底线:这个例子是任意假设一个幼稚的ABI。

  1. 如何知道x,y,z的声明使它们存储在寄存器中? g()里面的数据被存储,注册或者堆栈?

编译器倾向于使用更频繁访问的值的寄存器。 示例中没有任何内容需要使用堆栈。 然而,不太经常访问的值将被放置在堆栈上以使更多的寄存器可用。

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

底线:避免采取地址和传递/存储他们无情地。

编译器(与处理器types一起)完全取决于是将variables存储在内存还是寄存器中(或者在某些情况下是多个寄存器)(以及给出编译器的选项,假设它有可供select的选项这样的东西 – 大多数“好”的编译器)。 例如,LLVM / Clang编译器使用一个称为“mem2reg”的特定优化过程,将variables从内存移动到寄存器。 这样做的决定是基于variables的使用方式 – 例如,如果您在某个时候获取variables的地址,则需要将其存储在内存中。

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

另外,至less在编译器中有一些相似的可移植性,也将是实际目标的生成机器代码阶段,其中包含特定于目标的优化,这些目标特定的优化也可以将variables从内存移动到寄存器。

这是不可能的[不理解特定编译器如何工作]来确定代码中的variables是在寄存器还是在内存中。 人们可以猜到,但这样的猜测就像猜测其他“可预测的东西”一样,就像看着窗外猜测几小时内会下雨 – 这取决于你住的地方,这可能是一个完全的随机猜测,还是相当可预测的 – 在一些热带国家,你可以根据每天下午什么时候下雨,在其他国家很less下雨的时候设置你的手表,而在一些国家,比如在英国这样的国家,除了“现在在这里不是正在下雨“。

回答实际问题:

  1. 这取决于处理器。 正确的RISC处理器(如ARM,MIPS,29K等)没有使用除加载和存储types指令之外的内存操作数的指令。 因此,如果您需要添加两个值,则需要将这些值加载到寄存器中,并对这些寄存器使用add操作。 有些例如x86和68K允许两个操作数中的一个是内存操作数,例如PDP-11和VAX具有“完全自由”,无论您的操作数是在内存还是寄存器中,都可以使用相同的指令不同的操作数的寻址模式不同。
  2. 这里你的原始前提是错误的 – 不能保证g参数在堆栈中。 这只是许多select之一。 许多ABI(应用程序二进制接口,也称为“调用约定”)使用寄存器作为函数的前几个参数,所以它又取决于哪个编译器(在某种程度上)以及哪个处理器(远远超过哪个编译器)无论参数是在内存还是在寄存器中。
  3. 再次,这是编译器做出的决定 – 它取决于处理器有多less个寄存器,哪些是可用的,如果为xyz “释放”一些寄存器,成本是什么,其范围从“根本没有成本“到”相当多“ – 再次,取决于处理器模型和ABI。

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

甚至连这种说法都是真的。 对于你曾经使用过的所有平台来说,这可能是正确的,但肯定会有另外一个架构根本不使用处理器寄存器 。

但是,您的x86_64计算机不会。

我认为f()是一个函数,与g(x,y,z)是同一个函数。 在调用g()之前,x,y,z如何在寄存器中,并且在g()中传递的参数在堆栈中?

如何知道x,y,z的声明使它们存储在寄存器中? g()里面的数据被存储,注册或者堆栈?

这两个问题不能唯一地回答你的代码将被编译的任何编译器和系统。 他们甚至不能被视为理所当然,因为g的参数可能不在堆栈中,这一切都取决于我将在下面解释的几个概念。

首先,您应该了解所谓的调用约定 ,这些约定定义了函数参数如何传递(例如,被压入堆栈,被放置在寄存器中,还是被两者混合)。 这不是由C ++标准强制执行的,调用约定是ABI的一部分,这是关于低级机器代码程序问题的一个更广泛的主题。

其次, 寄存器分配 (即在任何给定时间实际上将哪些variables加载到寄存器中)是一项复杂的任务和一个NP完全问题。 编译器试图用他们所拥有的信息尽其所能。 通常不太经常访问的variables放在堆栈上,而更频繁访问的variables则放在寄存器上。 那么Where the data inside g() is stored, register or stack?部分Where the data inside g() is stored, register or stack? 不能一劳永逸地回答因为它取决于很多因素,包括注册压力 。

更不用说编译器优化,甚至可以消除一些variables的需求。

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

当然,你的里程可能会有所不同,并且有超过这个特定FAQ的范围之外的数十亿个variables,但是上面的例子是程序集成中可能发生的事情的一个例子。

即你所发布的这个段落有一些假设来设置一个例子。 这些只是假设,你应该这样对待他们。

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

在不考虑汇编语言的情况下,您无法知道variables是否在寄存器,堆栈,堆,全局内存或其他地方。 variables是一个抽象的概念。 只要执行没有改变 ,编译器就可以使用寄存器或其他内存。

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

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

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

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

关于你的#3问题,不知道x,y和z肯定会存储在f()中的寄存器中。 可以使用register关键字,但这更多的build议。 根据调用约定,假设编译器没有进行任何涉及parameter passing的优化,您可以预测参数是堆栈还是寄存器。

你应该熟悉一下调用约定。 调用约定处理parameter passing给函数的方式,通常包括以指定顺序将parameter passing到堆栈,将参数放入寄存器或两者的组合。

stdcallcdeclfastcall是调用约定的一些例子。 在parameter passing方面,stdcall和cdecl是一样的,参数按照从右到左的顺序推入堆栈。 在这种情况下,如果g()cdeclstdcall则调用者将按以下顺序推送z,y,x:

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

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

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

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

 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指令将被replace为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中。

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

编译器的担心是把你的程序翻译成汇编语言,但是如何完成它与编译器的工作方式紧密相关。 一些编译器允许你提示要注册哪个variables映射。 检查例如: https : //gcc.gnu.org/onlinedocs/gcc/Global-Reg-Vars.html

您的编译器会将转换应用于您的代码,以便获得某些内容,可能是性能,可能是较小的代码大小,并且会使用成本函数来估计此增益,因此通常只能看到拆分编译单元的结果。

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

这是通过devise来提高性能,因为处理器更容易(也更快)处理和处理寄存器中的数据。 架构寄存器的大小是有限的,所以一切都不能放入寄存器。 即使你'提示'你的编译器把它放到寄存器中,最终,如果可用的寄存器已满,OS可以在寄存器之外的主内存中pipe理它。

最有可能的是,一个variables将在主存中,因为它在近处执行时保持相关性并且可能在更长的CPU时间周期内保持依赖。 一个variables在架构寄存器中,因为它在即将到来的机器指令中保持相关性,并且执行将几乎立即执行,但可能不相关很长时间。

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

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

如何知道x,y,z的声明使它们存储在寄存器中? g()里面的数据被存储,注册或者堆栈?

假设编译器没有消除局部variables,它宁愿将它们放在寄存器中,因为寄存器比堆栈(驻留在主存储器或caching中)更快。

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

我认为f()是一个函数,与g(x,y,z)是同一个函数。 在调用g()之前,x,y,z如何在寄存器中,并且在g()中传递的参数在堆栈中?

即使我们假设variables实际上是存储在寄存器中的,当你调用一个函数的时候, 调用约定也会启动。这个约定描述了函数的调用方式,parameter passing的方式,清除堆栈,什么寄存器被保存。

所有调用约定都有一些开销。 这个开销的一个来源是parameter passing。 许多调用约定试图减less这种情况,通过优先通过寄存器传递参数,但是由于CPU寄存器的数量是有限的(与堆栈的空间相比),所以它们最终会在一系列参数后面回落。

你的问题中的段落假设了一个调用约定,它把所有的东西都通过堆栈,并基于这个假设,它试图告诉你的是,如果我们能够在编译时“复制”调用者内部被调用函数的主体(而不是发送函数的调用)。 这将在逻辑上产生相同的结果,但是它将消除函数调用的运行时成本。