Что такое оптимизация копирования и возврата значений?

377

Что такое копирование? Что такое (названная) оптимизация возвращаемого значения? Что они подразумевают?

В каких ситуациях они могут возникнуть? Какие ограничения?

Лучиан Григоре
источник
1
Копирование elision - один из способов взглянуть на это; исключение объекта или слияние объекта (или смешение) - это другой взгляд.
любопытный парень
Я нашел эту ссылку полезной.
соискатель

Ответы:

246

Введение

Для технического обзора - перейдите к этому ответу .

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

Копирование elision - это оптимизация, реализованная большинством компиляторов для предотвращения лишних (потенциально дорогих) копий в определенных ситуациях. Это делает возврат по значению или передачу по значению осуществимым на практике (применяются ограничения).

Это единственная форма оптимизации, которая исключает (ха!) Правило «как будто» - исключение копирования может применяться, даже если копирование / перемещение объекта имеет побочные эффекты .

Следующий пример взят из Википедии :

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C();
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f();
}

В зависимости от компилятора и настроек допустимы следующие выходные данные :

Привет мир!
Копия была сделана.
Копия была сделана.


Привет мир!
Копия была сделана.


Привет мир!

Это также означает, что можно создавать меньше объектов, поэтому вы также не можете полагаться на определенное количество вызываемых деструкторов. У вас не должно быть критической логики внутри конструкторов копирования / перемещения или деструкторов, так как вы не можете полагаться на их вызов.

Если вызов конструктора копирования или перемещения исключен, этот конструктор должен все еще существовать и быть доступным. Это гарантирует, что исключение копирования не позволяет копировать объекты, которые обычно не копируются, например, потому что они имеют закрытый или удаленный конструктор копирования / перемещения.

C ++ 17 : Начиная с C ++ 17, Copy Elision гарантируется, когда объект возвращается напрямую:

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C(); //Definitely performs copy elision
}
C g() {
    C c;
    return c; //Maybe performs copy elision
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f(); //Copy constructor isn't called
}
Лучиан Григоре
источник
2
Не могли бы вы объяснить, когда 2-й выход происходит, а когда 3-й?
zhangxaochen
3
@zhangxaochen, когда и как компилятор решает оптимизировать этот путь.
Лучиан Григоре
10
@zhangxaochen, 1-й вывод: копия 1 из возврата в temp, а копия 2 из temp в obj; 2-й, когда один из вышеперечисленных оптимизирован, вероятно, копия reutnr удаляется; три оба элидированы
победитель
2
Хм, но, на мой взгляд, это ДОЛЖНО быть функцией, на которую мы можем положиться. Потому что если мы не сможем, это серьезно повлияет на то, как мы реализуем наши функции в современном C ++ (RVO vs std :: move). Во время просмотра некоторых видеороликов CppCon 2014 у меня сложилось впечатление, что все современные компиляторы всегда делают RVO. Кроме того, я где-то читал, что и без каких-либо оптимизаций компиляторы применяют его. Но, конечно, я не уверен в этом. Вот почему я спрашиваю.
j00hi
8
@ j00hi: Никогда не записывайте перемещение в операторе возврата - если rvo не применяется, возвращаемое значение все равно удаляется по умолчанию.
MikeMB
96

Стандартная ссылка

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

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

Разрешение на копирование определяется в стандарте в:

12.8 Копирование и перемещение объектов класса [class.copy]

в виде

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

- в операторе возврата в функции с типом возврата класса, когда выражение является именем энергонезависимого автоматического объекта (кроме параметра функции или предложения catch) с тем же типом cvunqualified, что и тип возврата функции, Операцию копирования / перемещения можно опустить, создав автоматический объект непосредственно в возвращаемое значение функции.

- в выражении throw, когда операндом является имя энергонезависимого автоматического объекта (кроме параметра функции или предложения catch), область которого не выходит за пределы самого внутреннего включающего блока try (если есть 1) операция копирования / перемещения из операнда в объект исключения (15.1) может быть опущена путем создания автоматического объекта непосредственно в объект исключения

- когда временный объект класса, который не был связан со ссылкой (12.2), будет скопирован / перемещен в объект класса с тем же типом cv-unqualified, операция копирования / перемещения может быть опущена путем создания временного объекта непосредственно в цель пропущенного копирования / перемещения

- когда объявление исключения в обработчике исключений (пункт 15) объявляет объект того же типа (за исключением cv-квалификации), что и объект исключения (15.1), операцию копирования / перемещения можно опустить, обработав объявление исключения в качестве псевдонима для объекта исключения, если значение программы не изменится, за исключением выполнения конструкторов и деструкторов для объекта, объявленного объявлением исключения.

123) Поскольку уничтожается только один объект вместо двух, а один конструктор копирования / перемещения не выполняется, для каждого созданного объекта все еще остается один объект.

Пример приведен ниже:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

и объяснил:

