Was sind Kopierelision und Rückgabewertoptimierung?

377

Was ist Kopierentscheidung? Was ist (benannte) Rückgabewertoptimierung? Was implizieren sie?

In welchen Situationen können sie auftreten? Was sind Einschränkungen?

Luchian Grigore
quelle
1
Copy Elision ist eine Möglichkeit, es zu betrachten. Objektelision oder Objektfusion (oder Verwirrung) ist eine andere Ansicht.
Neugieriger
Ich fand diesen Link hilfreich.
Subtleseeker

Antworten:

246

Einführung

Für einen technischen Überblick - springen Sie zu dieser Antwort .

In häufigen Fällen, in denen eine Kopierentfernung auftritt, fahren Sie mit dieser Antwort fort .

Copy Elision ist eine Optimierung, die von den meisten Compilern implementiert wird, um in bestimmten Situationen zusätzliche (möglicherweise teure) Kopien zu vermeiden. Dies macht eine Rückgabe nach Wert oder Wertübergabe in der Praxis möglich (Einschränkungen gelten).

Dies ist die einzige Form der Optimierung, bei der (ha!) Die Als-ob-Regel außer Kraft gesetzt wird. Die Kopierelision kann angewendet werden, auch wenn das Kopieren / Verschieben des Objekts Nebenwirkungen hat .

Das folgende Beispiel stammt aus 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();
}

Abhängig vom Compiler und den Einstellungen sind alle folgenden Ausgaben gültig :

Hallo Welt!
Eine Kopie wurde gemacht.
Eine Kopie wurde gemacht.


Hallo Welt!
Eine Kopie wurde gemacht.


Hallo Welt!

Dies bedeutet auch, dass weniger Objekte erstellt werden können, sodass Sie sich auch nicht darauf verlassen können, dass eine bestimmte Anzahl von Destruktoren aufgerufen wird. Sie sollten keine kritische Logik in Kopier- / Verschiebungskonstruktoren oder Destruktoren haben, da Sie sich nicht darauf verlassen können, dass sie aufgerufen werden.

Wenn ein Aufruf eines Kopier- oder Verschiebungskonstruktors aufgehoben wird, muss dieser Konstruktor noch vorhanden und zugänglich sein. Dies stellt sicher, dass die Kopierelision das Kopieren von Objekten nicht zulässt, die normalerweise nicht kopierbar sind, z. B. weil sie einen privaten oder gelöschten Kopier- / Verschiebungskonstruktor haben.

C ++ 17 : Ab C ++ 17 ist Copy Elision garantiert, wenn ein Objekt direkt zurückgegeben wird:

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
}
Luchian Grigore
quelle
2
Könnten Sie bitte erklären, wann die 2. Ausgabe erfolgt und wann die 3.?
Zhangxaochen
3
@zhangxaochen wann und wie der Compiler beschließt, auf diese Weise zu optimieren.
Luchian Grigore
10
@zhangxaochen, 1. Ausgabe: Kopie 1 ist von der Rückkehr zu einem temporären und Kopie 2 von temporär zu obj; 2. Wenn eine der oben genannten Optionen optimiert ist, wird wahrscheinlich die erneute Kopie entfernt. Die Thris sind beide elidiert
Sieger
2
Hmm, aber meiner Meinung nach MUSS dies eine Funktion sein, auf die wir uns verlassen können. Denn wenn wir nicht können, würde dies die Art und Weise, wie wir unsere Funktionen in modernem C ++ implementieren (RVO vs std :: move), stark beeinträchtigen. Beim Anschauen einiger CppCon 2014-Videos hatte ich wirklich den Eindruck, dass alle modernen Compiler immer RVO machen. Außerdem habe ich irgendwo gelesen, dass die Compiler es auch ohne Optimierungen anwenden. Aber da bin ich mir natürlich nicht sicher. Deshalb frage ich.
J00hi
8
@ j00hi: Schreibe niemals move in eine return-Anweisung - wenn rvo nicht angewendet wird, wird der Rückgabewert sowieso standardmäßig verschoben.
MikeMB
96

Standardreferenz

Für eine weniger technische Ansicht und Einführung - fahren Sie mit dieser Antwort fort .

In häufigen Fällen, in denen eine Kopierentfernung auftritt, fahren Sie mit dieser Antwort fort .

Die Kopierentscheidung ist im Standard definiert in:

