Почему Visual Studio не выполняет оптимизацию возвращаемого значения (RVO) в этом случае

Я отвечал на вопрос и рекомендует возвращать по значению для большого типа, потому что я был уверен, что компилятор выполнит оптимизация возврата (RVO). Но затем мне было указано, что Visual Studio 2013 не выполняет RVO в моем коде.

Я нашел здесь есть вопрос о том, что 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";
  }
}

Я ожидаю, что компилятор выполнит RVO для возвращаемого типа из getBigFoo(). Но, похоже, вместо этого он копирует Foo.

Я знаю, что компилятор создаст конструктор копирования для Foo. Я также знаю, что в отличие от совместимого компилятора C ++ 11 Visual Studio не создает конструктор перемещения для Foo. Но это должно быть нормально, 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

Edit2: Почему бы мне просто не написать return v;?

Во-первых, это не помогает. Результаты профилировщика показывают, что Visual Studio 2013 по-прежнему копирует вектор, если я просто напишу return v;. И даже если бы это сработало, это было бы только обходным путем. Я не пытаюсь исправить этот конкретный фрагмент кода, я пытаюсь понять, почему RVO выходит из строя, чтобы я мог предсказать, когда он может выйти из строя в будущем. Это правда, что это более сжатый способ написания этого конкретного примера, но есть много случаев, когда я не мог просто написать return v;, например, если Foo имел дополнительные параметры конструктора.


person Chris Drew    schedule 21.09.2014    source источник
comment
Полный удар в темноте: наблюдаете ли вы иное поведение, если использовали return Foo(std::move(v)); (круглые скобки) вместо return Foo{std::move(v)}; (фигурные скобки)? Я не думаю, что это будет иметь значение, но я не собираюсь на это делать ставку.   -  person In silico    schedule 22.09.2014
comment
@Insilico, Нет, такое же поведение, я использовал только фигурные скобки (единая инициализация), потому что для удобства это было слишком близко к самому неприятному синтаксическому анализу. Поменяю пока никого не подкинет ...   -  person Chris Drew    schedule 22.09.2014
comment
Конечно, вы можете использовать return {std::move(v)};, поскольку этот конструктор не является явным. Для этого не требуется никакого (N) RVO, это указано, чтобы не создавать временный.   -  person dyp    schedule 22.09.2014
comment
@dyp. Интересно, еще один обходной путь. Думаю, я был бы немного удивлен, если бы увидел такой код, но, может быть, мне стоит начать к нему привыкать.   -  person Chris Drew    schedule 22.09.2014
comment
Почему бы тебе просто не написать return v;?   -  person Marc Glisse    schedule 22.09.2014
comment
@MarcGlisse, похоже, страдает той же проблемой.   -  person Chris Drew    schedule 22.09.2014
comment
Я только что попробовал его на Visual Studio 2014 CTP, и он применяет RVO для вашего кода. РЕДАКТИРОВАТЬ: пример @dyp, я должен сказать.   -  person Jagannath    schedule 22.09.2014
comment
вы компилируете в режиме Release? (Известно, что VS в некоторых случаях подавляет RVO в режиме отладки)   -  person M.M    schedule 22.09.2014
comment
@McNabb Да, компиляция в режиме выпуска.   -  person Chris Drew    schedule 22.09.2014
comment
@dyp Интересно, что я могу добиться того, чтобы ваш первый пример не провалился, когда я изменяю конструктор Foo, чтобы он принимал rvalue ref вместо перемещенного значения, живой пример   -  person Michael Karcher    schedule 21.10.2014
comment
RVO - это особая оптимизация, поскольку она может изменить поведение вашей программы. Вы можете легко протестировать RVO, вернув экземпляр своего собственного класса и выйдя из вызовов конструктора.   -  person pasztorpisti    schedule 21.10.2014
comment
@pasztorpisti Я знаю. Несомненно, RVO терпит неудачу. Я добавил ссылку на живую демонстрацию dyp, которая делает именно то, что вы предлагаете. Но я хотел показать, что это действительно повлияло на производительность, поэтому мой пример написан как есть.   -  person Chris Drew    schedule 21.10.2014
comment
@MichaelKarcher В вашем живом примере все еще печатается fail? std::move не предотвращает копирование / перемещение исключения копирования / перемещения Foo временного значения в возвращаемое значение функции.   -  person dyp    schedule 21.10.2014
comment
@dyp Извините, я еще не понял разницы между режимом вилки и режимом редактирования. Я хотел подчеркнуть, что изменение конструктора foo на Foo(vec_t && _v) : v(std::move(_v)) {} не приводит к сбою, но я напортачил. Скорее всего, живой пример сейчас верен.   -  person Michael Karcher    schedule 21.10.2014
comment
@MichaelKarcher А, теперь я понял. Это действительно странно: в вашем примере выполняется RVO, а в моем нет. Я помню, что читал что-то о том, что VS имеет проблемы с RVO из-за раскручивания стека в случае исключения. В моем примере необходимо создать временный vector. Может быть, это основная проблема. (Но я не могу найти никаких ссылок в банкомате.)   -  person dyp    schedule 21.10.2014
comment
@kuroineko: Что за чушь.   -  person Lightness Races in Orbit    schedule 09.11.2014
comment
@ChrisDrew: Лол, как получить самый неприятный синтаксический анализ операнда оператора return?   -  person Lightness Races in Orbit    schedule 09.11.2014
comment
Отправьте указатель (или интеллектуальный указатель) на функцию getBigFoo. VS умеет делать странные вещи. Дважды проверьте параметры компиляции, выпуск, оптимизацию ...   -  person Juan Chô    schedule 11.11.2014
comment
Посмотрите здесь connect.microsoft.com/VisualStudio/feedback/details/846490/   -  person Elvis Dukaj    schedule 12.11.2014
comment
@ elvis.dukaj Это определенно выглядит связанным, хотя это не одно и то же. В этом случае я не использую конструктор по умолчанию.   -  person Chris Drew    schedule 12.11.2014
comment
Я знаю, что это не поможет вам понять вашу проблему, но кажется неразумным доверять компиляторам делать то, что они могут делать, но не обязаны ... даже если вы считаете, что можете надежно предсказать поведение из один конкретный компилятор от одного конкретного поставщика, поскольку ваш код может когда-нибудь быть перенесен на другие платформы, или его поведение может измениться со следующей версией компилятора.   -  person lvella    schedule 12.11.2014
comment
@Ivella Может быть, ты прав. Может быть, хорошее практическое правило - использовать возврат по значению только в том случае, если тип является дешевым для перемещения, тогда это не конец света, если компилятор не сможет исключить перемещение. Хотя это кажется позором. Возврат по стоимости намного приятнее, чем альтернативы, и работает большую часть времени.   -  person Chris Drew    schedule 12.11.2014
comment
Это, вероятно, не связано, но я помню более раннюю версию Visual C ++, где RVO не запускался, если возвращаемое значение не было const.   -  person zumalifeguard    schedule 12.11.2014
comment
Вы пробовали использовать этот код с классами вместо структур? Вы можете быть удивлены результатами. Я исследую и вернусь к вам относительно обработки VS2013 структур и классов.   -  person GMasucci    schedule 14.11.2014
comment
Я опубликовал некоторые подробности о том, когда выполняется RVO, а когда он терпит неудачу (на основе примера из @dyp) здесь: rovrov.com/blog/2014/11/21/RVO-and-copy-elision-failing. Это не объясняет, почему RVO дает сбой, но некоторые наблюдения могут быть интересны.   -  person Roman L    schedule 22.11.2014
comment
@LightnessRacesinOrbit кто-то подверг цензуре мою полную чушь, но я могу понять, что человек, зарабатывающий на жизнь, объясняющий, что struct : bar {} foo {}; означает для компилятора C ++, может быть щепетильным по поводу врожденной запутанности и нестабильности ее любимого языка.   -  person kuroi neko    schedule 11.12.2014
comment
@kuroineko: Что? Не похоже на Я обидчивый, приятель ... Что в твоем комментарии? Не помню.   -  person Lightness Races in Orbit    schedule 11.12.2014
comment
Я отправил ошибка в Microsoft. Отзывов пока нет.   -  person Chris Drew    schedule 11.12.2014


