Подтвердить что ты не робот

Оптимизация возвращаемого значения и побочные эффекты

Оптимизация возвращаемого значения (RVO) - это метод оптимизации, включающий copy elision, который исключает временный объект, созданный для хранения возвращаемого значения функции в определенных ситуациях. Я понимаю выгоду RVO в целом, но у меня есть пара вопросов.

В этом стандарте говорится об этом в пункте 32 статьи 12.8 раздела этот рабочий проект (выделено мной).

Когда выполняются определенные критерии, реализации разрешено опускать конструкцию копирования/перемещения объекта класса , даже если конструктор copy/move и/или деструктор для объекта имеют побочные эффекты. В таких случаях реализация рассматривает источник и цель пропущенной операции копирования/перемещения как просто два разных способа обращения к одному и тому же объекту, а уничтожение этого объекта происходит в более поздние времена, когда эти два объекта были бы уничтожен без оптимизации.

Затем он перечисляет ряд критериев, когда реализация может выполнить эту оптимизацию.


У меня есть несколько вопросов относительно этой потенциальной оптимизации:

  • Я привык к ограничениям оптимизации, что они не могут изменять наблюдаемое поведение. Это ограничение, похоже, не относится к RVO. Должен ли я когда-нибудь беспокоиться о побочных эффектах, упомянутых в стандарте? Делать угловые случаи там, где это может вызвать проблемы?

  • Что мне нужно делать программисту (или не делать) для того, чтобы эта оптимизация выполнялась?. Например, запрещается ли использование исключения для копирования (из-за move):

std::vector<double> foo(int bar){
    std::vector<double> quux(bar,0);
    return std::move(quux);
}

Изменить

Я разместил это как новый вопрос, потому что конкретные вопросы, о которых я упоминал, не получили прямого ответа в других связанных вопросах.

4b9b3361

Ответ 1

Я привык к ограничениям оптимизации, что они не могут изменять наблюдаемое поведение.

Это правильно. В качестве общего правила, известного как правило as-if, компиляторы могут изменять код, если изменение не наблюдается.

Это ограничение, похоже, не относится к RVO.

Да. Предложение, указанное в OP, дает исключение из правила as-if и позволяет исключить конструкцию копирования, даже если она имеет побочные эффекты. Обратите внимание, что RVO - это всего лишь один случай копирования-элиции (первая маркерная точка в С++ 11 12.8/31).

Должен ли я когда-либо беспокоиться о побочных эффектах, упомянутых в стандарте?

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

Что мне нужно делать программисту (или не делать), чтобы эта оптимизация выполнялась?

В принципе, если возможно, верните локальную переменную (или временную) с тем же самым неавтоматическим типом cv, что и тип возвращаемой функции. Это позволяет RVO, но не обеспечивает его (компилятор может не выполнять RVO).

Например, запрещает ли использование копирования (из-за перемещения) следующее:

// notice that I fixed the OP example by adding <double>
std::vector<double> foo(int bar){
    std::vector<double> quux(bar, 0);
    return std::move(quux);
}

Да, это происходит потому, что вы не возвращаете имя локальной переменной. Это

std::vector<double> foo(int bar){
    std::vector<double> quux(bar,0);
    return quux;
}

позволяет RVO. Можно было бы волноваться, что если RVO не выполняется, то перемещение лучше, чем справляться (что объясняет использование std::move выше). Не беспокойтесь об этом. Все основные компиляторы будут делать RVO здесь (по крайней мере, в выпуске). Даже если компилятор не выполняет RVO, но условия для RVO выполняются, то он попытается сделать ход, а не копию. Таким образом, использование std::move выше, безусловно, сделает ход. Не использовать его, вероятно, не скопирует и не перемещает ничего, а в худшем (маловероятном) случае будет двигаться.

( Обновление: Как указал haohaolee (см. комментарии), следующие пункты неверны. Однако я оставляю их здесь, потому что они предлагают идею, которая может работать для классов, которые не имеют конструктор, принимающий a std::initializer_list (см. ссылку внизу). Для std::vector haohaolee нашел обходное решение.)

