Cosa sono l'ottimizzazione della copia e l'ottimizzazione del valore di ritorno?


377

Cos'è la copia elisione? Che cos'è l'ottimizzazione del valore di ritorno (denominato)? Cosa implicano?

In quali situazioni possono verificarsi? Quali sono le limitazioni?


1
Copia elision è un modo per vederlo; elisione oggetto o fusione oggetto (o confusione) è un'altra visione.
curioso

Ho trovato utile questo link .
subtleseeker

Risposte:


246

introduzione

Per una panoramica tecnica, passa a questa risposta .

Per i casi più comuni in cui si verifica l'eliminazione della copia, passare a questa risposta .

Copy elision è un'ottimizzazione implementata dalla maggior parte dei compilatori per impedire copie extra (potenzialmente costose) in determinate situazioni. Rende possibile in pratica la restituzione per valore o pass-by-value (si applicano restrizioni).

È l'unica forma di ottimizzazione che elide (ha!) La regola as-if - la copia dell'elisione può essere applicata anche se la copia / spostamento dell'oggetto ha effetti collaterali .

Il seguente esempio tratto da 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();
}

A seconda del compilatore e delle impostazioni, sono validi tutti i seguenti output :

Ciao mondo!
È stata fatta una copia.
È stata fatta una copia.


Ciao mondo!
È stata fatta una copia.


Ciao mondo!

Ciò significa anche che è possibile creare un numero inferiore di oggetti, quindi non è possibile fare affidamento su un numero specifico di distruttori chiamati. Non dovresti avere una logica critica all'interno di costruttori / distruttori di copia / spostamento, poiché non puoi fare affidamento sul fatto che vengano chiamati.

Se viene elusa una chiamata a un costruttore di copia o spostamento, tale costruttore deve ancora esistere e deve essere accessibile. Ciò garantisce che l'eliminazione della copia non consenta di copiare oggetti che non sono normalmente copiabili, ad esempio perché hanno un costruttore di copia / spostamento privato o cancellato.

C ++ 17 : A partire da C ++ 17, Copy Elision è garantito quando un oggetto viene restituito direttamente:

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
potresti spiegare quando si verifica la seconda uscita e quando la terza?
Zhangxaochen,

3
@zhangxaochen quando e come il compilatore decide di ottimizzare in quel modo.
Luchian Grigore,

10
@zhangxaochen, 1a uscita: la copia 1 viene dal ritorno a una temperatura e la copia 2 da temp a obj; Il secondo è quando uno dei precedenti è optimezed, probabilmente la copia di reutnr viene elusa; i due sono entrambi eletti
vincitore il

2
Hmm, ma secondo me, questa DEVE essere una caratteristica su cui possiamo contare. Perché se non ci riusciamo, ciò influenzerebbe gravemente il modo in cui implementiamo le nostre funzioni nel moderno C ++ (RVO vs std :: move). Durante la visione di alcuni dei video di CppCon 2014, ho avuto davvero l'impressione che tutti i compilatori moderni facciano sempre RVO. Inoltre, ho letto da qualche parte che anche senza alcuna ottimizzazione, i compilatori lo applicano. Ma, ovviamente, non ne sono sicuro. Ecco perché lo sto chiedendo.
j00hi,

8
@ j00hi: non scrivere mai move in un'istruzione return - se non viene applicato rvo, il valore restituito viene comunque spostato di default.
MikeMB

96

Riferimento standard

Per una visione e un'introduzione meno tecniche, passa a questa risposta .

Per i casi più comuni in cui si verifica l'eliminazione della copia, passare a questa risposta .

Copia elisione è definita nello standard in:

12.8 Copia e spostamento di oggetti di classe [class.copy]

come

31) Quando vengono soddisfatti determinati criteri, un'implementazione può omettere la costruzione di copia / spostamento di un oggetto di classe, anche se il costruttore di copia / spostamento e / o distruttore per l'oggetto ha effetti collaterali. In tali casi, l'implementazione considera l'origine e la destinazione dell'operazione di copia / spostamento omessa semplicemente come due modi diversi di riferirsi allo stesso oggetto, e la distruzione di quell'oggetto avviene nel momento successivo in cui i due oggetti sarebbero stati distrutto senza l'ottimizzazione. 123 Questa elisione delle operazioni di copia / spostamento, chiamata elisione della copia , è consentita nelle seguenti circostanze (che possono essere combinate per eliminare più copie):

- in un'istruzione return in una funzione con un tipo restituito di classe, quando l'espressione è il nome di un oggetto automatico non volatile (diverso da una funzione o parametro catch-clause) con lo stesso tipo cvunqualified del tipo restituito funzione, il l'operazione di copia / spostamento può essere omessa costruendo l'oggetto automatico direttamente nel valore di ritorno della funzione

- in un'espressione di lancio, quando l'operando è il nome di un oggetto automatico non volatile (diverso da una funzione o parametro catch-clause) il cui ambito non si estende oltre la fine del blocco di prova racchiuso più interno (se presente uno), l'operazione di copia / spostamento dall'operando all'oggetto eccezione (15.1) può essere omessa costruendo l'oggetto automatico direttamente nell'oggetto eccezione

- quando un oggetto di classe temporaneo che non è stato associato a un riferimento (12.2) verrebbe copiato / spostato in un oggetto di classe con lo stesso tipo non qualificato cv, l'operazione di copia / spostamento può essere omessa costruendo l'oggetto temporaneo direttamente nel bersaglio della copia / mossa omessa