12.8 Kopieren und Verschieben von Klassenobjekten [class.copy]

wie

31) Wenn bestimmte Kriterien erfüllt sind, kann eine Implementierung die Kopier- / Verschiebungskonstruktion eines Klassenobjekts weglassen, selbst wenn der Kopier- / Verschiebungskonstruktor und / oder der Destruktor für das Objekt Nebenwirkungen haben. In solchen Fällen behandelt die Implementierung die Quelle und das Ziel der ausgelassenen Kopier- / Verschiebungsoperation einfach als zwei verschiedene Arten, auf dasselbe Objekt zu verweisen, und die Zerstörung dieses Objekts erfolgt zu einem späteren Zeitpunkt, zu dem die beiden Objekte gewesen wären ohne die Optimierung zerstört. 123 Diese Elision von Kopier- / Verschiebevorgängen, die als Kopierelision bezeichnet wird , ist unter folgenden Umständen zulässig (die kombiniert werden können, um mehrere Kopien zu eliminieren):

- in einer return-Anweisung in einer Funktion mit einem Klassenrückgabetyp, wenn der Ausdruck der Name eines nichtflüchtigen automatischen Objekts (außer einer Funktion oder eines catch-Klausel-Parameters) mit demselben cvunqualified-Typ wie der Funktionsrückgabetyp ist, der Der Kopier- / Verschiebevorgang kann weggelassen werden, indem das automatische Objekt direkt in den Rückgabewert der Funktion erstellt wird

- in einem Wurfausdruck, wenn der Operand der Name eines nichtflüchtigen automatischen Objekts (außer einer Funktion oder eines catch-Klausel-Parameters) ist, dessen Gültigkeitsbereich nicht über das Ende des innersten umschließenden try-Blocks hinausgeht (falls vorhanden) Erstens kann die Kopier- / Verschiebungsoperation vom Operanden zum Ausnahmeobjekt (15.1) weggelassen werden, indem das automatische Objekt direkt in das Ausnahmeobjekt eingebaut wird

- Wenn ein temporäres Klassenobjekt, das nicht an eine Referenz (12.2) gebunden war, in ein Klassenobjekt mit demselben cv-unqualifizierten Typ kopiert / verschoben wird, kann die Kopier- / Verschiebungsoperation weggelassen werden, indem das temporäre Objekt direkt in das erstellt wird Ziel der ausgelassenen Kopie / Verschiebung

- Wenn die Ausnahmeerklärung eines Ausnahmebehandlers (Abschnitt 15) ein Objekt desselben Typs (mit Ausnahme der Lebenslaufqualifizierung) wie das Ausnahmeobjekt (15.1) deklariert, kann die Kopier- / Verschiebungsoperation durch Behandeln der Ausnahmeerklärung weggelassen werden als Alias ​​für das Ausnahmeobjekt, wenn die Bedeutung des Programms bis auf die Ausführung von Konstruktoren und Destruktoren für das in der Ausnahmeerklärung deklarierte Objekt unverändert bleibt.

123) Da nur ein Objekt anstelle von zwei zerstört wird und kein Kopier- / Verschiebungskonstruktor ausgeführt wird, wird für jedes erstellte Objekt immer noch ein Objekt zerstört.

Das gegebene Beispiel ist:

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

und erklärt:

Hier können die Kriterien für die Elision kombiniert werden, um zwei Aufrufe des Kopierkonstruktors der Klasse zu eliminieren Thing: das Kopieren des lokalen automatischen Objekts tin das temporäre Objekt für den Rückgabewert der Funktion f() und das Kopieren dieses temporären Objekts in das Objekt t2. Tatsächlich kann die Konstruktion des lokalen Objekts t als direkte Initialisierung des globalen Objekts angesehen werden t2, und die Zerstörung dieses Objekts erfolgt beim Beenden des Programms. Das Hinzufügen eines Verschiebungskonstruktors zu Thing hat den gleichen Effekt, es wird jedoch die Verschiebungskonstruktion vom temporären Objekt zum Objekt t2entfernt.

Luchian Grigore
quelle
1
Ist das aus dem C ++ 17 Standard oder aus einer früheren Version?
Nils
90

Gängige Formen der Kopierentfernung

Für einen technischen Überblick - springen Sie zu dieser Antwort .

Für eine weniger technische Ansicht und Einführung - fahren Sie mit dieser Antwort fort .

