Co to jest eliminacja kopii i optymalizacja wartości zwrotu?

377

Co to jest kopia prawna? Czym jest (nazwana) optymalizacja wartości zwracanej? Co oni implikują?

W jakich sytuacjach mogą wystąpić? Jakie są ograniczenia?

Luchian Grigore
źródło
1
Kopiowanie elekcji jest jednym ze sposobów, aby na to spojrzeć; elucja obiektu lub fuzja (lub zamieszanie) to inny pogląd.
ciekawy,
Ten link był dla mnie pomocny.
subtleseeker

Odpowiedzi:

246

Wprowadzenie

Przegląd techniczny - przejdź do tej odpowiedzi .

W typowych przypadkach, w których występuje usuwanie kopii, przejdź do tej odpowiedzi .

Eliminacja kopiowania to optymalizacja realizowana przez większość kompilatorów w celu zapobiegania dodatkowym (potencjalnie drogim) kopiom w niektórych sytuacjach. W praktyce umożliwia to zwrot według wartości lub według wartości (obowiązują ograniczenia).

Jest to jedyna forma optymalizacji, która wymusza (ha!) Zasadę „jak gdyby” - można zastosować eliminację kopiowania, nawet jeśli kopiowanie / przenoszenie obiektu ma skutki uboczne .

Poniższy przykład pochodzi z Wikipedii :

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

W zależności od kompilatora i ustawień wszystkie poniższe dane wyjściowe są prawidłowe :

Witaj świecie!
Kopia została wykonana.
Kopia została wykonana.


Witaj świecie!
Kopia została wykonana.


Witaj świecie!

Oznacza to również, że można utworzyć mniej obiektów, więc nie można również polegać na wywołaniu określonej liczby niszczycieli. Nie powinieneś mieć krytycznej logiki wewnątrz konstruktorów kopiowania / przenoszenia lub destruktorów, ponieważ nie możesz polegać na ich wywoływaniu.

Jeśli zostanie wywołane wywołanie konstruktora kopiowania lub przenoszenia, konstruktor ten musi nadal istnieć i musi być dostępny. Zapewnia to, że usuwanie kopii nie pozwala na kopiowanie obiektów, których normalnie nie można skopiować, np. Ponieważ mają one prywatny lub usunięty konstruktor kopiowania / przenoszenia.

C ++ 17 : Począwszy od C ++ 17, Copy Elision jest gwarantowane, gdy obiekt jest zwracany bezpośrednio:

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
źródło
2
czy mógłbyś wyjaśnić, kiedy nastąpi 2. wyjście, a kiedy 3.?
zhangxaochen
3
@zhangxaochen kiedy i jak kompilator zdecyduje się zoptymalizować w ten sposób.
Luchian Grigore,
10
@zhangxaochen, 1. wyjście: kopia 1 pochodzi z powrotu do temp, a kopia 2 z temp do obj; Po drugie, gdy jeden z powyższych jest zoptymalizowany, prawdopodobnie kopia reutnr jest pomijana; obaj są uciec
zwycięzca
2
Hmm, ale moim zdaniem, MUSI to być funkcja, na której możemy polegać. Ponieważ jeśli nie możemy, poważnie wpłynęłoby to na sposób implementacji naszych funkcji we współczesnym języku C ++ (RVO vs std :: move). Podczas oglądania niektórych filmów z CppCon 2014 naprawdę odniosłem wrażenie, że wszystkie nowoczesne kompilatory zawsze wykonują RVO. Co więcej, przeczytałem gdzieś, że także bez żadnych optymalizacji kompilatory go stosują. Ale oczywiście nie jestem tego pewien. Właśnie dlatego pytam.
j00hi
8
@ j00hi: Nigdy nie pisz move w instrukcji return - jeśli rvo nie jest zastosowane, wartość zwracana i tak jest domyślnie przenoszona.
MikeMB
96

Standardowe odniesienie

Aby uzyskać mniej techniczny widok i wprowadzenie - przejdź do tej odpowiedzi .

W typowych przypadkach, w których występuje usuwanie kopii, przejdź do tej odpowiedzi .