В этом примере вы можете заставить RVO (строго говоря, это больше не RVO, но позвольте продолжать этот путь для простоты), возвращая список с бинтом-init, из которого может быть создан тип возврата:

std::vector<double> foo(int bar){
    return {bar, 0}; // <-- This doesn't work. Next line shows a workaround:
    // return {bar, 0.0, std::vector<double>::allocator_type{}};
}

См. этот пост и R. Martinho Fernandes блестящий ответ.

Будьте осторожны! Если тип возврата был std::vector<int>, последний код выше имел бы другое поведение от оригинала. (Это еще одна история.)

Ответ 2

Я очень рекомендую прочитать "Внутри объектной модели С++" Стэнли Б. Липпмана для получения подробной информации и некоторой исторической информации о том, как работает оптимизация значений имен.

Например, в главе 2.1 он должен сказать об именованной оптимизации возвращаемого значения:

В такой функции, как bar(), где все возвращаемые операторы возвращают одно и то же имя, возможно, для самого компилятора оптимизировать функция, заменив аргумент результата для именованного возврата стоимость. Например, учитывая исходное определение bar():

X bar() 
{ 
   X xx; 
   // ... process xx 
   return xx; 
} 

__ результат заменяется на xx компилятором:

void 
bar( X &__result ) 
{ 
   // default constructor invocation 
   // Pseudo C++ Code 
   __result.X::X(); 

   // ... process in __result directly 

   return; 
}

(....)

Хотя оптимизация NRV обеспечивает значительную производительность улучшения, есть несколько критических замечаний по этому подходу. Один что, поскольку оптимизация выполняется молчанием компилятором, было ли это фактически выполнено, не всегда ясно (в частности поскольку несколько компиляторов документируют степень его реализации или независимо от того, реализуется ли она вообще). Во-вторых, поскольку функция усложняется, оптимизация становится сложнее подать выражение. Например, в cfront оптимизация применяется только в том случае, если все именованные операторы return выполняются на верхнем уровне функции. Ввести вложенный локальный блок с оператором return и cfront тихо отключает оптимизацию.

Ответ 3

Он утверждает, что это довольно ясно, не так ли? Это позволяет исключить ctor с побочными эффектами. Поэтому у вас никогда не должно быть побочных эффектов в ctors, или если вы настаиваете, вы должны использовать методы, которые устраняют (N) RVO. Что касается второго, я считаю, что он запрещает NRVO , поскольку std::move создает T&&, а не T, который был бы кандидатом для NRVO (RVO), потому что std::move удаляет имя, а NRVO требует его (спасибо к комментарию @DyP).

Просто протестирован следующий код на MSVC:

#include <iostream>

class A
{
public:
    A()
    {
        std::cout << "Ctor\n";
    }
    A(const A&)
    {
        std::cout << "Copy ctor\n";
    }
    A(A&&)
    {
        std::cout << "Move\n";
    }

};

A foo()
{
    A a;
    return a;
}

int main() 
{
    A a = foo();
    return 0;
}

он производит Ctor, поэтому мы потеряли побочные эффекты для перемещения ctor. И если вы добавите std::move в foo(), вы исключите NRVO.

Ответ 4

  • Это, вероятно, очевидно, но если вы избегаете писать конструкторы copy/move с побочными эффектами (большинство из них не нуждаются в них), тогда проблема полностью спорна. Даже в простых случаях побочных эффектов, таких как построение/уничтожение, все равно должно быть хорошо. Единственный случай, который может волноваться - это сложные побочные эффекты, и сильный запах дизайна для повторного изучения вашего кода.

  • Это звучит как преждевременная оптимизация для меня. Просто напишите очевидный, легко поддерживаемый код и дайте компилятору оптимизировать. Только если профилирование показывает, что определенные области плохо работают, если вы решили принять изменения для повышения производительности.