【问题标题】:Why does Visual Studio not perform return value optimization (RVO) in this case为什么 Visual Studio 在这种情况下不执行返回值优化 (RVO)
【发布时间】:2014-11-15 19:20:01
【问题描述】:

我回答a question 并推荐return by-value for a large type,因为我相信编译器会执行return-value optimization (RVO)。但后来有人向我指出,Visual Studio 2013 没有在我的代码上执行 RVO。

我发现 a question here 关于 Visual Studio 无法执行 RVO,但在这种情况下,结论似乎是,如果真的很重要,Visual Studio 将执行 RVO。就我而言,它确实很重要,它会对性能产生重大影响,我已经通过分析结果证实了这一点。这是简化的代码:

#include <vector>
#include <numeric>
#include <iostream>

struct Foo {
  std::vector<double> v;
  Foo(std::vector<double> _v) : v(std::move(_v)) {}
};

Foo getBigFoo() {
  std::vector<double> v(1000000);
  std::iota(v.begin(), v.end(), 0);  // Fill vector with non-trivial data

  return Foo(std::move(v));  // Expecting RVO to happen here.
}

int main() {
  std::cout << "Press any key to start test...";
  std::cin.ignore();

  for (int i = 0; i != 100; ++i) {  // Repeat test to get meaningful profiler results
    auto foo = getBigFoo();
    std::cout << std::accumulate(foo.v.begin(), foo.v.end(), 0.0) << "\n";
  }
}

我希望编译器对来自getBigFoo() 的返回类型执行 RVO。但它似乎是在复制Foo

我知道 will create a copy-constructor 的编译器 Foo。我也知道,与 Foo 的兼容 C++11 编译器 Visual Studio does not create a move-constructor 不同。但这应该没问题,RVO 是一个 C++98 概念,无需移动语义即可工作。

那么,问题是,Visual Studio 2013 在这种情况下不执行返回值优化是否有充分的理由?

我知道一些解决方法。我可以为Foo 定义一个移动构造函数:

Foo(Foo&& in) : v(std::move(in.v)) {}

这很好,但是有很多遗留类型没有移动构造函数,很高兴知道我可以依赖 RVO 来处理这些类型。此外,某些类型可能本质上是可复制的,但不可移动。

如果我从 RVO 更改为 NVRO(命名返回值优化),那么 Visual Studio 确实 似乎会执行优化:

  Foo foo(std::move(v))
  return foo;

这很好奇,因为我认为 NVRO 的可靠性不如 RVO。

更奇怪的是,如果我更改Foo 的构造函数,那么它会创建并填充vector

  Foo(size_t num) : v(num) {
    std::iota(v.begin(), v.end(), 0);  // Fill vector with non-trivial data
  }

当我尝试做 RVO 时,它没有将其移入,而是有效:

Foo getBigFoo() {
  return Foo(1000000);
}

我很高兴采用其中一种解决方法,但我希望能够预测 RVO 将来何时可能会像这样失败,谢谢。

编辑:来自@dyp的More concise live demo

Edit2:我为什么不直接写return v;

首先,它没有帮助。 Profiler 结果显示,如果我只写return v;,Visual Studio 2013 仍会复制向量,即使它确实有效,也只是一种解决方法。我并没有试图真正修复这段特定的代码,我试图理解 RVO 失败的原因,以便我可以预测它将来何时可能失败。确实,这是编写这个特定示例的一种更简洁的方式,但在很多情况下我不能只写 return v;,例如,如果 Foo 有额外的构造函数参数。

【问题讨论】:

  • 当然,您可以使用return {std::move(v)};,因为该构造函数不是显式的。这不需要任何 (N)RVO,它指定不创建临时。
  • 你为什么不直接写return v;
  • 我刚刚在 Visual Studio 2014 CTP 上尝试过,它为您的代码应用了 RVO。编辑:我应该说@dyp的例子。
  • 我在此处发布了一些有关何时执行 RVO 以及何时失败(基于来自 @dyp 的示例)的详细信息:rovrov.com/blog/2014/11/21/RVO-and-copy-elision-failing。这并不能解释为什么 RVO 会失败,但一些观察结果可能仍然很有趣。

