【问题标题】:Where is the return object stored?返回对象存储在哪里?
【发布时间】:2017-01-28 14:13:48
【问题描述】:

我大致了解函数如何按值返回对象。但我想在较低的层次上理解它。合理的装配水平。

我明白这段代码

ClassA fun(){
    ClassA a;
    a.set(...);
    return a;
}

在内部转换为

void fun(Class& ret){
    ClassA a;
    a.set(...);
    ret.ClassA::ClassA(a);
}

在返回值上有效地调用复制构造函数。

我也知道有一些优化(如 NRVO)可以生成以下代码,避免复制构造函数。

void fun(Class& ret){
    ret.set(...);
}

但是我的问题更基本一些。它实际上与特定的对象无关。它甚至可以是原始类型。

假设我们有这个代码:

int fun(){
   return 0;
}
int main(){
    fun();
}

我的问题是返回对象存储在内存中的什么位置。

如果我们查看堆栈...有main 的堆栈帧,然后是fun 的堆栈帧。返回对象是否存储在某个地址中,例如两个堆栈帧之间?或者它可能存储在main 堆栈帧中的某个位置(并且可能是在生成的代码中通过引用传递的地址)。

我已经考虑过了,第二个似乎更实用但是我不明白编译器如何知道要在main 的堆栈帧中推送多少内存?它是否计算出最大的返回类型并推送它,即使可能会浪费一些内存?还是动态完成,只在调用函数之前分配空间?

【问题讨论】:

  • 取决于编译器,但你总是可以look at its assembly
  • 顺便说一句,ret.ClassA::ClassA(a); 无法编译。应该这样写:new (&ret) ClassA(a);.
  • 强大的实现细节。通常,编译器会尝试在 CPU 寄存器中返回任何内容。如果它不适合,那么它将让调用者在其堆栈帧上保留空间并将指针传递给此存储。在这种情况下可能出现的情况。只需在您自己的编译器上尝试,要求它生成程序集列表。你会得到一个事实而不是猜测。

标签: c++ compiler-construction


【解决方案1】:

C++ 语言规范没有指定这些低级细节。它们由每个 C++ 实现指定,实际实现细节因平台而异。

几乎在每种情况下,一个简单的本机类型的返回值都会在某个指定的 CPU 寄存器中返回。当一个函数返回一个类实例时,细节会有所不同,具体取决于实现。有几种常见的方法,但典型的情况是调用者负责在堆栈上为返回值分配足够的空间,然后调用函数并将附加的隐藏参数传递给函数,函数将在其中复制返回值(或构造它,在 RVO 的情况下)。或者,参数是隐式的,函数可以在调用的堆栈帧之后在堆栈上找到返回值本身的空间。

给定的 C++ 实现也有可能仍然使用 CPU 寄存器来返回小到足以放入单个 CPU 寄存器的类。或者,可能会保留一些 CPU 寄存器用于返回稍大的类。

详细信息各不相同,您需要查阅 C++ 编译器或操作系统的文档,以确定适用于您的具体详细信息。

【讨论】:

  • 通常调用约定由平台 ABI 指定,它可能依赖于操作系统,但严格来说不是操作系统的一部分,也不取决于编译器。当然,文档可能随处可见(如果幸运的话)或无处(如果不是),但搜索 ABI 通常是一个好的开始。
【解决方案2】:

答案是 ABI 特定的,但通常调用是使用隐藏参数编译的,该参数是指向函数应该使用的内存的指针,就像你说的假设函数编译为

void fun(Class& ret){
    ClassA a;
    a.set(...);
    ret.ClassA::ClassA(a);
}

然后在呼叫站点你会得到类似的东西

Class instance = fun();
fun(instance);

现在这使得调用者在堆栈上保留 sizeof(Class) 字节并将该地址传递给函数,以便 fun 可以“填充”该空间。

这与调用者的堆栈帧为自己的本地人保留空间的方式没有什么不同,唯一的区别是其本地人之一的地址被传递给fun

请注意,如果sizeof(Class) 小于一个寄存器(或几个寄存器)的大小,则完全有可能直接在其中返回值。

【讨论】:

    【解决方案3】:

    在以下代码中:

    int fun()
    {
       return 0;
    }
    

    返回值存储在寄存器中。在英特尔架构上,这通常是ax(16 位)或eax(32 位)或rax(64 位)。 (历史上称为累加器。)

    如果返回值是一个指针或一个对象的引用,它仍然会通过那个寄存器返回。

    如果返回值大于机器字,则 ABI(应用程序二进制接口)可能需要使用另一个寄存器来保存高位字。因此,如果您在 16 位架构上返回 32 位数量,则将使用 dx:ax。 (对于更大架构中的更大数量等等。)

    较大的返回值通过其他方式传递,例如您已经知道的void fun(Class& ret) 机制。

    通过累加器寄存器传递返回值非常有效,而且这是一个有点强的约定,我见过的几乎所有 ABI 都需要它。

    【讨论】:

    • 这真的是误导。无论优化如何,翻译单元都必须能够调用在其他翻译单元中编译的函数,即使两个翻译单元已使用不同的优化设置进行编译。因此,优化只能修改仅在定义它们的翻译单元中可见的函数的调用约定(即static)。对于外部可见的函数,平台 ABI 规定了独立于优化设置的调用约定。
    • @rici 什么是误导?我的整个帖子,还是我正在提出的一些具体观点?
    • 您建议优化级别影响调用约定的最后两段。
    • @rici 好吧,我的记忆可能出卖了我,也许您需要明确指定一个特定的调用约定,如 fastcall 以使这些优化发生。我将从我的答案中删除这些部分。
    • 大多数编译器允许您指定非标准调用约定,但您需要在头文件中这样做,以便调用者和被调用者都生成可互操作的代码。我们是否进行优化是一个见仁见智的问题,但它肯定不是 编译器 优化,因为它是由代码明确要求的。
    猜你喜欢
    • 2018-11-09
    • 2015-07-05
    • 2021-03-17
    • 2012-02-24
    • 1970-01-01
    • 2017-03-19
    • 1970-01-01
    • 2015-07-23
    • 1970-01-01
    相关资源
    最近更新 更多