web-dev-qa-db-fra.com

Que sont l'optimisation de la copie et l'optimisation de la valeur de retour?

Qu'est-ce que la copie élision? Qu'est-ce que l'optimisation de la valeur de retour (nommée)? Qu'impliquent-ils?

Dans quelles situations peuvent-ils se produire? Quelles sont les limitations?

336
Luchian Grigore

Introduction

Pour un aperçu technique - passez à cette réponse .

Pour les cas courants où la copie se produit - passez à cette réponse .

La suppression de copies est une optimisation mise en œuvre par la plupart des compilateurs pour éviter les copies supplémentaires (potentiellement coûteuses) dans certaines situations. Cela rend possible le retour par valeur ou par valeur par passage (des restrictions s'appliquent).

C'est la seule forme d'optimisation qui élide (ha!) La règle as-if - l'élision de copie peut être appliquée même si copier/déplacer l'objet a des effets secondaires .

L'exemple suivant tiré de Wikipedia :

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();
}

En fonction du compilateur et des paramètres, les sorties suivantes sont toutes valides :

Bonjour le monde!
Une copie a été faite.
Une copie a été faite.


Bonjour le monde!
Une copie a été faite.


Bonjour le monde!

Cela signifie également que moins d'objets peuvent être créés, vous ne pouvez donc pas compter sur un nombre spécifique de destructeurs appelés. Vous ne devriez pas avoir de logique critique dans les constructeurs/destructeurs de copy/move, car vous ne pouvez pas vous fier à leur appel.

Si un appel à un constructeur de copie ou de déplacement est supprimé, ce constructeur doit toujours exister et doit être accessible. Cela garantit que la copie ne permet pas de copier des objets qui ne sont normalement pas copiables, par exemple. parce qu’ils ont un constructeur de copie/déplacement privé ou supprimé.

C++ 17 : à partir de C++ 17, la copie de l'élision est garantie lorsqu'un objet est renvoyé directement:

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
}
217
Luchian Grigore

Référence standard

Pour une vue et une introduction moins techniques - passez à cette réponse .

Pour les cas courants où la copie se produit - passez à cette réponse .

Copier l'élision est défini dans la norme dans:

12.8 Copier et déplacer des objets de classe [class.copy]

comme

31) Lorsque certains critères sont remplis, une implémentation est autorisée à omettre la construction de copie/déplacement d'un objet de classe, même si le constructeur et/ou le destructeur de copie/déplacement de l'objet ont des effets secondaires. Dans de tels cas, l’implémentation traite la source et la cible de l’opération de copie/déplacement omise comme simplement deux manières différentes de faire référence au même objet, et la destruction de cet objet se produit au plus tard des moments où les deux objets auraient été détruit sans l'optimisation.123 Cette élision d'opérations de copie/déplacement, appelée copie d'élision, est autorisée dans les cas suivants (pouvant être combinés pour éliminer plusieurs copies):

