I was answering a question and recommending return by-value for a large type because I was confident the compiler would perform return-value optimization (RVO). But then it was pointed out to me that Visual Studio 2013 was not performing RVO on my code.
I've found a question here regarding Visual Studio failing to perform RVO but in that case the conclusion seemed to be that if it really matters Visual Studio will perform RVO. In my case it does matter, it makes a significant impact to performance which I've confirmed with profiling results. Here is the simplified code:
#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"; } }
I'm expecting the compiler to perform RVO on the return type from getBigFoo()
. But it appears to be copying Foo
instead.
I'm aware that the compiler will create a copy-constructor for Foo
. I'm also aware that unlike a compliant C++11 compiler Visual Studio does not create a move-constructor for Foo
. But that should be OK, RVO is a C++98 concept and works without move-semantics.
So, the question is, is there a good reason why Visual Studio 2013 does not perform return value optimization in this case?
I know of a few workarounds. I can define a move-constructor for Foo
:
Foo(Foo&& in) : v(std::move(in.v)) {}
which is fine, but there are a lot of legacy types out there that don't have move-constructors and it would be nice to know I can rely on RVO with those types. Also, some types may be inherently copyable but not movable.
If I change from RVO to NVRO (named return value optimization) then Visual Studio does appear to perform the optimization:
Foo foo(std::move(v)) return foo;
which is curious because I thought NVRO was less reliable than RVO.
Even more curious is if I change the constructor of Foo
so it creates and fills the vector
:
Foo(size_t num) : v(num) { std::iota(v.begin(), v.end(), 0); // Fill vector with non-trivial data }
instead of moving it in then when I try to do RVO, it works:
Foo getBigFoo() { return Foo(1000000); }
I'm happy to go with one of these workarounds but I'd like to be able to predict when RVO might fail like this in the future, thanks.
Edit: More concise live demo from @dyp
Edit2: Why don't I just write return v;
?
For a start, it doesn't help. Profiler results show that Visual Studio 2013 still copies the vector if I just write return v;
And even if it did work it would only be a workaround. I'm not trying to actually fix this particular piece of code, I'm trying to understand why RVO fails so I can predict when it might fail in the future. It is true that it is a more concise way of writing this particular example but there are plenty of cases where I couldn't just write return v;
, for example if Foo
had additional constructor parameters.
If the code looks like it should be optimized, but is not getting optimized I would submit bug here http://connect.microsoft.com/VisualStudio or raise a support case with Microsoft. This article, although it is for VC++2005 (I couldn't find a current version of document) does explain some scenarios where it won't work. http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx#nrvo_cpp05_topic3
If we want to be sure the optimization has occurred, one possibility is to check the assembly output. This could be automated as a build task if desired.
This requires generating .asm output using /FAs option like so:
cl test.cpp /FAs
Will generate test.asm.
A potential example in PowerShell below, which can be used in this way:
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>
The script:
# 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" } } } } }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With