Ответы (1)


Если код выглядит так, как будто его следует оптимизировать, но не оптимизируется, я бы отправил сообщение об ошибке здесь http://connect.microsoft.com/VisualStudio или обратитесь в Microsoft. В этой статье, хотя она и для VC ++ 2005 (я не смог найти текущую версию документа), объясняются некоторые сценарии, в которых это не сработает. http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx#nrvo_cpp05_topic3

Если мы хотим быть уверены, что оптимизация произошла, одна из возможностей - проверить вывод сборки. При желании это можно автоматизировать как задачу сборки.

Для этого требуется сгенерировать вывод .asm с использованием параметра / FAs, например:

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"
                }
            }
        }
    }

}
person Malcolm McCaffery    schedule 17.11.2014
comment
Я действительно не хочу проверять сборку каждый раз, когда хочу использовать RVO! Если это необходимо, думаю, я просто не буду полагаться на RVO. Я ценю инструкции о том, как сгенерировать / проверить сборку. Я подтвердил наблюдение Джаганнатха о том, что это исправлено в Visual Studio 2014 CTP, поэтому, вероятно, нет смысла сообщать об ошибке. - person Chris Drew; 20.11.2014
comment
Это единственный возможный способ быть на 100% уверенным, потому что даже если вы будете следовать логике, упомянутой в статье Microsoft, которую я связал, может быть ошибка компилятора или вы попадаете в неочевидный сценарий. Однако скрипт можно использовать для автоматизации проверки после сборки, если это очень важно. Если вы действительно хотите знать, проверьте реальную логику, я думаю, вам придется использовать компилятор с открытым исходным кодом CLang или GCC. - person Malcolm McCaffery; 20.11.2014
comment
При этом, если у вас действительно хорошо задокументированный, воспроизводимый сценарий, Microsoft обычно отреагирует на отчет об ошибке, это может быть сделано намеренно, но, по крайней мере, вы можете получить причину. - person Malcolm McCaffery; 20.11.2014
comment
@ChrisDrew Если это необходимо, думаю, я просто не буду полагаться на RVO. - По возможности, я твердо считаю, что именно так и следует поступать. Всегда будут программы, в которых RVO разрешено стандартом, но не выполняется компилятором. Практически невозможно гарантировать, что RVO всегда выполняется всякий раз, когда это разрешено, потому что при построении объекта, который должен быть возвращен, компилятор может быть не в состоянии определить, какой оператор return будет выполнен в конечном итоге. Оптимизируйте конструкторы ходов, и RVO будет иметь значительно меньшее влияние. - person ; 29.03.2015
comment
@ChrisDrew И, к сожалению, это означает подробное описание каждого конструктора перемещения, если вы имеете дело с компилятором, который не будет генерировать их неявно, даже если в стандарте это указано. - person ; 29.03.2015