- dans une instruction return dans une fonction avec un type de retour de classe, lorsque l'expression est le nom d'un objet automatique non volatile (autre qu'un paramètre function ou catch-clause) avec le même type cvunqualified que le type de retour de la fonction, l'opération de copie/déplacement peut être omise en construisant l'objet automatique directement dans la valeur de retour de la fonction

- dans une expression de rejet, lorsque l'opérande est le nom d'un objet automatique non volatile (autre qu'un paramètre function ou catch-clause) dont la portée ne s'étend pas au-delà de la fin du bloc try le plus interne (s'il existe 1), l'opération de copie/déplacement de l'opérande vers l'objet exception (15.1) peut être omise en construisant l'objet automatique directement dans l'objet exception

- lorsqu'un objet de classe temporaire qui n'a pas été lié à une référence (12.2) serait copié/déplacé vers un objet de classe avec le même type cv-non qualifié, l'opération de copie/déplacement peut être omise en construisant l'objet temporaire directement dans le cible de la copie ou du déplacement omis

- lorsque la déclaration d'exception d'un gestionnaire d'exception (clause 15) déclare un objet du même type (à l'exception de cv-qualification) en tant qu'objet exception (15.1), l'opération de copie/déplacement peut être omise en traitant la déclaration d'exception comme alias pour l'objet exception si la signification du programme restera inchangée, à l'exception de l'exécution des constructeurs et des destructeurs pour l'objet déclaré par la déclaration d'exception.

123) Comme un seul objet est détruit au lieu de deux, et qu'un constructeur de copie/déplacement n'est pas exécuté, il reste un objet détruit pour chaque objet construit.

L'exemple donné est:

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

et a expliqué:

Ici, les critères d'élision peuvent être combinés pour éliminer deux appels au constructeur de copie de la classe Thing: la copie de l'objet automatique local t dans l'objet temporaire pour la valeur de retour de la fonction f() et la copie de cet objet temporaire dans l'objet t2. En réalité, la construction de l’objet local t peut être considérée comme initialisant directement l’objet global t2, et la destruction de cet objet aura lieu à la sortie du programme. L'ajout d'un constructeur de déplacement à Thing a le même effet, mais c'est la construction de déplacement de l'objet temporaire vers t2 qui est supprimée.

89
Luchian Grigore

Formes courantes d'élision de copie

Pour un aperçu technique - passez à cette réponse .

Pour une vue et une introduction moins techniques - passez à cette réponse .

(Nommé) L'optimisation de la valeur de retour est une forme courante d'élision de copie. Il fait référence à la situation dans laquelle un objet renvoyé en valeur par une méthode a sa copie supprimée. L'exemple présenté dans la norme illustre l'optimisation de la valeur de retour nommée , puisque l'objet est nommé.

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

L'optimisation de la valeur de retour régulière se produit lorsqu'un temporaire est renvoyé:

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

Les autres endroits courants où la copie a lieu sont lorsqu’un temporaire est passé par valeur :

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

foo(Thing());

ou lorsqu'une exception est levée et interceptée par valeur :

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

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

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

Les limitations courantes de la copie sont:

  • points de retour multiples
  • initialisation conditionnelle

La plupart des compilateurs de qualité commerciale prennent en charge la résolution de copie et le (N) RVO (en fonction des paramètres d'optimisation).

82
Luchian Grigore

L'élision de copie est une technique d'optimisation du compilateur qui élimine les copies/déplacements inutiles d'objets.

Dans les circonstances suivantes, un compilateur est autorisé à omettre les opérations de copie/déplacement et donc à ne pas appeler le constructeur associé:

  1. NRVO (Optimisation de la valeur de retour nommée): Si une fonction retourne un type de classe par valeur et que l'expression de l'instruction return est le nom d'un objet non volatile avec une durée de stockage automatique (ce qui n'est pas un paramètre de fonction ), la copie/le déplacement effectué par un compilateur non optimiseur peut être omis. Si tel est le cas, la valeur renvoyée est construite directement dans la mémoire sur laquelle la valeur renvoyée par la fonction serait autrement déplacée ou copiée.
  2. RVO (Optimisation de la valeur de retour): Si la fonction retourne un objet temporaire sans nom qui serait déplacé ou copié dans la destination par un compilateur naïf, la copie ou le déplacement peut être omis comme indiqué en 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  

Même lorsque la copie est effectuée et que le constructeur de copie/déplacement n'est pas appelé, il doit être présent et accessible (comme si aucune optimisation ne s'était produite), sinon le programme serait mal formé.

Vous ne devez autoriser une telle copie que dans des endroits où cela n’affectera pas le comportement observable de votre logiciel. L'élision de copie est la seule forme d'optimisation autorisée à avoir (c'est-à-dire une élide) des effets secondaires observables. Exemple:

#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 fournit l'option -fno-elide-constructors pour désactiver la copie. Si vous voulez éviter une copie possible, utilisez -fno-elide-constructors.

Désormais, presque tous les compilateurs fournissent des informations sur la copie lorsque l'optimisation est activée (et si aucune autre option n'est définie pour la désactiver).

Conclusion

Avec chaque copie de copie, une construction et une destruction correspondante de la copie sont omises, économisant ainsi le temps CPU, et un objet n'étant pas créé, économisant ainsi de l'espace sur le cadre de la pile.

47
Ajay yadav