Почему следует полагаться на оптимизацию именованных возвращаемых значений?

Я читал о NRVO и пытался понять, когда следует полагаться на него, а когда нет. Теперь у меня вопрос: зачем вообще полагаться на NRVO? Всегда можно явно передать возвращаемый параметр по ссылке, поэтому есть ли причина полагаться на NRVO?

4 ответа

Решение

Работать с возвращаемыми значениями проще, чем с методами, которые возвращаются путем записи в ссылочный параметр. Рассмотрим следующие 2 метода

C GetByRet() { ... }
void GetByParam(C& returnValue) { ... }

Первая проблема состоит в том, что это делает невозможным цепочку вызовов методов

Method(GetByRet());  
// vs. 
C temp;
GetByParam(temp);
Method(temp);

Это также делает такие функции, как auto невозможно использовать. Не такая большая проблема для такого типа, как C но более важно для таких типов, как std::map<std::string, std::list<std::string>*>

auto ret = GetByRet();
// vs.
auto value; // Error! 
GetByParam(value);

Также, как указал GMacNickG, что если тип C есть частный конструктор, который нормальный код не может использовать? Может быть, конструктор private или просто нет конструктора по умолчанию. Снова GetByRet работает как чемпион и GetByParam терпит неудачу

C ret = GetByRet();  // Score! 
// vs.
C temp; // Error! Can't access the constructor 
GetByParam(temp);

Это не ответ, но это также ответ в некотором смысле...

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

void f(T *ptr) {     
   // uses ptr->...
}
  1. Добавьте ссылку на объект в функцию и замените все виды использования ptr ссылкой

    void f(T *ptr) { T & obj = *ptr; /* uses obj. instead of ptr-> */ }

  2. Теперь удалите аргумент, добавьте возвращаемый тип, замените T& obj с T obj и измените все возвраты, чтобы получить 'obj'

    T f() { T obj; // No longer a ref! /* code does not change */ return obj; }

  3. На данный момент у вас есть функция, которая возвращает значение, для которого NRVO является тривиальным, поскольку все операторы возврата ссылаются на один и тот же объект.

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

Точно такая же стоимость?

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

T f();

Соглашение о вызовах преобразует это в:

void mangled_name_for_f( T* __result )

Итак, если вы сравните альтернативы: T t; f(&t); а также T t = f(); в обоих случаях сгенерированный код выделяет пространство в кадре вызывающей стороны, [1] вызывает функцию, передающую указатель. В конце функции компилятор вернет [2]. Где [#] - это место, где конструктор объекта фактически вызывается в каждой из альтернатив. Стоимость обеих альтернатив одинакова, с той разницей, что в [1] объект должен быть построен по умолчанию, в то время как в [2] вы уже знаете конечные значения объекта и сможете сделать что-то более эффективное.

Что касается производительности, это все, что есть?

На самом деле, нет. Если позже вам нужно передать этот объект в функцию, которая принимает аргумент по значению, скажите void g(T value) в случае передачи по указателю в стеке вызывающего объекта есть именованный объект, поэтому объект должен быть скопирован (или перемещен) в место, где соглашение о вызовах требует наличия аргумента value. В случае возврата по значению компилятор, зная, что он вызовет g(f()) знает, что единственное использование возвращенного объекта из f() является аргументом g() так что он может просто передать указатель на соответствующее место при вызове f(), что означает, что не будет никаких копий. На этом этапе ручной подход действительно начинает отставать от подхода компилятора, даже если реализация f использует тупое преобразование выше!

T obj;    // default initialize
f(&obj);  // assign (or modify in place)
g(obj);   // copy

g(f());   // single object is returned and passed to g(), no copies

Фактически НЕ возможно (или желательно) всегда возвращать значение по ссылке (подумайте о operator+ в качестве основного контрпример).

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

РЕДАКТИРОВАТЬ для some function could be optimized just by using return parameter:

Во-первых, помните, что если функция вызывается не часто или у компилятора достаточно умных умений, вы не можете гарантировать, что параметр return-by-out является оптимизацией. Во-вторых, помните, что у вас будут будущие сопровождающие кода, и что написание понятного кода, пригодного для работы с кодами, - одна из самых больших подсказок, которую вы можете предоставить (не важно, насколько быстро работает поврежденный код). В-третьих, найдите минутку и прочитайте http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/ и посмотрите, не изменит ли это ваше мнение.

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

Кроме того, есть много предопределенных операторов, которые возвращают свои результаты по значению (например, арифметические операторы, такие как operator+, operator-, так далее...). Поскольку вы хотите сохранить семантику (и сигнатуру) по умолчанию для таких операторов, вы вынуждены полагаться на NRVO для оптимизации временного объекта, возвращаемого по значению.

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

Другие вопросы по тегам