10

在 C++ 中,局部变量总是分配在堆栈上。堆栈是您的应用程序可以占用的允许内存的一部分。该内存保留在您的 RAM 中(如果没有换出到磁盘)。现在,C++ 编译器是否总是创建将局部变量存储在堆栈上的汇编代码?

以下面的简单代码为例:

int foo( int n ) {
   return ++n;
}

在 MIPS 汇编代码中,可能如下所示:

foo:
addi $v0, $a0, 1
jr $ra

如您所见,我根本不需要为 n 使用堆栈。C++ 编译器会认识到这一点,并直接使用 CPU 的寄存器吗?

编辑:哇,非常感谢您几乎立即和广泛的回答!foo 的函数体当然应该是return ++n;,不是return n++;。:)

4

6 回答 6

12

是的。没有“变量总是在堆栈上分配”的规则。C++ 标准没有提到堆栈。它不假定堆栈存在或寄存器存在。它只是说代码应该如何表现,而不是应该如何实现。

编译器仅在必须时才将变量存储在堆栈上 - 例如,当它们必须经过函数调用时,或者如果您尝试获取它们的地址。

编译器并不愚蠢。;)

于 2009-12-02T12:34:15.627 回答
9

免责声明:我不知道MIPS,但我知道一些x86,我认为原理应该是一样的..

在通常的函数调用约定中,编译器会将 的值压入n堆栈以将其传递给函数foo。但是,fastcall您可以使用约定来告诉 gcc 通过寄存器传递值。(MSVC 也有这个选项,但我不确定它的语法是什么。)

test.cpp:

int foo1 (int n) { return ++n; }
int foo2 (int n) __attribute__((fastcall));
int foo2 (int n) {
    return ++n;
}

用 编译上述内容g++ -O3 -fomit-frame-pointer -c test.cpp,我得到foo1

mov eax,DWORD PTR [esp+0x4]
add eax,0x1
ret

如您所见,它从堆栈中读取值。

这是foo2

lea eax,[ecx+0x1]
ret

现在它直接从寄存器中获取值。

当然,如果您内联函数,编译器将在较大函数的主体中进行简单的添加,而不管您指定的调用约定。但是当你不能让它内联时,这将会发生。

免责声明 2:我并不是说您应该不断地猜测编译器。在大多数情况下,这可能是不切实际和必要的。但不要假设它会产生完美的代码。

编辑1:如果您谈论的是普通的局部变量(不是函数参数),那么是的,编译器会在它认为合适的时候将它们分配到寄存器或堆栈中。

编辑 2:调用约定似乎是特定于体系结构的,并且 MIPS 将传递堆栈上的前四个参数,正如 Richard Pennington 在他的回答中所说的那样。因此,在您的情况下,您不必指定额外的属性(实际上是 x86 特定的属性。)

于 2009-12-02T12:34:29.363 回答
8

是的,一个好的优化 C/C++ 将优化它。甚至更多:请参阅此处:Felix von Leitners 编译器调查

无论如何,普通的 C/C++ 编译器不会将每个变量都放入堆栈。您的foo()函数的问题可能是变量可以通过堆栈传递给函数(您的系统(硬件/操作系统)的 ABI 定义了这一点)。

使用 C 的register关键字,您可以向编译器提示将变量存储在寄存器中可能会很好。样本:

register int x = 10;

但请记住:编译器可以随意不存储x在寄存器中!

于 2009-12-02T12:34:20.027 回答
6

答案是肯定的,也许。它取决于编译器、优化级别和目标处理器。

在 mips 的情况下,前四个参数(如果很小)在寄存器中传递,返回值在寄存器中返回。所以你的例子不需要在堆栈上分配任何东西。

事实上,真实比虚构更离奇。在您的情况下,参数返回不变:返回的值是 ++ 运算符之前的 n 值:

foo:
    .frame  $sp,0,$ra
    .mask   0x00000000,0
    .fmask  0x00000000,0

    addu    $2, $zero, $4
    jr      $ra
    nop
于 2009-12-02T12:35:46.823 回答
2

由于您的示例foo函数是一个标识函数(它只返回它的参数),我的 C++ 编译器(VS 2008)完全删除了这个函数调用。如果我将其更改为:

int foo( int n ) {
   return ++n;
}

编译器将其内联

lea edx, [eax+1] 
于 2009-12-02T12:45:30.990 回答
0

是的,寄存器在 C++ 中使用。MDR(内存数据寄存器)包含正在获取和存储的数据。例如,要检索单元格 123 的内容,我们会将值 123(二进制)加载到 MAR 中并执行提取操作。操作完成后,单元格 123 内容的副本将在 MDR 中。要将值 98 存储到单元格 4 中,我们将 4 加载到 MAR 中,将 98 加载到 MDR 中并执行存储。操作完成后,单元格 4 的内容将被设置为 98,方法是丢弃之前的内容。数据和地址寄存器与他们一起工作来实现这一点。在 C++ 中,当我们用一个值初始化一个 var 或询问它的值时,同样的现象也会发生。

而且,还有一件事,现代编译器还执行寄存器分配,这比内存分配快一点。

于 2017-07-24T09:37:55.597 回答