Здесь критерии исключения могут быть объединены, чтобы исключить два вызова конструктора копирования класса Thing: копирование локального автоматического объекта tво временный объект для возвращаемого значения функции f() и копирование этого временного объекта в объект t2. По сути, создание локального объекта t можно рассматривать как непосредственную инициализацию глобального объекта t2, и разрушение этого объекта произойдет при выходе из программы. Добавление конструктора перемещения в Thing имеет тот же эффект, но это конструкция перемещения из временного объекта в t2который исключается.

Лучиан Григоре
источник
1
Это из стандарта C ++ 17 или из более ранней версии?
Нильс
90

Распространенные формы исключения

Для технического обзора - перейдите к этому ответу .

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

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

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

Регулярная оптимизация возвращаемого значения происходит при возвращении временного:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  return Thing();
}
Thing t2 = f();

Другие распространенные места, где исключение копии имеет место, когда временное значение передается по значению :

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
void foo(Thing t);

foo(Thing());

или когда исключение выдается и перехватывается значением :

struct Thing{
  Thing();
  Thing(const Thing&);
};

void foo() {
  Thing c;
  throw c;
}

int main() {
  try {
    foo();
  }
  catch(Thing c) {  
  }             
}

Общие ограничения права на копирование:

  • несколько точек возврата
  • условная инициализация

Большинство компиляторов коммерческого класса поддерживают функцию копирования elision & (N) RVO (в зависимости от настроек оптимизации).

Лучиан Григоре
источник
4
Мне было бы интересно увидеть объяснения пункта «Общие ограничения», немного объяснившие ... что делает эти ограничивающие факторы?
телефонный звонок
@phonetagger Я связался со статьей msdn, надеюсь, это кое-что прояснит.
Лучиан Григоре
54

Copy elision - это метод оптимизации компилятора, который исключает ненужное копирование / перемещение объектов.

В следующих обстоятельствах компилятору разрешено пропускать операции копирования / перемещения и, следовательно, не вызывать связанный конструктор:

  1. NRVO (оптимизация именованных возвращаемых значений) : если функция возвращает тип класса по значению, а выражением оператора возврата является имя энергонезависимого объекта с автоматической продолжительностью хранения (который не является параметром функции), то копирование / перемещение это может быть выполнено неоптимизирующим компилятором. Если это так, возвращаемое значение создается непосредственно в хранилище, в которое в противном случае возвращаемое значение функции было бы перемещено или скопировано.
  2. RVO (оптимизация возвращаемого значения) : если функция возвращает безымянный временный объект, который был бы перемещен или скопирован в место назначения наивным компилятором, копирование или перемещение могут быть опущены согласно пункту 1.
#include <iostream>  
using namespace std;

class ABC  
{  
public:   
    const char *a;  
    ABC()  
     { cout<<"Constructor"<<endl; }  
    ABC(const char *ptr)  
     { cout<<"Constructor"<<endl; }  
    ABC(ABC  &obj)  
     { cout<<"copy constructor"<<endl;}  
    ABC(ABC&& obj)  
    { cout<<"Move constructor"<<endl; }  
    ~ABC()  
    { cout<<"Destructor"<<endl; }  
};

ABC fun123()  
{ ABC obj; return obj; }  

ABC xyz123()  
{  return ABC(); }  

int main()  
{  
    ABC abc;  
    ABC obj1(fun123());//NRVO  
    ABC obj2(xyz123());//NRVO  
    ABC xyz = "Stack Overflow";//RVO  
    return 0;  
}

**Output without -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor    
Constructor  
Constructor  
Constructor  
Destructor  
Destructor  
Destructor  
Destructor  

**Output with -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors    
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Destructor  
Destructor  
Destructor  
Destructor  

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

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

#include <iostream>     
int n = 0;    
class ABC     
{  public:  
 ABC(int) {}    
 ABC(const ABC& a) { ++n; } // the copy constructor has a visible side effect    
};                     // it modifies an object with static storage duration    

int main()   
{  
  ABC c1(21); // direct-initialization, calls C::C(42)  
  ABC c2 = ABC(21); // copy-initialization, calls C::C( C(42) )  

  std::cout << n << std::endl; // prints 0 if the copy was elided, 1 otherwise
  return 0;  
}

Output without -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp  
root@ajay-PC:/home/ayadav# ./a.out   
0

Output with -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors  
root@ajay-PC:/home/ayadav# ./a.out   
1

GCC предоставляет -fno-elide-constructorsвозможность отключить копирование elision. Если вы хотите избежать возможного копирования, используйте -fno-elide-constructors.

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

Вывод

При каждом исключении копии одна конструкция и одно соответствующее уничтожение копии опускаются, что экономит время ЦП, а один объект не создается, тем самым экономя место на фрейме стека.

Аджай Ядав
источник
6
утверждение ABC obj2(xyz123());это НРВО или РВО? разве он не получает временную переменную / объект такой же как ABC xyz = "Stack Overflow";//RVO
Асиф Муштак
3
Чтобы иметь более конкретную иллюстрацию RVO, вы можете обратиться к сборке, которую генерирует компилятор (измените флаг компилятора -fno-elide-constructors, чтобы увидеть diff). godbolt.org/g/Y2KcdH
Габ 是 好人