(Benannt) Die Rückgabewertoptimierung ist eine übliche Form der Kopierelision. Es bezieht sich auf die Situation, in der für ein Objekt, dessen Wert von einer Methode zurückgegeben wird, die Kopie entfernt wird. Das im Standard angegebene Beispiel veranschaulicht die Optimierung des benannten Rückgabewerts , da das Objekt benannt ist.

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

Regelmäßige Rückgabewertoptimierung tritt auf, wenn ein temporäres zurückgegeben wird:

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

Andere häufige Orte, an denen eine Kopierentfernung stattfindet, sind, wenn ein temporärer Wert übergeben wird :

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

foo(Thing());

oder wenn eine Ausnahme ausgelöst und vom Wert abgefangen wird :

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

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

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

Häufige Einschränkungen bei der Kopierentfernung sind:

  • mehrere Rückgabepunkte
  • bedingte Initialisierung

Die meisten kommerziellen Compiler unterstützen Copy Elision und (N) RVO (abhängig von den Optimierungseinstellungen).

Luchian Grigore
quelle
4
Es würde mich interessieren, wenn die Aufzählungspunkte "Allgemeine Einschränkungen" nur ein wenig erklärt werden ... was macht diese einschränkenden Faktoren aus?
Phonetagger
@phonetagger Ich habe gegen den MSDN-Artikel verlinkt, hoffe, das räumt ein paar Sachen aus.
Luchian Grigore
54

Copy Elision ist eine Compiler-Optimierungstechnik, die unnötiges Kopieren / Verschieben von Objekten verhindert.

Unter den folgenden Umständen darf ein Compiler Kopier- / Verschiebevorgänge weglassen und daher den zugehörigen Konstruktor nicht aufrufen:

  1. NRVO (Named Return Value Optimization) : Wenn eine Funktion einen Klassentyp nach Wert zurückgibt und der Ausdruck der return-Anweisung der Name eines nichtflüchtigen Objekts mit automatischer Speicherdauer ist (was kein Funktionsparameter ist), wird kopiert / verschoben das von einem nicht optimierenden Compiler ausgeführt werden würde, kann weggelassen werden. In diesem Fall wird der zurückgegebene Wert direkt in dem Speicher erstellt, in den der Rückgabewert der Funktion andernfalls verschoben oder kopiert würde.
  2. RVO (Return Value Optimization) : Wenn die Funktion ein namenloses temporäres Objekt zurückgibt, das von einem naiven Compiler in das Ziel verschoben oder kopiert wird, kann das Kopieren oder Verschieben gemäß 1 weggelassen werden.
#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  

Selbst wenn eine Kopierelision stattfindet und der Kopier- / Verschiebungskonstruktor nicht aufgerufen wird, muss er vorhanden und zugänglich sein (als ob überhaupt keine Optimierung stattgefunden hätte), da sonst das Programm fehlerhaft ist.

Sie sollten eine solche Kopierentfernung nur an Stellen zulassen, an denen das beobachtbare Verhalten Ihrer Software nicht beeinträchtigt wird. Die Kopierelision ist die einzige Form der Optimierung, bei der (dh Elide) beobachtbare Nebenwirkungen auftreten dürfen. Beispiel:

#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 bietet die -fno-elide-constructorsOption, die Kopierelision zu deaktivieren. Wenn Sie eine mögliche Kopierentfernung vermeiden möchten, verwenden Sie -fno-elide-constructors.

Jetzt bieten fast alle Compiler eine Kopierelision an, wenn die Optimierung aktiviert ist (und wenn keine andere Option zum Deaktivieren aktiviert ist).

Fazit

Bei jeder Kopierentfernung werden eine Konstruktion und eine übereinstimmende Zerstörung der Kopie weggelassen, wodurch CPU-Zeit gespart wird und kein Objekt erstellt wird, wodurch Platz auf dem Stapelrahmen gespart wird.

Ajay yadav
quelle
6
die Aussage ABC obj2(xyz123());ist es NRVO oder RVO? wird es nicht temporäre Variable / Objekt wie ABC xyz = "Stack Overflow";//RVO
Asif Mushtaq
3
Um eine konkretere Darstellung von RVO zu erhalten, können Sie auf die vom Compiler generierte Assembly verweisen (ändern Sie das Compiler-Flag -fno-elide-Konstruktoren, um den Unterschied anzuzeigen). godbolt.org/g/Y2KcdH
Gab