- quando la dichiarazione di eccezione di un gestore di eccezioni (clausola 15) dichiara un oggetto dello stesso tipo (ad eccezione della qualifica cv) dell'oggetto di eccezione (15.1), l'operazione di copia / spostamento può essere omessa trattando la dichiarazione di eccezione come alias per l'oggetto eccezione se il significato del programma rimarrà invariato tranne per l'esecuzione di costruttori e distruttori per l'oggetto dichiarato dalla dichiarazione di eccezione.

123) Poiché viene distrutto un solo oggetto anziché due e non viene eseguito un costruttore copia / sposta, esiste comunque un oggetto distrutto per ognuno costruito.

L'esempio fornito è:

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

e spiegato:

Qui i criteri per l'elissione possono essere combinati per eliminare due chiamate al costruttore della classe di Thingcopia: la copia dell'oggetto automatico locale nell'oggetto ttemporaneo per il valore di ritorno della funzione f() e la copia di quell'oggetto temporaneo nell'oggetto t2. In effetti, la costruzione dell'oggetto locale t può essere vista come inizializzazione diretta dell'oggetto globale t2e la distruzione di quell'oggetto avverrà all'uscita dal programma. L'aggiunta di un costruttore di mosse a Thing ha lo stesso effetto, ma è la costruzione di mosse dall'oggetto temporaneo a t2quella eliminata.


1
Proviene dallo standard C ++ 17 o da una versione precedente?
Nils,

90

Forme comuni di elisione delle copie

Per una panoramica tecnica, passa a questa risposta .

Per una visione e un'introduzione meno tecniche, passa a questa risposta .

(Denominato) L'ottimizzazione del valore restituito è una forma comune di elisione della copia. Si riferisce alla situazione in cui un oggetto restituito in base al valore da un metodo ha la sua copia eliminata. L'esempio riportato nella norma illustra l' ottimizzazione del valore restituito , poiché l'oggetto è denominato.

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

L' ottimizzazione del valore di ritorno regolare si verifica quando viene restituito un temporaneo:

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

Altri luoghi comuni in cui si verifica l'eliminazione della copia sono quando un valore temporaneo viene passato per valore :

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

foo(Thing());

o quando viene generata un'eccezione e rilevata dal valore :

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

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

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

Limitazioni comuni dell'eliminazione della copia sono:

  • più punti di ritorno
  • inizializzazione condizionale

La maggior parte dei compilatori di livello commerciale supportano copy elision & (N) RVO (a seconda delle impostazioni di ottimizzazione).


4
Sarei interessato a vedere i punti elenco "Limitazioni comuni" spiegati solo un po '... cosa rende questi fattori limitanti?
fonagger

@phonetagger Ho collegato l'articolo di msdn, spero che chiarisca alcune cose.
Luchian Grigore,

54

Copia elisione è una tecnica di ottimizzazione del compilatore che elimina la copia / lo spostamento non necessari di oggetti.

Nelle seguenti circostanze, un compilatore può omettere le operazioni di copia / spostamento e quindi non chiamare il costruttore associato:

  1. NRVO (Named Return Value Optimization) : se una funzione restituisce un tipo di classe in base al valore e l'espressione dell'istruzione return è il nome di un oggetto non volatile con durata di memorizzazione automatica (che non è un parametro di funzione), quindi copia / sposta che verrebbe eseguito da un compilatore non ottimizzante può essere omesso. In tal caso, il valore restituito viene creato direttamente nella memoria in cui il valore restituito della funzione verrebbe altrimenti spostato o copiato.
  2. RVO (Return Value Optimization) : se la funzione restituisce un oggetto temporaneo senza nome che verrebbe spostato o copiato nella destinazione da un compilatore ingenuo, la copia o lo spostamento possono essere omessi come da 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  

Anche quando si verifica la copia elisione e il costruttore di copia / sposta non viene chiamato, deve essere presente e accessibile (come se non si verificasse alcuna ottimizzazione), altrimenti il ​​programma è mal formato.

Dovresti consentire tale copia elettorale solo in luoghi in cui non influirà sul comportamento osservabile del tuo software. Copy elision è l'unica forma di ottimizzazione che può avere effetti collaterali (ad esempio elide) osservabili. Esempio:

#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 offre l' -fno-elide-constructorsopzione per disabilitare la copia elisione. Se si desidera evitare la possibile elisione della copia, utilizzare -fno-elide-constructors.

Ora quasi tutti i compilatori forniscono l'elisione della copia quando l'ottimizzazione è abilitata (e se non è impostata alcuna altra opzione per disabilitarla).

Conclusione

Ad ogni elisione della copia, una costruzione e una distruzione corrispondente della copia vengono omesse, risparmiando così tempo di CPU e non viene creato un oggetto, risparmiando così spazio sul frame dello stack.


6
la dichiarazione ABC obj2(xyz123());è NRVO o RVO? non sta ottenendo la variabile / oggetto temporaneo uguale a ABC xyz = "Stack Overflow";//RVO
Asif Mushtaq il

3
Per avere un'illustrazione più concreta di RVO, puoi fare riferimento all'assembly generato dal compilatore (cambia il flag del compilatore -fno-elide-constructors per vedere il diff). godbolt.org/g/Y2KcdH
Gab 是 好人
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.