Apa yang dimaksud dengan optimasi salin dan pengembalian nilai?

377

Apa itu copy elision? Apa yang dimaksud dengan (dinamai) optimasi nilai kembali? Apa yang mereka maksudkan?

Dalam situasi apa mereka bisa terjadi? Apa itu batasan?

Luchian Grigore
sumber
1
Copy elision adalah salah satu cara untuk melihatnya; objek elision atau fusion objek (atau kebingungan) adalah pandangan lain.
curiousguy
Saya menemukan tautan ini bermanfaat.
subtleseeker

Jawaban:

246

pengantar

Untuk tinjauan teknis - lewati ke jawaban ini .

Untuk kasus-kasus umum di mana copy elision terjadi - lewati ke jawaban ini .

Copy elision adalah optimisasi yang diterapkan oleh kebanyakan kompiler untuk mencegah salinan ekstra (berpotensi mahal) dalam situasi tertentu. Itu membuat pengembalian dengan nilai atau nilai demi nilai layak dalam praktiknya (pembatasan berlaku).

Ini satu-satunya bentuk optimasi yang menghilangkan (ha!) Aturan as-if - copy elision dapat diterapkan bahkan jika menyalin / memindahkan objek memiliki efek samping .

Contoh berikut diambil dari 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();
}

Bergantung pada kompiler & pengaturan, output berikut semuanya valid :

Halo Dunia!
Salinan telah dibuat.
Salinan telah dibuat.


Halo Dunia!
Salinan telah dibuat.


Halo Dunia!

Ini juga berarti lebih sedikit objek yang bisa dibuat, jadi Anda juga tidak bisa mengandalkan sejumlah destruktor yang dipanggil. Anda seharusnya tidak memiliki logika kritis di dalam copy / move-constructor atau destructor, karena Anda tidak dapat mengandalkan mereka yang dipanggil.

Jika panggilan untuk menyalin atau memindahkan konstruktor dihapus, konstruktor itu harus tetap ada dan harus dapat diakses. Ini memastikan bahwa copy elision tidak memungkinkan menyalin objek yang biasanya tidak dapat disalin, misalnya karena mereka memiliki konstruktor copy / move pribadi atau dihapus.

C ++ 17 : Pada C ++ 17, Copy Elision dijamin ketika suatu objek dikembalikan secara langsung:

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
sumber
2
bisakah Anda menjelaskan kapan output ke-2 terjadi dan kapan ke-3?
zhangxaochen
3
@ Zhangxaochen kapan dan bagaimana kompiler memutuskan untuk mengoptimalkan cara itu.
Luchian Grigore
10
@ Zhangxaochen, output pertama: copy 1 dari return to temp, dan copy 2 dari temp to obj; 2 adalah ketika salah satu dari di atas di optimezed, mungkin salinan reutnr dihilangkan; keduanya dihilangkan
pemenang
2
Hmm, tapi menurut saya, ini HARUS menjadi fitur yang bisa kita andalkan. Karena jika kita tidak bisa, itu akan sangat mempengaruhi cara kita mengimplementasikan fungsi kita di C ++ modern (RVO vs std :: move). Selama menonton beberapa video CppCon 2014, saya benar-benar mendapat kesan bahwa semua kompiler modern selalu melakukan RVO. Selanjutnya, saya sudah membaca di suatu tempat yang juga tanpa optimasi, kompiler menerapkannya. Tapi, tentu saja, saya tidak yakin tentang itu. Itu sebabnya saya bertanya.
j00hi
8
@ j00hi: Jangan pernah menulis pindahkan dalam pernyataan pengembalian - jika rvo tidak diterapkan, nilai pengembalian tetap dipindahkan secara default.
MikeMB
96

Referensi standar

Untuk tampilan & pengenalan yang kurang teknis - lewati ke jawaban ini .

Untuk kasus-kasus umum di mana copy elision terjadi - lewati ke jawaban ini .

Salinan salinan didefinisikan dalam standar di:

