C++11编译器何时能使RVO和NRVO优于移动语义和常量引用绑定?

15

考虑当从函数中返回启用了移动语义的“整个”对象时,例如 std::basic_string<>:

std::wstring build_report() const
{
    std::wstring report;
    ...

    return report;
}

那么,我是否实际上可以期望做出“最佳”选择,使用具有移动语义的返回字符串,如下所示:

const std::wstring report(std::move(build_report()));

还是我应该依赖 (N)RVO 来发生

const std::wstring report(build_report());

甚至可以使用const引用将临时对象绑定到

const std::wstring& report(build_report());
有没有一种方案可以确定性地选择这些选项?请注意,上面使用的std::wstring只是启用了移动语义类型的示例。它同样可以用你的arbitrary_large_structure进行交换。我检查了在VS 2010中运行速度优化的发布版本时生成的汇编代码。
std::wstring build_report(const std::wstring& title, const std::wstring& content)
{
    std::wstring report;
    report.append(title);
    report.append(content);

    return report;
}

const std::wstring title1(L"title1");
const std::wstring content1(L"content1");

const std::wstring title2(L"title2");
const std::wstring content2(L"content2");

const std::wstring title3(L"title3");
const std::wstring content3(L"content3");

int _tmain(int argc, _TCHAR* argv[])
{
    const std::wstring  report1(std::move(build_report(title1, content1)));
    const std::wstring  report2(build_report(title2, content2));
    const std::wstring& report3(build_report(title3, content3));

    ...

    return 0;
}

最有趣的两个结果:

  • 显式调用 std::move 以使用移动构造函数来三倍增加指令计数。
  • 正如 James McNellis 在下面他的回答中所述,report2report3 的生成汇编确实与显式调用 std::move 相比少了三倍的指令。

我认为调用move没有被内联和消除真的很奇怪。 - James McNellis
1
@James:是的,特别是在Kerrek SB的评论中提到STL说RVO发生在任何其他事情之前的情况下。(https://dev59.com/Ymw15IYBdhLWcg3wkMnz#6533181)你不会恰好就在STL附近,可以走过去问问他吗?;-) - Johann Gerell
@Howard:虽然你对那个问题的回答非常好,但我认为这个问题涉及到更深层次的问题。 - Johann Gerell
是的,我确实花了相当多的时间在他工作的大楼里 :-) 不过我还没有真正见过STL先生本人。不过我会跟进为什么这里没有内联std::move的问题;我确实觉得这很令人困惑。我将度假到下周末结束,所以需要几周时间才能回复您。 - James McNellis
3个回答

13

std::move(build_report()) 是完全不必要的: build_report() 已经是一个右值表达式 (它是通过值返回对象的函数的调用),因此如果有移动构造函数,将会使用 std::wstring 的移动构造函数 (实际上已经有了)。

另外,当你返回一个局部变量时,如果它是具有移动构造函数的类型,它就会被移动,所以不会进行任何拷贝。

声明 report 为对象或常量引用应该没有任何功能上的区别;在两种情况下,您最终都会得到一个对象(要么是命名的 report 对象,要么是可以绑定到 report 引用的未命名对象)。


另外两个在(N)RVO存在的情况下应该是功能上相同的 - 很有趣。我还没有考虑过这个。我会查看生成的汇编代码。我会回来的! - Johann Gerell
我稍微修改了一下:这两者之间实际上不应该有任何区别:从实际角度来看,在这两种情况下,你都必须在report的作用域内拥有某个对象;唯一的“区别”就是report是对象的名称还是绑定到该对象的引用...从性能的角度来看,这两者之间不应该有任何区别。 - James McNellis
请查看我在问题中的编辑2 - Johann Gerell

4
我不确定这是否是标准化的(正如Nicol所说,所有优化都取决于编译器),但我听说STL谈论过这个问题,并且(至少在MSVC中),RVO发生在任何其他操作之前。因此,如果有机会应用RVO,则会在您不采取任何措施的情况下自动发生。其次,当您返回临时对象时,您不必编写std::move(我认为实际上这是标准的),因为返回值将隐式地被视为rvalue。
总之:不要怀疑编译器,只需编写最自然的代码,它会给您最佳的结果。

在我的第二次编辑中,可以看到显式调用std::move甚至会完全阻止(N)RVO。 - Johann Gerell
1
@Johann:很酷。我想这表明只有在直接返回裸临时对象时才考虑RVO。这是不试图聪明地超越语言的又一个原因! - Kerrek SB

3
有什么方案可以确定这些选项的确定性选择吗?
没有,也永远不会有。
编译器不需要进行任何形式的优化。唯一可以确定的是编译一些代码,然后查看输出结果。
最终你可能得到一个通用的启发式算法,一个社区共识,人们会说,“对于大多数编译器,X似乎是最快的。”但仅此而已。这需要多年时间,因为编译器需要适应C++0x并且实现成熟。

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