【问题标题】:How does the caller of a function know whether Return Value Optimization was used?函数的调用者如何知道是否使用了返回值优化?
【发布时间】:2013-09-05 13:33:48
【问题描述】:

我对返回值优化的理解是,编译器偷偷传递了将要存储返回值的对象的地址,并对该对象而不是局部变量进行更改。

例如代码

std::string s = f();

std::string f()
{
    std::string x = "hi";
    return x;
}

变得类似于

std::string s;
f(s);

void f(std::string& x)
{
    x = "hi";
}

使用 RVO 时。这意味着函数的接口发生了变化,因为多了一个隐藏参数。

现在考虑以下我从维基百科窃取的案例

std::string f(bool cond)
{
    std::string first("first");
    std::string second("second");
    // the function may return one of two named objects
    // depending on its argument. RVO might not be applied
    return cond ? first : second;
}

让我们假设编译器会将 RVO 应用于第一种情况,但不会应用于第二种情况。但是函数的接口不会根据是否应用RVO而改变吗?如果函数体f对编译器不可见,编译器如何知道是否应用了RVO以及调用者是否需要传递隐藏地址参数?

【问题讨论】:

  • 每个编译器都可以选择以他们想要的任何方式执行此操作,如果在您描述的情况下这对他们来说是个问题,他们可能会选择始终使用 RVO。当您对不同的编译器如何在后台执行此操作感兴趣时,我建议您阅读生成的汇编代码。使用 gcc 资源管理器轻松访问 clang/gcc 生成的程序集。
  • @PlasmaHH 但 RVO 并不总是可行的。
  • 这并不意味着调用约定在使用或不使用时会发生变化。看看一些汇编程序是如何做到的。

标签: c++


【解决方案1】:

界面没有变化。在所有情况下,结果 函数必须出现在调用者的范围内; 通常,编译器使用隐藏指针。唯一的 不同之处在于,当使用 RVO 时,如您的第一种情况, 编译器将“合并”x 和这个返回值,构造 x在指针给定的地址处;不使用时, 编译器将生成对复制构造函数的调用 return 语句,将任何内容复制到此返回值中。

我可能会补充说,您的第二个示例不是非常接近 发生。在呼叫站点,您几乎总能得到一些东西 喜欢:

<raw memory for string> s;
f( &s );

而被调用的函数要么构造一个局部变量 或直接在它通过的地址临时,或复制 在这个地址构造一些其他的值。所以在你的最后 例如,return 语句或多或少是 相当于:

if ( cont ) {
    std::string::string( s, first );
} else {
    std::string::string( s, second );
}

(显示传递给副本的隐式this指针 构造函数。)在第一种情况下,如果 RVO 适用,则特殊的 代码将在x的构造函数中:

std::string::string( s, "hi" );

然后在函数中的其他任何地方用*s替换x (并且在返回时什么都不做)。

【讨论】:

  • 所以它添加了一个隐藏参数并使函数无效? (显然在引擎盖下)
  • @xanatos:在汇编器级别上没有“void”函数,对于放置返回值的位置以及调用者期望它们的位置有约定,“void”只是返回值的数量为 0。
  • @PlasmaHH 并且在汇编程序级别没有参数......只有有人压入堆栈或放入寄存器的东西。我们可以整天玩这个游戏。假设它不会将返回值压入堆栈或将其放入寄存器,因此它类似于 void 函数。
  • @xanatos:如果您的调用约定总是将返回值写入某个寄存器中作为指针给出的位置怎么办?你所有的 C++ 函数在那个实现上都是 void 函数吗?
  • @xanatos 你可以这样看。当 CFront 使用 C 作为其中间语言时,在这种情况下,通常会声明 C 函数返回 void。 (IIRC——自从我上次使用 CFront 已经有一段时间了。)但请注意,这对于在 C 中返回 struct 的函数也是如此。如果它不适合寄存器,您需要一些约定来确定函数将它放在内存中的位置(并且它不能在本地堆栈上,因为它正在消失)。
【解决方案2】:

让我们一起玩 NRVO、RVO 和复制省略!

这是一个类型:

#include <iostream>
struct Verbose {
  Verbose( Verbose const& ){ std::cout << "copy ctor\n"; }
  Verbose( Verbose && ){ std::cout << "move ctor\n"; }
  Verbose& operator=( Verbose const& ){ std::cout << "copy asgn\n"; }
  Verbose& operator=( Verbose && ){ std::cout << "move asgn\n"; }
};

这很冗长。

这是一个函数:

Verbose simple() { return {}; }

这很简单,并且使用直接构造它的返回值。如果Verbose 缺少复制或移动构造函数,上述函数将起作用!

这是一个使用 RVO 的函数:

Verbose simple_RVO() { return Verbose(); }

这里未命名的Verbose() 临时对象被告知将自身复制到返回值。 RVO 意味着编译器可以跳过该副本,并直接将Verbose() 构造为返回值,当且仅当存在复制或移动构造函数时。复制或移动构造函数没有被调用,而是被省略了。

这是一个使用 NRVO 的函数:

 Verbose simple_NRVO() {
   Verbose retval;
   return retval;
 }

为了实现 NRVO,每条路径都必须返回完全相同的对象,并且您不能对此偷偷摸摸(如果您将返回值强制转换为引用,然后返回该引用,这将阻止 NRVO)。在这种情况下,编译器所做的就是将命名对象retval 直接构造到返回值位置。与 RVO 类似,复制或移动构造函数必须存在,但不会被调用。

这是一个无法使用 NRVO 的函数:

 Verbose simple_no_NRVO(bool b) {
   Verbose retval1;
   Verbose retval2;
   if (b)
     return retval1;
   else
     return retval2;
 }

由于它可以返回两个可能的命名对象,它不能在返回值位置构造两个,因此它必须进行实际复制。在 C++11 中,返回的对象将隐含地 moved 而不是复制,因为它是在简单的 return 语句中从函数返回的局部变量。所以至少是这样的。

最后,在另一端有复制省略:

Verbose v = simple(); // or simple_RVO, or simple_NRVO, or...

当你调用一个函数时,你给它提供了它的参数,你告诉它应该把它的返回值放在哪里。调用者负责清理返回值并为其分配内存(在堆栈上)。

这种通信是通过调用约定以某种方式完成的,通常是隐式的(即通过堆栈指针)。

在许多调用约定下,可以存储返回值的位置最终会被用作局部变量。

一般来说,如果你有一个形式的变量:

Verbose v = Verbose();

可以省略隐含的副本——Verbose() 直接在v 中构造,而不是创建一个临时的然后复制到v。同样,如果编译器的运行时模型支持(通常也支持)simple(或simple_NRVO,或其他)的返回值,则可以省略它。

基本上,调用站点可以告诉simple_* 将返回值放在特定位置,并将该位置视为局部变量v

请注意,NRVO 和 RVO 以及隐式移动都是在函数中完成的,调用者不需要对此一无所知。

同样,调用站点的省略都是在函数之外完成的,如果调用约定支持它,则不需要函数体的任何支持。

这不一定在每个调用约定和运行时模型中都是正确的,因此 C++ 标准使这些优化成为可选的。

【讨论】:

  • 如果我们改变“simple_no_NRVO”的例子来构造不同if-else-cases中的变量,nrvo是否适用?详细 simple_no_NRVO(bool b) { if (b) { 详细 retval1;返回 retval1; } else { 详细 retval2;返回 retval2; } }
猜你喜欢
  • 2018-08-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-26
  • 1970-01-01
相关资源
最近更新 更多