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

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

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

Ответы

Ответ 1

Введение

Технический обзор - пропустите этот ответ.

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

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

Это единственная форма оптимизации, которая исключает (ha!) Правило as-if - copy 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();
}

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

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


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


Привет мир!

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

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

С++ 17: Начиная с С++ 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

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

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

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

Копирование elision определено в стандарте в:

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

а

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

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

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

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

- когда объявление исключения обработчика исключений (раздел 15) объявляет объект того же типа (за исключением cv-qualification) в качестве объекта исключения (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, который отклоняется.

Ответ 3

Общие формы копирования elision

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

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

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

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

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

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

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

Ответ 4

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

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

  • NRVO (Именованная оптимизация возвращаемого значения). Если функция возвращает тип класса по значению, а выражение оператора return - это имя энергонезависимого объекта с автоматической продолжительностью хранения (что не является параметр функции), то копирование/перемещение, которое будет выполняться не оптимизирующим компилятором, можно опустить. Если это так, возвращаемое значение создается непосредственно в хранилище, к которому в противном случае возвращаемое значение функции было бы перемещено или скопировано.
  • 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**  
[email protected]:/home/ajay/c++# ./a.out   
Constructor    
Constructor  
Constructor  
Constructor  
Destructor  
Destructor  
Destructor  
Destructor  

**Output with -fno-elide-constructors**  
[email protected]:/home/ajay/c++# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors    
[email protected]:/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  

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

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

#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  
[email protected]:/home/ayadav# g++ -std=c++11 copy_elision.cpp  
[email protected]:/home/ayadav# ./a.out   
0

Output with -fno-elide-constructors  
[email protected]:/home/ayadav# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors  
[email protected]:/home/ayadav# ./a.out   
1

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

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

Заключение

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