12.8 Menyalin dan memindahkan objek kelas [class.copy]

sebagai

31) Ketika kriteria tertentu terpenuhi, implementasi diperbolehkan untuk menghilangkan copy / memindahkan konstruksi objek kelas, bahkan jika copy / move constructor dan / atau destructor untuk objek memiliki efek samping. Dalam kasus seperti itu, implementasi memperlakukan sumber dan target operasi penyalinan / pemindahan yang dihilangkan hanya sebagai dua cara berbeda untuk merujuk ke objek yang sama, dan penghancuran objek tersebut terjadi pada saat-saat terakhir ketika kedua objek tersebut akan menjadi dimusnahkan tanpa optimasi. 123 Elisi operasi penyalinan / pemindahan ini, disebut copy elision , diizinkan dalam kondisi berikut (yang dapat digabungkan untuk menghilangkan banyak salinan):

- dalam pernyataan pengembalian dalam fungsi dengan tipe pengembalian kelas, ketika ekspresi adalah nama objek otomatis yang tidak mudah menguap (selain fungsi atau parameter klausa tangkap) dengan jenis kvunqualifikasi yang sama dengan jenis pengembalian fungsi, operasi salin / pindahkan dapat dihilangkan dengan membuat objek otomatis langsung ke nilai pengembalian fungsi

- dalam ekspresi lemparan, ketika operan adalah nama objek otomatis yang tidak mudah menguap (selain fungsi atau parameter klausa tangkap) yang ruang lingkupnya tidak melampaui ujung blok coba tertutup yang paling dalam (jika ada satu), operasi salin / pindahkan dari operan ke objek pengecualian (15.1) dapat dihilangkan dengan membuat objek otomatis langsung ke objek pengecualian

- ketika objek kelas sementara yang belum terikat ke referensi (12.2) akan disalin / dipindahkan ke objek kelas dengan tipe cv-wajar yang sama, operasi salin / pindahkan dapat dihilangkan dengan membangun objek sementara langsung ke dalam target dari salinan / pemindahan yang dihilangkan

- ketika pengecualian-deklarasi pengendali pengecualian (Klausul 15) menyatakan objek dengan tipe yang sama (kecuali untuk kualifikasi-cv) sebagai objek pengecualian (15.1), operasi penyalinan / pemindahan dapat dihilangkan dengan memperlakukan pengecualian-deklarasi sebagai alias untuk objek pengecualian jika makna program tidak akan berubah kecuali untuk pelaksanaan konstruktor dan destruktor untuk objek yang dinyatakan oleh pengecualian-deklarasi.

123) Karena hanya satu objek dihancurkan bukan dua, dan satu copy / move constructor tidak dieksekusi, masih ada satu objek dihancurkan untuk masing-masing dibangun.

Contoh yang diberikan adalah:

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

dan menjelaskan:

Di sini kriteria untuk elision dapat digabungkan untuk menghilangkan dua panggilan ke copy constructor kelas Thing: menyalin objek otomatis lokal tke objek sementara untuk mengembalikan nilai fungsi f() dan menyalin objek sementara ke objek t2. Secara efektif, pembangunan objek lokal t dapat dilihat sebagai inisialisasi langsung objek global t2, dan kehancuran objek akan terjadi pada saat program keluar. Menambahkan move constructor ke Thing memiliki efek yang sama, tetapi itu adalah konstruksi pemindahan dari objek sementara ke t2yang di-elided.

Luchian Grigore
sumber
1
Apakah itu dari standar C ++ 17 atau dari versi sebelumnya?
Nils
90

Bentuk umum salinan salinan

Untuk tinjauan teknis - lewati ke jawaban ini .

Untuk tampilan & pengenalan yang kurang teknis - lewati ke jawaban ini .

(Bernama) Optimasi nilai kembali adalah bentuk umum dari salin salinan. Ini merujuk pada situasi di mana objek yang dikembalikan oleh nilai dari suatu metode memiliki salinannya dihapuskan. Contoh yang ditetapkan dalam standar menggambarkan pengoptimalan nilai kembali , karena objek tersebut dinamai.

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

