【问题标题】:C++ CPU Register UsageC++ CPU 寄存器使用
【发布时间】:2010-12-22 09:39:35
【问题描述】:

在 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

标签: c++ compiler-construction assembly mips cpu-registers


【解决方案1】:

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

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

编译器并不愚蠢。 ;)

【讨论】:

    【解决方案2】:

    免责声明:我不知道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 特定的属性。)

    【讨论】:

    • -O 在不干扰调试的机器上禁用堆栈帧设置 - x86 不是其中之一,您需要一个单独的 -fomit-frame-pointer 来消除“冗余”堆栈帧setup(这实际上对调试很有用,即在堆栈帧展开中)
    • 是的,我完全忘记了这一点。我会修复它。但差异仍然存在。
    • 进行链接时间优化的编译器也可以识别出可以自行将调用转换为快速调用,因为它可以查看并修复所有调用站点。
    • 哪个“差异仍然存在”? matja 指出的一个是唯一效率低下,不是吗?这是由于缺少优化标志造成的。 -1 表示您不能假设编译器会将中间体存储在寄存器中。当然,更复杂的优化是对的,但是对于那个呢?
    • 不同之处在于编译器将执行相当于快速调用的操作,而程序员不必使用非标准的属性
    【解决方案3】:

    是的,一个好的优化 C/C++ 将优化它。甚至更多See here: Felix von Leitners Compiler Survey

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

    使用C 的register 关键字,您可以给编译器一个提示,将变量存储在寄存器中可能会更好。示例:

    register int x = 10;
    

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

    【讨论】:

      【解决方案4】:

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

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

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

      foo:
          .frame  $sp,0,$ra
          .mask   0x00000000,0
          .fmask  0x00000000,0
      
          addu    $2, $zero, $4
          jr      $ra
          nop
      

      【讨论】:

        【解决方案5】:

        由于您的示例 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 费用 费用:.frame $sp,0,$ra .mask 0x00000000,0 .fmask 0x00000000,0 addiu $2, $zero, 5 jr $ra nop .set 宏。设置重新订购 .end fee .size fee, .-fee
        【解决方案6】:

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

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

        【讨论】:

          猜你喜欢
          • 2021-03-25
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-12-31
          • 2016-01-19
          • 2019-04-23
          • 1970-01-01
          • 2010-12-30
          相关资源
          最近更新 更多