Wat zijn kopieerelisie- en retourwaarde-optimalisatie?

Wat is kopieerelisie? Wat is (genoemd) rendementsoptimalisatie? Wat houden ze in?

In welke situaties kunnen ze voorkomen? Wat zijn beperkingen?


Antwoord 1, autoriteit 100%

Inleiding

Voor een technisch overzicht – ga naar dit antwoord.

Voor veelvoorkomende gevallen waarin kopieerelisie optreedt: ga door naar dit antwoord.

Kopieerelisie is een optimalisatie die door de meeste compilers wordt geïmplementeerd om in bepaalde situaties extra (potentieel dure) kopieën te voorkomen. Het maakt het retourneren op waarde of pass-by-waarde in de praktijk mogelijk (beperkingen zijn van toepassing).

Het is de enige vorm van optimalisatie die (ha!) de as-if-regel elimineert – kopieerelisie kan worden toegepast, zelfs als het kopiëren/verplaatsen van het object bijwerkingen heeft.

Het volgende voorbeeld uit 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();
}

Afhankelijk van de compiler & instellingen, zijn de volgende uitgangen allemaal geldig:

Hallo wereld!
Er is een kopie gemaakt.
Er is een kopie gemaakt.


Hallo wereld!
Er is een kopie gemaakt.


Hallo wereld!

Dit betekent ook dat er minder objecten kunnen worden gemaakt, dus je kunt er ook niet op vertrouwen dat een bepaald aantal destructors wordt aangeroepen. Je mag geen kritische logica hebben in copy/move-constructors of destructors, omdat je er niet op kunt vertrouwen dat ze worden aangeroepen.

Als een aanroep van een kopieer- of verplaatsingsconstructor wordt weggelaten, moet die constructor nog steeds bestaan ​​en toegankelijk zijn. Dit zorgt ervoor dat kopieerelisie niet toestaat dat objecten worden gekopieerd die normaal niet kunnen worden gekopieerd, b.v. omdat ze een privé of verwijderde copy/move-constructor hebben.

C++17: vanaf C++17 is Copy Elision gegarandeerd wanneer een object direct wordt geretourneerd:

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
}

Antwoord 2, autoriteit 36%

Standaard referentie

Voor een minder technisch beeld & introductie – ga door naar dit antwoord.

Voor veelvoorkomende gevallen waarin kopieerelisie optreedt: ga door naar dit antwoord.

Kopieer elisiewordt gedefinieerd in de standaard in:

12.8 Klassenobjecten kopiëren en verplaatsen [class.copy]

als

31) Wanneer aan bepaalde criteria wordt voldaan, mag een implementatie de kopieer-/verplaatsingsconstructie van een klasse weglaten
object, zelfs als de copy/move-constructor en/of destructor voor het object neveneffecten hebben. In dergelijke gevallen,
de implementatie behandelt de bron en het doel van de weggelaten kopieer-/verplaatsingsbewerking als gewoon twee verschillende
manieren om naar hetzelfde object te verwijzen, en de vernietiging van dat object vindt plaats op een later tijdstip
wanneer de twee objecten zouden zijn vernietigd zonder de optimalisatie.123Deze elisie van kopiëren/verplaatsen
bewerkingen, genaamd copy elision, is toegestaan ​​in de volgende omstandigheden (die kunnen worden gecombineerd tot:
elimineer meerdere kopieën):

— in een return-instructie in een functie met een class-retourtype, wanneer de expressie de naam is van a
niet-vluchtig automatisch object (anders dan een functie- of catch-clausuleparameter) met dezelfde cvunqualified
type als het retourtype van de functie, kan de bewerking kopiëren/verplaatsen worden weggelaten door te construeren
het automatische object direct in de retourwaarde van de functie

— in een throw-expressie, wanneer de operand de naam is van een niet-vluchtig automatisch object (anders dan een
functie of catch-clause parameter) waarvan de reikwijdte niet verder reikt dan het einde van de binnenste
try-block insluiten (als die er is), de kopieer-/verplaatsingsbewerking van de operand naar de uitzondering
object (15.1) kan worden weggelaten door het automatische object rechtstreeks in het uitzonderingsobject te construeren

— wanneer een tijdelijk klasseobject dat niet aan een referentie (12.2) is gebonden, zou worden gekopieerd/verplaatst
naar een klasseobject met hetzelfde cv-unqualified type, kan de bewerking kopiëren/verplaatsen worden weggelaten door:
het tijdelijke object rechtstreeks construeren in het doel van de weggelaten kopiëren/verplaatsen

— wanneer de uitzonderingsverklaring van een uitzonderingsbehandelaar (clausule 15) een object van hetzelfde type declareert
(behalve voor cv-kwalificatie) als uitzonderingsobject (15.1), kan de bewerking kopiëren/verplaatsen worden weggelaten
door de uitzonderingsverklaring te behandelen als een alias voor het uitzonderingsobject als de betekenis van het programma
zal ongewijzigd blijven, behalve voor de uitvoering van constructors en destructors voor het object gedeclareerd door
de uitzonderingsverklaring.

123) Omdat er maar één object wordt vernietigd in plaats van twee, en één copy/move-constructor niet wordt uitgevoerd, is er nog steeds één
object vernietigd voor elke gebouwde.

Het gegeven voorbeeld is:

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

en uitgelegd:

