воскресенье, февраля 10, 2008

RVO и NRVO

Тема RVO была затронута zorgg'ом в одном из комментариев, я решила написать про нее подробнее.

RVO расшифровывается как return value optimization, оптимизация возвращаемого значения. В случае такого вот кода:


std::string foo()
{
    return std::string('a',1000000);
} 
компилятор может не создавать временный объект и сразу же создать строку в возвращаемом объекте. Это старая оптимизация, Скотт Мейерс рассказывает о ней в Правиле 20 второй своей "эффективной" книги. Он особенно напирает на то, что эта оптимизация удобна при переопределении операторов.

Кроме RVO есть еще NRVO, она поновее, ее сложнее было реализовать в компиляторах. NRVO расшифровывается как named return value optimization, оптимизация именованного возращаемого значения. NRVO применяется в случаях чуть отличных от предыдущего.

std::string foo()
{
     std::string  a('a',1000000);
     return a;
} 
Тут Стандарт позволяет компилятору не создавать локальный объект, хотя не требует такого поведения (12.8/15).

При NRVO не просто не происходит вызова конструктора копирования, здесь даже не будет побитового присваивания.

Про NRVO есть хорошая статья на MSDN, где рассказывается как NRVO работает, когда не работает, почему при применении NRVO у компилятора могут появиться проблемы. В статье рассматривается компилятор Visual C++ 2005, но все компиляторы будут делать примерно то же самое. Хороший пример оттуда.
Есть такая функция:

A MyMethod (B &var)
{
   A retVal;
   retVal.member = var.value + bar(var);
   return retVal;
}
Которая вызывается где-то вот таким образом

valA = MyMethod(valB);
Псевдокод, который подробнее рассказывает о том, что будет происходить в функции без NRVO.

A MyMethod (A &_hiddenArg, B &var)
{
   A retVal;
   retVal.A::A(); // constructor for retVal
   retVal.member = var.value + bar(var);
   _hiddenArg.A::A(retVal);  // the copy constructor for A
   return;
retVal.A::~A();  // destructor for retVal
}
А вот с NRVO все происходит несколько быстрее, потому что локальная переменная retVal вообще выбрасывается

A MyMethod(A &_hiddenArg, B &var)
{
   _hiddenArg.A::A();
   _hiddenArg.member = var.value + bar(var);
   Return
}
NRVO можно применить не всегда, в этой MSDN'овской статье рассматриваются случаи, когда при ветвлениях в функции возвращаются разные значения, еще возможны проблемы при работе с исключениями и с ассемблерными вставками.

Чтобы проверить, есть ли в компиляторе NRVO, можно воспользоваться вот таким кусочком кода из блога Стенли Липпмана.

#include <iostream>

using namespace std;
 
class foo {
public:

      foo() { cout <<"foo::foo()\n"; }
      foo( const foo& ){ cout << "foo::foo( const foo& )\n"; }
      ~foo(){ cout << "foo::~foo()\n"; }
};

foo bar(){ foo local_foo; return local_foo; }
 
int main()
{
      foo f = bar();
}
Если при запуске выдалось вот такое, то NRVO не отработало.

foo::foo()
foo::foo( const foo& )
foo::~foo()
foo::~foo()
Возможно, надо поиграть с найстройками оптимизации, а, возможно, компилятор вовсе не умеет NRVO.

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

Ссылки по теме:
Named Return Value Optimization in Visual C++ 2005
comp.lang.c++.moderated - RVO
comp.lang.c++.moderated - Return value optimization
comp.lang.c++.moderated - Return Value Optimization
Несмотря на то, что последние две ссылки называются одинаково, это разные обсуждения

20 коммент.:

Анонимный комментирует...

Спасибо, интересная статья. g++ 4.2.3 NRVO умеет.

Сергей Кондриков комментирует...

Есть интересный пост под забавным названием Do Pigs Fly Yet? с примером оптимизации, которую выполняет компилятор Microsoft и с которой не справляется. Интересно также почитать статью, ссылка на которую указана в этом посте

zrxq комментирует...

2Сергей Кондриков: свиньи не всегда летают в том числе потому, что в отдельных случаях NRVO может наделать сказочного свинства с передачей ссылок и shallow copy.

Анонимный комментирует...

BCB6 NRVO не умеет, RVO - умеет.
gcc - умеет все :)

можно ли из статьи сделать такой вывод: "по возможности стоит полагаться (и использовать) на RVO, а не NRVO" ?

Alena комментирует...

2Анонимный:
BCB6 NRVO не умеет, RVO - умеет.

Я смотрела только на VC++7.1. NRVO не обнаружила.

можно ли из статьи сделать такой вывод: "по возможности стоит полагаться (и использовать) на RVO, а не NRVO" ?

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