Skasowanie kopii jest zdefiniowane w standardzie w:

12.8 Kopiowanie i przenoszenie obiektów klasy [class.copy]

tak jak

31) Po spełnieniu określonych kryteriów implementacja może pominąć konstrukcję kopiowania / przenoszenia obiektu klasy, nawet jeśli konstruktor kopiowania / przenoszenia i / lub destruktor obiektu mają skutki uboczne. W takich przypadkach implementacja traktuje źródło i cel pominiętej operacji kopiowania / przenoszenia jako po prostu dwa różne sposoby odwoływania się do tego samego obiektu, a zniszczenie tego obiektu następuje w późniejszym czasie, gdy dwa obiekty byłyby zniszczone bez optymalizacji. 123 Ta eliminacja operacji kopiowania / przenoszenia, zwana eliminacją kopii , jest dozwolona w następujących okolicznościach (które można łączyć w celu wyeliminowania wielu kopii):

- w instrukcji return w funkcji z typem zwracanym przez klasę, gdy wyrażenie jest nazwą nielotnego obiektu automatycznego (innego niż funkcja lub parametr klauzuli catch) o takim samym potwierdzonym typie, jak typ zwracanej funkcji, operację kopiowania / przenoszenia można pominąć, konstruując obiekt automatyczny bezpośrednio w wartości zwracanej przez funkcję

- w wyrażeniu typu „rzut”, gdy operand jest nazwą nieulotnego obiektu automatycznego (innego niż funkcja lub parametr klauzuli catch), którego zakres nie wykracza poza koniec najbardziej wewnętrznego otaczającego try-block (jeśli istnieje jeden) operację kopiowania / przenoszenia z operandu do obiektu wyjątku (15.1) można pominąć, konstruując obiekt automatyczny bezpośrednio w obiekcie wyjątku

- gdy tymczasowy obiekt klasy, który nie został powiązany z odniesieniem (12.2), zostałby skopiowany / przeniesiony do obiektu klasy o tym samym typie bez kwalifikacji cv, operację kopiowania / przenoszenia można pominąć, konstruując obiekt tymczasowy bezpośrednio w cel pominiętej kopii / przeniesienia

- gdy deklaracja wyjątku procedury obsługi wyjątku (klauzula 15) deklaruje obiekt tego samego typu (z wyjątkiem kwalifikacji cv) jako obiekt wyjątku (15.1), operację kopiowania / przenoszenia można pominąć, traktując deklarację wyjątku jako alias dla obiektu wyjątku, jeśli znaczenie programu pozostanie niezmienione, z wyjątkiem wykonania konstruktorów i destruktorów dla obiektu zadeklarowanego w deklaracji wyjątku.

123) Ponieważ zniszczony jest tylko jeden obiekt zamiast dwóch, a jeden konstruktor kopiuj / przenieś nie jest wykonywany, dla każdego zbudowanego nadal jest jeden obiekt zniszczony.

Podany przykład to:

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

i wyjaśnił:

Tutaj kryteria wyboru można połączyć, aby wyeliminować dwa wywołania do konstruktora kopiowania klasy Thing: kopiowanie lokalnego obiektu automatycznego tdo obiektu tymczasowego w celu zwrócenia wartości funkcji f() i kopiowanie tego obiektu tymczasowego do obiektu t2. W efekcie konstrukcję obiektu lokalnego t można postrzegać jako bezpośrednią inicjalizację obiektu globalnego t2, a zniszczenie tego obiektu nastąpi przy wyjściu z programu. Dodanie konstruktora ruchu do Rzeczy ma ten sam efekt, ale jest to konstrukcja ruchu z obiektu tymczasowego do t2tego, która jest pomijana.

Luchian Grigore
źródło
1
Czy to ze standardu C ++ 17 czy z wcześniejszej wersji?
Nils
90

Typowe formy wyboru kopii

Przegląd techniczny - przejdź do tej odpowiedzi .

Aby uzyskać mniej techniczny widok i wprowadzenie - przejdź do tej odpowiedzi .