Hier kunnen de criteria voor elisie worden gecombineerd om twee aanroepen naar de kopieerconstructor van klasse Thingte elimineren:
het kopiëren van het lokale automatische object tnaar het tijdelijke object voor de retourwaarde van functie f()
en het kopiëren van dat tijdelijke object naar object t2. In feite is de constructie van het lokale object t
kan worden gezien als het direct initialiseren van het globale object t2, en de vernietiging van dat object zal plaatsvinden op het programma
Uitgang. Het toevoegen van een verplaatsingsconstructor aan Thing heeft hetzelfde effect, maar het is de verplaatsingsconstructie van de
tijdelijk object naar t2dat wordt weggelaten.


Antwoord 3, autoriteit 35%

Veelvoorkomende vormen van kopieerelisie

Voor een technisch overzicht – ga naar dit antwoord.

Voor een minder technisch beeld & introductie – ga door naar dit antwoord.

(Benoemd) Optimalisatie van de retourwaarde is een veel voorkomende vorm van kopieerelisie. Het verwijst naar de situatie waarin een object dat wordt geretourneerd door waarde van een methode, waarvan de kopie wordt weggelaten. Het voorbeeld dat in de norm wordt uiteengezet, illustreert optimalisatie van de retourwaarde met een naam, aangezien het object een naam heeft.

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

Regelmatige optimalisatie van de retourwaardevindt plaats wanneer een tijdelijke wordt geretourneerd:

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

Andere veelvoorkomende plaatsen waar kopieerelisie plaatsvindt, is wanneer een tijdelijke waarde wordt doorgegeven:

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

of wanneer een uitzondering wordt gegooid en gevangen door waarde:

struct Thing{
  Thing();
  Thing(const Thing&);
};
void foo() {
  Thing c;
  throw c;
}
int main() {
  try {
    foo();
  }
  catch(Thing c) {  
  }             
}

Veelvoorkomende beperkingen van kopieerelisie zijn :

  • meerdere retourpunten
  • voorwaardelijke initialisatie

De meeste commerciële compilers ondersteunen copy elision & (N)RVO (afhankelijk van optimalisatie-instellingen).


Antwoord 4, autoriteit 22%

Copy elision is een compiler-optimalisatietechniek die onnodig kopiëren/verplaatsen van objecten elimineert.

In de volgende omstandigheden is het een compiler toegestaan ​​om kopieer-/verplaatsingsbewerkingen weg te laten en daarom de bijbehorende constructor niet aan te roepen:

  1. NRVO (Named Return Value Optimization): als een functie een klassetype op waarde retourneert en de expressie van de return-instructie de naam is van een niet-vluchtig object met automatische opslagduur (wat niet een functieparameter), dan kan het kopiëren/verplaatsen dat zou worden uitgevoerd door een niet-optimaliserende compiler worden weggelaten. Als dat het geval is, wordt de geretourneerde waarde rechtstreeks in de opslag geconstrueerd waarnaar de geretourneerde waarde van de functie anders zou worden verplaatst of gekopieerd.
  2. RVO (Return Value Optimization): als de functie een naamloos tijdelijk object retourneert dat door een naïeve compiler naar de bestemming zou worden verplaatst of gekopieerd, kan het kopiëren of verplaatsen worden weggelaten volgens 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());    //RVO, not 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  

Zelfs wanneer kopieerelisie plaatsvindt en de copy-/move-constructor niet wordt aangeroepen, moet deze aanwezig en toegankelijk zijn (alsof er helemaal geen optimalisatie heeft plaatsgevonden), anders is het programma slecht gevormd.

U dient dergelijke kopieerelisie alleen toe te staan ​​op plaatsen waar dit het waarneembare gedrag van uw software niet beïnvloedt. Kopieerelisie is de enige vorm van optimalisatie die waarneembare neveneffecten heeft (d.w.z. elimineren). Voorbeeld:

#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 biedt de optie -fno-elide-constructorsom kopieerelisie uit te schakelen.
Gebruik -fno-elide-constructorsals je mogelijke kopieerelisie wilt vermijden.

Nu bieden bijna alle compilers kopieerelisie wanneer optimalisatie is ingeschakeld (en als er geen andere optie is ingesteld om het uit te schakelen).

Conclusie

Bij elke kopie-elisie wordt één constructie en één overeenkomende vernietiging van de kopie weggelaten, waardoor CPU-tijd wordt bespaard, en er wordt geen object gemaakt, waardoor ruimte op het stapelframe wordt bespaard.


Antwoord 5

Hier geef ik nog een voorbeeld van kopieerelisie die ik vandaag blijkbaar ben tegengekomen.

# include <iostream>
class Obj {
public:
  int var1;
  Obj(){
    std::cout<<"In   Obj()"<<"\n";
    var1 =2;
  };
  Obj(const Obj & org){
    std::cout<<"In   Obj(const Obj & org)"<<"\n";
    var1=org.var1+1;
  };
};
int  main(){
  {
    /*const*/ Obj Obj_instance1;  //const doesn't change anything
    Obj Obj_instance2;
    std::cout<<"assignment:"<<"\n";
    Obj_instance2=Obj(Obj(Obj(Obj(Obj_instance1))))   ;
    // in fact expected: 6, but got 3, because of 'copy elision'
    std::cout<<"Obj_instance2.var1:"<<Obj_instance2.var1<<"\n";
  }
}

Met als resultaat:

In   Obj()
In   Obj()
assignment:
In   Obj(const Obj & org)
Obj_instance2.var1:3

Other episodes