标签: c++ c++11 visual-studio-2013 move-semantics rvo


【解决方案1】:

如果代码看起来应该优化,但没有得到优化,我会在这里提交错误http://connect.microsoft.com/VisualStudio 或向 Microsoft 提出支持案例。 这篇文章虽然是针对 VC++2005 的(我找不到当前版本的文档),但它确实解释了一些它不起作用的场景。 http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx#nrvo_cpp05_topic3

如果我们想确定优化已经发生,一种可能性是检查汇编输出。如果需要,这可以作为构建任务自动化。

这需要使用 /FAs 选项生成 .asm 输出,如下所示:

cl test.cpp /FAs

会生成test.asm。

下面 PowerShell 中的一个潜在示例,可以这样使用:

PS C:\test> .\Get-RVO.ps1 C:\test\test.asm test.cpp
NOT RVO test.cpp - ; 13   :   return Foo(std::move(v));// Expecting RVO to happen here.

PS C:\test> .\Get-RVO.ps1 C:\test\test_v2.optimized.asm test.cpp
RVO OK test.cpp - ; 13   :   return {std::move(v)}; // Expecting RVO to happen here.

PS C:\test> 

脚本:

# Usage Get-RVO.ps1 <input.asm file> <name of CPP file you want to check>
# Example .\Get-RVO.ps1 C:\test\test.asm test.cpp
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True,Position=1)]
  [string]$assemblyFilename,

  [Parameter(Mandatory=$True,Position=2)]
  [string]$cppFilename
)

$sr=New-Object System.IO.StreamReader($assemblyFilename)
$IsInReturnSection=$false
$optimized=$true
$startLine=""
$inFile=$false

while (!$sr.EndOfStream)
{
    $line=$sr.ReadLine();

    # ignore any files that aren't our specified CPP file
    if ($line.StartsWith("; File"))
    {
        if ($line.EndsWith($cppFilename))
        {
            $inFile=$true
        }
        else
        {
            $inFile=$false
        }
    }

    # check if we are in code section for our CPP file...
    if ($inFile)
    {
        if ($line.StartsWith(";"))
        {
            # mark start of "return" code
            # assume optimized, unti proven otherwise
            if ($line.Contains("return"))
            {
                $startLine=$line 
                $IsInReturnSection=$true
                $optimized=$true
            }
        }

        if ($IsInReturnSection)
        {
            # call in return section, not RVO
            if ($line.Contains("call"))
            {
                $optimized=$false
            }

            # check if we reached end of return code section
            if ($line.StartsWith("$") -or $line.StartsWith("?"))
            {
                $IsInReturnSection=$false
                if ($optimized)
                {
                    "RVO OK $cppfileName - $startLine"
                }
                else
                {
                    "NOT RVO $cppfileName - $startLine"
                }
            }
        }
    }

}

【讨论】:

  • 我真的不想每次使用 RVO 时都必须检查程序集!如果这是必要的,我想我不会依赖 RVO。我很欣赏有关如何生成/检查程序集的说明。我已经确认 Jagannath 的观察,即它已在 Visual Studio 2014 CTP 中修复,因此提交错误可能毫无意义。
  • 这是 100% 确定的唯一可能方法,因为即使您遵循我链接的 Microsoft 文章中提到的逻辑,也可能存在编译器错误,或者您遇到的情况不是-明显的。但是,如果非常重要,该脚本可用于自动检查构建后。如果您真的想知道检查实际逻辑,我认为您必须使用开源编译器 CLang 或 GCC。
  • 话虽如此,如果您确实有很好的文档记录、可重现的场景,Microsoft 通常会响应错误报告,它可能会“按设计”得出结论,但至少您可能会得到一个理由。跨度>
  • @ChrisDrew “如果有必要,我想我不会依赖 RVO。” -- 只要有可能,我强烈认为这正是你应该做的。总会有标准允许 RVO 但编译器不执行的程序。几乎不可能保证在允许的情况下总是执行 RVO,因为在构造要返回的对象时,编译器可能无法确定最终会执行哪个返回语句。优化您的移动构造函数,RVO 的影响应该会小得多。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-03-02
  • 2014-09-12
  • 2018-04-26
  • 2018-05-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多