Що таке оптимізація копіювання і оптимізація повернення?


377

Що таке копіювання elision? Що таке (названа) оптимізація повернення? Що вони мають на увазі?

У яких ситуаціях вони можуть виникнути? Що таке обмеження?


1
Скопіювати елісіон - це один із способів його перегляду; Елісія об'єкта або злиття об'єкта (або плутанина) - це інший погляд.
curiousguy

Я вважаю це посилання корисним.
subtleseeker

Відповіді:


246

Вступ

Для технічного огляду - перейдіть до цієї відповіді .

Для поширених випадків, коли відбувається копіювання елізії, - перейдіть до цієї відповіді .

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

Це єдина форма оптимізації, що виключає (га!) Правило як би - копіювання 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, копіювання 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 - з повернення до темпу, а копія 2 - з temp в obj; 2-е, коли одне з перерахованих вище оптимізовано, ймовірно, копія reutnr відмічена; обидва трибуни відмічені
переможець

2
Хм, але, на мою думку, це ОБОВ'ЯЗКОВО має бути ознакою, на яку можна покластися. Тому що якщо ми не зможемо, це сильно вплине на те, як ми реалізуємо свої функції в сучасному C ++ (RVO vs std :: move). Під час перегляду деяких відео з CppCon 2014 я справді склався враження, що всі сучасні компілятори завжди роблять RVO. Крім того, я десь читав, що також без будь-яких оптимізацій компілятори застосовують це. Але, звичайно, я не впевнений у цьому. Ось чому я прошу.
j00hi

8
@ j00hi: Ніколи не записуйте переміщення у зворотному операторі - якщо rvo не застосовується, значення повернення все одно виводиться за замовчуванням.
MikeMB

96

Стандартна довідка

Для менш технічного перегляду та вступу - перейдіть до цієї відповіді .

Для поширених випадків, коли відбувається копіювання елізії, - перейдіть до цієї відповіді .

Копія elision визначена у стандарті у:

12.8 Копіювання та переміщення об’єктів класу [class.copy]

як

31) Коли певні критерії виконуються, реалізація дозволяється опускати конструкцію копіювання / переміщення об'єкта класу, навіть якщо конструктор копіювання / переміщення та / або деструктор для об'єкта мають побічні ефекти. У таких випадках реалізація розглядає джерело та ціль опущеної операції копіювання / переміщення як просто два різні способи посилання на один і той же об'єкт, і знищення цього об'єкта відбувається в більш пізні часи, коли два об'єкти були б знищено без оптимізації. 123 Це елісія операцій копіювання / переміщення, що називається копія елісією , дозволено за таких обставин (які можуть поєднуватися для усунення кількох копій):

- у операторі return у функції з типом повернення класу, коли вираз - це ім'я енергонезалежного автоматичного об'єкта (окрім параметра функції або параметра "catch") з тим же cvunquified типу, що і тип return return, Операцію копіювання / переміщення можна опустити, побудувавши автоматичний об'єкт безпосередньо у зворотному значенні функції

- у виразі кидка, коли операнд - це ім'я енергонезалежного автоматичного об'єкта (крім параметра функції або параметр-застереження), сфера дії якого не виходить за межі кінця самого внутрішнього опробовуючого блоку (якщо є один) операцію копіювання / переміщення з операнда до об'єкта винятків (15.1) можна опустити, побудувавши автоматичний об'єкт безпосередньо в об'єкт виключення

- коли тимчасовий об'єкт класу, який не був пов'язаний з посиланням (12.2), буде скопійований / переміщений до об'єкта класу з тим же самим некваліфікованим типом cv, операція копіювання / переміщення може бути опущена шляхом побудови тимчасового об'єкта безпосередньо у ціль опущеної копії / переміщення

- коли декларація про виняток обробника винятків (п. 15) оголошує об'єкт одного типу (крім cv-кваліфікації) як об'єкт виключення (15.1), операцію копіювання / переміщення можна опустити, обробляючи декларацію про виключення як псевдонім для об'єкта виключення, якщо значення програми буде незмінним, за винятком виконання конструкторів та деструкторів для об'єкта, оголошеного декларацією про виключення.

123) Оскільки лише один об'єкт знищений замість двох, а один конструктор копіювання / переміщення не виконується, все одно існує один об’єкт, знищений для кожного створеного.

Наведений приклад:

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

і пояснив:

Тут критерії elision можуть бути об'єднані для усунення двох викликів до конструктора копій класу Thing: копіювання локального автоматичного об'єкта tу тимчасовий об'єкт для повернення значення функції f() та копіювання цього тимчасового об’єкта в об’єкт t2. Ефективно, побудова локального об'єкта t може розглядатися як безпосередньо ініціалізація глобального об'єкта t2, і руйнування цього об'єкта буде відбуватися при виході програми. Додавання конструктора переміщення до Thing має той самий ефект, але саме конструкція переміщення з тимчасового об'єкта до t2цього відміняється.


1
Це зі стандарту C ++ 17 або з попередньої версії?
Нілс

90

Поширені форми копіювання елізії

Для технічного огляду - перейдіть до цієї відповіді .

Для менш технічного перегляду та вступу - перейдіть до цієї відповіді .

(Названо) Оптимізація повернутого значення - це поширена форма усунення копій. Він посилається на ситуацію, коли об’єкт, повернутий за значенням методом, втратив свою копію. Приклад, наведений у стандарті, ілюструє оптимізацію повернутого значення , оскільки об'єкт іменований.

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) {  
  }             
}

Поширеними обмеженнями копіювання елізії є:

  • кілька точок повернення
  • умовна ініціалізація

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


4
Мені було б цікаво побачити пункти кулі "Загальні обмеження", які пояснюються трохи ... що робить ці обмежуючі фактори?
телефоніст

@phonetagger Я пов'язував із статтею msdn, сподіваюся, що це очистить деякі речі.
Лучіан Григоре

54

Копіювати elision - це техніка оптимізації компілятора, яка виключає непотрібне копіювання / переміщення об'єктів.

У наступних обставинах компілятору дозволено опускати операції копіювання / переміщення і, отже, не викликати пов'язаного конструктора:

  1. NRVO (оптимізація іменованого зворотного значення) : Якщо функція повертає тип класу за значенням, а вираз оператора return - це ім'я енергонезалежного об'єкта з автоматичною тривалістю зберігання (що не є параметром функції), то скопіювати / перемістити що буде виконано неоптимізуючим компілятором, можна пропустити. У такому випадку повернене значення будується безпосередньо в сховищі, до якого інакше повернуте значення функції було б переміщено або скопійовано.
  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());це NRVO чи RVO? це не отримує тимчасової змінної / об’єкта так само, як ABC xyz = "Stack Overflow";//RVO
Асиф Муштак

3
Щоб мати більш конкретну ілюстрацію RVO, ви можете звернутися до збірки, яку створює компілятор (змініть прапор компілятора -fno-elide-конструктори, щоб побачити різницю). godbolt.org/g/Y2KcdH
Gab 是 好人
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.