(Nazwany) Optymalizacja wartości zwracanej jest powszechną formą usuwania kopii. Odnosi się do sytuacji, w której obiekt zwrócony przez wartość z metody ma swoją kopię. Przykład podany w standardzie ilustruje optymalizację nazwanych wartości zwrotnych , ponieważ obiekt jest nazwany.

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

Regularna optymalizacja wartości zwracanej następuje po zwróceniu wartości tymczasowej:

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

Inne typowe miejsca, w których odbywa się usuwanie kopii, to moment, w którym wartość tymczasowa jest przekazywana :

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

foo(Thing());

lub gdy wyjątek jest zgłaszany i wychwytywany według wartości :

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

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

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

Typowe ograniczenia wyboru kopii to:

  • wiele punktów powrotu
  • warunkowa inicjalizacja

Większość komercyjnych kompilatorów obsługuje usuwanie kopii i (N) RVO (w zależności od ustawień optymalizacji).

Luchian Grigore
źródło
4
Byłbym zainteresowany, aby wyjaśnić nieco punktor „Wspólne ograniczenia”… co powoduje te czynniki ograniczające?
fonetagger
@ phonetagger Powiązałem z artykułem msdn, mam nadzieję, że to wyjaśni niektóre rzeczy.
Luchian Grigore
54

Copy elision to technika optymalizacji kompilatora, która eliminuje niepotrzebne kopiowanie / przenoszenie obiektów.

W następujących okolicznościach kompilator może pominąć operacje kopiowania / przenoszenia, a tym samym nie wywoływać powiązanego konstruktora:

  1. NRVO (Optymalizacja nazw zwracanych wartości) : Jeśli funkcja zwraca typ klasy według wartości, a wyrażenie instrukcji return jest nazwą nieulotnego obiektu z automatycznym czasem przechowywania (który nie jest parametrem funkcji), wówczas kopiuj / przenieś które zostałyby wykonane przez nieoptymalizujący kompilator, można pominąć. Jeśli tak, zwracana wartość jest konstruowana bezpośrednio w pamięci, do której w przeciwnym razie wartość zwracana przez funkcję zostałaby przeniesiona lub skopiowana.
  2. RVO (Return Value Optimization) : Jeśli funkcja zwraca bezimienny obiekt tymczasowy, który zostałby przeniesiony lub skopiowany do miejsca docelowego przez naiwny kompilator, kopiowanie lub przenoszenie można pominąć zgodnie z 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  

Nawet jeśli ma miejsce usuwanie kopii i nie wywoływany jest konstruktor kopiowania / przenoszenia, musi on być obecny i dostępny (tak jakby w ogóle nie nastąpiła optymalizacja), w przeciwnym razie program będzie źle sformułowany.

Powinieneś zezwolić na taką kopię kopii tylko w miejscach, w których nie wpłynie to na obserwowalne zachowanie twojego oprogramowania. Kopiowanie danych jest jedyną formą optymalizacji, która może mieć (tzn. Uniknąć) obserwowalne skutki uboczne. Przykład:

#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 zapewnia -fno-elide-constructorsopcję wyłączenia kopiowania. Jeśli chcesz uniknąć możliwego usunięcia kopii, użyj -fno-elide-constructors.

Teraz prawie wszystkie kompilatory zapewniają usuwanie kopii, gdy optymalizacja jest włączona (i jeśli nie ustawiono żadnej innej opcji, aby ją wyłączyć).

Wniosek

Przy każdym usuwaniu kopii pomijana jest jedna konstrukcja i jedno pasujące zniszczenie kopii, co oszczędza czas procesora, a jeden obiekt nie jest tworzony, co oszczędza miejsce na ramce stosu.

Ajay Yadav
źródło
6
oświadczenie ABC obj2(xyz123());to NRVO czy RVO? czy nie otrzymuje tymczasowej zmiennej / obiektu tak jak ABC xyz = "Stack Overflow";//RVO
Asif Mushtaq
3
Aby uzyskać bardziej konkretną ilustrację RVO, możesz odnieść się do zestawu generowanego przez kompilator (zmień konstruktory flag kompilatora -fno-elide-konstruktorów, aby zobaczyć różnicę). godbolt.org/g/Y2KcdH
Gab 是