Вообще, если код под NRVO легко меняется на код под RVO, как в приведенном у меня примере со строками, то лучше поменять. Далеко не все компиляторы умеют NRVO, а те, кто умеет, с RVO справляются лучше. Но эта ситуация может измениться с течением времени.

Анонимный комментирует...

Алена, а Вы видели библиотеку U++? Там ввели весьма забавную семантику pick, которая позволяет решить не только проблему возврата объектов, но и проблему передачи по значению

Alena комментирует...

2Shire:
Алена, а Вы видели библиотеку U++?

Не видела, но теперь посмотрю, спасибо :-).

Там ввели весьма забавную семантику pick, которая позволяет решить не только проблему возврата объектов, но и проблему передачи по значению

Не могу сказать, что передача по значению для меня является прямо-таки проблемой. Передачей по ссылке все решалось нормально, в ситуации, где бы обязательно надо было передавать "тяжелые" объекты по значению, я не попадала...
Но интересно, посмотрим...

Анонимный комментирует...

Такая ситуация возникает, когда нужно "отдать" функции объект, который владеет ресурсом (буфером в heap). Например, при помещении его в контейнер. В большинстве случаев это решается "в лоб" - добавляют механизм Attach/Detach или выделяют новый буфер и копируют в него, что не есть гуд.

zrxq комментирует...

2shire: Потому что смарт пойнтеры для дураков, да.

Анонимный комментирует...

да-да, смартпойнтер на стековый объект выглядит великолепно. К тому же на ровном месте - лишние выделение-освобождение куска в куче.

zrxq комментирует...

Стековых объектов после возвращения из функции не бывает. В этом-то и вся их прелесть.

Я вот скучаю за alloca.

Анонимный комментирует...

2shire

в u++ "ввели" семантику pick вот так:

#define pick_ const

После того, как этим "пиком" присвоишь чего-нибудь, оригинал удаляется...

То есть, это такой хак. Как только введут какую-то штучку вроде && в стандарт, это всё перестанет быть таким некрасивым, но пока - это хак.

_Andrey_ комментирует...

Проверил на gcc версии 4.1.2, получил такой рез-т:
$ ./a.out
foo::foo()
foo::~foo()

Следовательно NRVO поддерживается этим компилятором.

Victor Sergienko комментирует...

А я-то, наивный нанайский юнош, считал, что RVO от NRVO практически не отличается. И что года с 80-го все компиляторы умеют сводить их к одинаковому коду ещё до генерации машинного кода. Читал какую-то старую книжку об этом приёме.

Конечно, не считая случаев с ветвлениями и сайдэффектами (исключения и ассемблерные вставки).

Интересно, какого свинства может наделать shallow copy, если создаётся всего один объект указанного типа. Если объект сам не свинячит.

Анонимный комментирует...

Меня мучает вопрос.. стоит ли полагаться на RVO, если функция должна возвращать (допустим большой) вектор данных? Или лучше создавать вектор по new и возвращать его адрес? Как пишут в корпоративном сегменте?

Alena комментирует...

kalnitsky
Меня мучает вопрос.. стоит ли полагаться на RVO, если функция должна возвращать (допустим большой) вектор данных?

Я полагаюсь на RVO, но если есть сомнения это легко проверить. Посмотрите на кусок памяти с которым происходит работа, там сразу будет видно, происходит копирование или нет.

Или лучше создавать вектор по new и возвращать его адрес? Как пишут в корпоративном сегменте?

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

Andrey Epifantsev комментирует...

Я попробовал прогнать подобный тест под своим компилятором(Visual C++ 2008), и первоначально обнаружил у себя RVO но не обнаружил NRVO. Затем я увидел, что вновь созданный проект по умолчанию собирается в конфигурации Debug.

Я переключил в конфигурацию Release и под ней NRVO есть!

1. Возможно у тех, кто здесь писал, что у него нет NRVO, просто забыли собрать программу под конфигурацией включающей все оптимизации?
2. Даже в режиме Debug с отключенными оптимизациями оптимизация RVO работает. Это правильно? И это не влияет на удобство отладки?

Alena комментирует...

Andrey Epifantsev
2. Даже в режиме Debug с отключенными оптимизациями оптимизация RVO работает. Это правильно? И это не влияет на удобство отладки?

Угу, так и есть. В GCC есть волшебный ключик, чтобы RVO отключить, про VC++ не знаю.
Мешать будет, только если RVO решишь поизучать.

Kashroot комментирует...

Ссылка неверная (или битая), по ней не открывается статься.
https://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx - верная ссылка, по которой корректно происходит редирект.

Alena комментирует...

Kashroot

Ссылка неверная (или битая), по ней не открывается статься.

Спасибо, поправила