C++ CPU寄存器使用

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++;。 :)


编译器会进行优化。尝试使用 gcc -fverbose-asm -O2 -S yoursource.c 命令,然后查看 yoursource.s 文件的内容。 - Basile Starynkevitch
6个回答

12

是的。并不存在"变量总是在栈上分配"这样的规定。C++标准并没有关于栈的规定。它不假设栈存在,也不假设寄存器存在。它仅仅规定代码应该如何运行,而不是如何实现。

编译器只有在必要时才会将变量存储在栈中——例如当它们需要在函数调用后继续存在或者当你尝试获取它们的地址时。

编译器并不愚蠢。


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特有的属性)。


1
在不影响调试的机器上,-O会禁用堆栈帧设置。x86不是其中之一,您需要一个单独的-fomit-frame-pointer来消除“冗余”的堆栈帧设置(实际上对于调试非常有用,例如在堆栈帧展开中)。 - matja
是的,我完全忘记了那个。我会修复它。但是差异仍然存在。 - int3
区别在于编译器会执行等效的快速调用操作,而程序员不需要使用非标准的 __attribute__。 - Richard Pennington
是的,还有一个指令。这是因为它必须使用与调用者相同的调用约定。否则,实际上调用函数是不可能的。如果函数被内联(通常会这样),那么显然会删除这个额外的指令。但是说实话,我认为你已经走得太远了。请阅读实际问题。问题不是“我的编译器是否忽略调用约定以生成最佳代码”,而是“我的编译器能否将局部变量存储在寄存器中”。函数参数并不是局部变量,这就是为什么它不在寄存器中的原因。 - jalf
@jalf:我认为我们一直在就不同的问题意见分歧。对于造成的混淆,我感到抱歉。 - int3
显示剩余6条评论

8

是的,一个好的、优化的C/C++可以优化它。甚至还可以做得更好:参见这里:Felix von Leitners编译器调查

一个普通的C/C++编译器不会把每个变量都放在栈上。你foo()函数的问题可能是变量通过栈传递到函数中(你的系统(硬件/操作系统)的ABI定义了这一点)。

使用C语言的register关键字,您可以给编译器一个提示,建议将变量存储在寄存器中。示例:

register int x = 10;

但请记住:如果编译器不想,它是可以不把x存储在寄存器中的!


6
答案是可能的,取决于编译器、优化级别和目标处理器。对于mips来说,如果前四个参数很小,则会在寄存器中传递,并且返回值也会在寄存器中返回。因此,您的示例不需要在堆栈上分配任何内容。实际上,真相比小说更奇怪。在这种情况下,参数未更改返回:返回的值是++运算符之前的n的值。
foo:
    .frame  $sp,0,$ra
    .mask   0x00000000,0
    .fmask  0x00000000,0

    addu    $2, $zero, $4
    jr      $ra
    nop

2

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

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

编译器将其与代码一起内联。
lea edx, [eax+1] 

是的,再来一个 MIPS 的例子:static int foo( int n ) { return n++; } int fee() { return foo(5); }输出结果为: .text .align 2 .globl fee .ent fee fee: .frame $sp,0,$ra .mask 0x00000000,0 .fmask 0x00000000,0 addiu $2, $zero, 5 jr $ra nop .set macro .set reorder .end fee .size fee, .-fee - Richard Pennington

0

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接