Optimalisasi nilai pengembalian reguler terjadi ketika sementara dikembalikan:

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

Tempat umum lainnya tempat salinan elision terjadi adalah ketika sebuah nilai sementara diberikan :

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

foo(Thing());

atau saat pengecualian dilemparkan dan ditangkap berdasarkan nilai :

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

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

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

Batasan umum dari copy elision adalah:

  • beberapa poin pengembalian
  • inisialisasi bersyarat

Sebagian besar kompiler tingkat komersial mendukung elisi salinan & (N) RVO (tergantung pada pengaturan optimasi).

Luchian Grigore
sumber
4
Saya akan tertarik melihat butir-butir "batasan umum" yang dijelaskan hanya sedikit ... apa yang membuat faktor-faktor pembatas ini?
phonetagger
@ phonetagger Saya ditautkan dengan artikel msdn, berharap bisa menghapus beberapa hal.
Luchian Grigore
54

Copy elision adalah teknik optimisasi kompiler yang menghilangkan penyalinan / pemindahan objek yang tidak perlu.

Dalam keadaan berikut, kompiler diperbolehkan untuk menghilangkan operasi salin / pindah dan karenanya tidak memanggil konstruktor terkait:

  1. NRVO (Named Return Value Optimization) : Jika suatu fungsi mengembalikan tipe kelas dengan nilai dan ekspresi pernyataan pengembalian adalah nama objek yang tidak mudah menguap dengan durasi penyimpanan otomatis (yang bukan merupakan parameter fungsi), maka salin / pindahkan yang akan dilakukan oleh kompiler yang tidak mengoptimalkan dapat dihilangkan. Jika demikian, nilai yang dikembalikan dibangun langsung di penyimpanan yang nilai balik fungsinya akan dipindahkan atau disalin.
  2. RVO (Return Value Optimization) : Jika fungsi mengembalikan objek sementara tanpa nama yang akan dipindahkan atau disalin ke tujuan oleh kompiler naif, salinan atau perpindahan dapat dihilangkan sesuai 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  

Bahkan ketika copy elision terjadi dan copy-/ move-constructor tidak dipanggil, itu harus ada dan dapat diakses (seolah-olah tidak ada optimasi yang terjadi sama sekali), jika tidak, program ini salah bentuk.

Anda harus mengizinkan elisi salinan seperti itu hanya di tempat-tempat di mana itu tidak akan mempengaruhi perilaku yang dapat diamati dari perangkat lunak Anda. Salinan salinan adalah satu-satunya bentuk optimasi yang diizinkan memiliki (yaitu elide) efek samping yang dapat diamati. Contoh:

#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 menyediakan -fno-elide-constructorsopsi untuk menonaktifkan copy elision. Jika Anda ingin menghindari kemungkinan copy elision, gunakan -fno-elide-constructors.

Sekarang hampir semua kompiler menyediakan elisi salinan saat optimasi diaktifkan (dan jika tidak ada opsi lain yang ditetapkan untuk menonaktifkannya).

Kesimpulan

Dengan setiap elisi salinan, satu konstruksi dan satu penghancuran salinan yang cocok dihilangkan, sehingga menghemat waktu CPU, dan satu objek tidak dibuat, sehingga menghemat ruang pada bingkai tumpukan.

Ajay yadav
sumber
6
pernyataan ABC obj2(xyz123());apakah itu NRVO atau RVO? apakah itu tidak mendapatkan variabel sementara / objek yang sama dengan ABC xyz = "Stack Overflow";//RVO
Asif Mushtaq
3
Untuk memiliki ilustrasi RVO yang lebih konkret, Anda dapat merujuk ke rakitan yang dihasilkan kompiler (ubah flag kompiler -fno-elide-constructors untuk melihat diff). godbolt.org/g/Y2KcdH
Gab 是 好人