Bisakah C ++ modern memberi Anda kinerja gratis?

205

Kadang-kadang diklaim bahwa C ++ 11/14 dapat membuat Anda meningkatkan kinerja bahkan ketika hanya mengkompilasi kode C ++ 98. Pembenaran biasanya sepanjang garis semantik bergerak, karena dalam beberapa kasus pembangun nilai secara otomatis dihasilkan atau sekarang bagian dari STL. Sekarang saya bertanya-tanya apakah kasus-kasus ini sebelumnya sebenarnya sudah ditangani oleh RVO atau optimisasi kompiler serupa.

Pertanyaan saya kemudian adalah apakah Anda dapat memberi saya contoh aktual dari kode C ++ 98 yang, tanpa modifikasi, berjalan lebih cepat menggunakan kompiler yang mendukung fitur bahasa baru. Saya mengerti bahwa kompiler penyesuai standar tidak diharuskan untuk melakukan copy elision dan hanya dengan alasan itu semantik bergerak dapat menghasilkan kecepatan, tetapi saya ingin melihat kasus yang kurang patologis, jika Anda mau.

EDIT: Hanya untuk memperjelas, saya tidak bertanya apakah kompiler baru lebih cepat dari kompiler lama, tetapi jika ada kode di mana menambahkan -std = c ++ 14 ke flag kompiler saya akan berjalan lebih cepat (hindari salinan, tetapi jika Anda dapat datang dengan hal lain selain memindahkan semantik, saya akan tertarik juga)

besar
sumber
3
Ingat bahwa optimasi salin dan nilai pengembalian dilakukan ketika membuat objek baru menggunakan copy constructor. Namun, di operator penugasan salinan, tidak ada salinan salinan (bagaimana bisa, karena kompiler tidak tahu apa yang harus dilakukan dengan objek yang sudah dibangun yang bukan sementara). Oleh karena itu, dalam kasus itu, C ++ 11/14 menang besar, dengan memberi Anda kemungkinan menggunakan operator penugasan bergerak. Tentang pertanyaan Anda, saya tidak berpikir kode C ++ 98 harus lebih cepat jika dikompilasi oleh kompiler C ++ 11/14, mungkin lebih cepat karena kompilernya lebih baru.
vsoftco
27
Juga kode yang menggunakan pustaka standar berpotensi lebih cepat, bahkan jika Anda membuatnya sepenuhnya kompatibel dengan C ++ 98, karena dalam C ++ 11/14 pustaka yang mendasarinya menggunakan semantik yang dipindahkan secara internal jika memungkinkan. Jadi kode yang terlihat identik dalam C ++ 98 dan C ++ 11/14 akan (mungkin) lebih cepat dalam kasus terakhir, setiap kali Anda menggunakan objek pustaka standar seperti vektor, daftar dll dan memindahkan semantik membuat perbedaan.
vsoftco
1
@vsoftco, Itulah jenis situasi yang saya singgung, tetapi tidak dapat memberikan contoh: Dari apa yang saya ingat jika saya harus mendefinisikan konstruktor salinan, konstruktor gerakan tidak akan secara otomatis dihasilkan, yang membuat kita dengan kelas yang sangat sederhana di mana RVO, saya pikir, selalu berfungsi. Pengecualian mungkin ada hubungannya dengan wadah STL, di mana konstruktor nilai dihasilkan oleh pelaksana perpustakaan (artinya saya tidak perlu mengubah apa pun dalam kode untuk menggunakan gerakan).
alarge
kelas tidak perlu sederhana agar tidak memiliki copy constructor. C ++ berkembang pada semantik nilai, dan menyalin konstruktor, operator penugasan, destruktor dll harus menjadi pengecualian.
sp2danny
1
@Eric Terima kasih atas tautannya, itu menarik. Namun, setelah cepat memeriksanya, keuntungan kecepatan di dalamnya tampaknya sebagian besar berasal dari penambahan std::movedan pemindahan konstruktor (yang akan membutuhkan modifikasi pada kode yang ada). Satu-satunya hal yang benar-benar terkait dengan pertanyaan saya adalah kalimat "Anda mendapatkan keuntungan kecepatan langsung hanya dengan mengkompilasi ulang", yang tidak didukung oleh contoh apa pun (itu menyebutkan STL pada slide yang sama, seperti yang saya lakukan dalam pertanyaan saya, tetapi tidak ada yang spesifik ). Saya meminta beberapa contoh. Jika saya salah membaca slide, beri tahu saya.
alarge

Jawaban:

221

Saya mengetahui 5 kategori umum di mana mengkompilasi ulang kompiler C ++ 03 karena C ++ 11 dapat menyebabkan peningkatan kinerja tanpa batas yang secara praktis tidak terkait dengan kualitas implementasi. Ini semua adalah variasi dari semantik gerakan.

std::vector realokasi

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03

setiap kali foobuffer dialokasikan kembali dalam C ++ 03 itu disalin setiap vectordalam bar.

Dalam C ++ 11 ia malah memindahkan bar::datas, yang pada dasarnya gratis.

Dalam hal ini, ini bergantung pada optimasi di dalam stdwadah vector. Dalam setiap kasus di bawah ini, penggunaan stdkontainer hanya karena mereka adalah objek C ++ yang memiliki movesemantik efisien dalam C ++ 11 "secara otomatis" ketika Anda meningkatkan kompiler Anda. Objek yang tidak memblokirnya yang berisi stdwadah juga mewarisi movekonstruktor yang ditingkatkan secara otomatis .

Kegagalan NRVO

Ketika NRVO (bernama optimisasi nilai balik) gagal, dalam C ++ 03 jatuh kembali pada salinan, pada C ++ 11 jatuh kembali bergerak. Kegagalan NRVO mudah:

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}

atau bahkan:

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}

Kami memiliki tiga nilai - nilai kembali, dan dua nilai berbeda dalam fungsi. Elision memungkinkan nilai-nilai dalam fungsi untuk 'digabung' dengan nilai kembali, tetapi tidak dengan satu sama lain. Keduanya tidak dapat digabungkan dengan nilai pengembalian tanpa bergabung satu sama lain.

Masalah mendasarnya adalah bahwa NRVO elision rapuh, dan kode dengan perubahan tidak di dekat returnsitus tiba-tiba dapat memiliki pengurangan kinerja besar-besaran di tempat itu tanpa diagnostik yang dikeluarkan. Dalam kebanyakan kasus kegagalan NRVO C ++ 11 berakhir dengan a move, sedangkan C ++ 03 berakhir dengan salinan.

Mengembalikan argumen fungsi

Penghilangan juga tidak mungkin di sini:

std::set<int> func(std::set<int> in){
  return in;
}

di C ++ 11 ini murah: di C ++ 03 tidak ada cara untuk menghindari salinan. Argumen untuk fungsi tidak dapat dielakkan dengan nilai kembali, karena masa pakai dan lokasi parameter dan nilai kembali dikelola oleh kode panggilan.

Namun, C ++ 11 dapat berpindah dari satu ke yang lain. (Dalam contoh mainan yang kurang, sesuatu mungkin dilakukan untuk set).

push_back atau insert

Akhirnya, elisi ke dalam wadah tidak terjadi: tetapi C ++ 11 membebani secara berlebihan nilai memindahkan operator insert, yang menyimpan salinan.

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );

di C ++ 03 whateverdibuat sementara , kemudian disalin ke dalam vektor v. 2 std::stringbuffer dialokasikan, masing-masing dengan data yang identik, dan satu dibuang.

Dalam C ++ 11 sementara whateverdibuat. The whatever&& push_backberlebihan maka moves yang sementara ke vektor v. Satu std::stringbuffer dialokasikan, dan dipindahkan ke vektor. Sebuah kosong std::stringdibuang.

Tugas

Dicuri dari jawaban @ Jarod42 di bawah ini.

Elision tidak dapat terjadi dengan penugasan, tetapi pindah-dari bisa.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

di sini some_functionmengembalikan kandidat untuk dielakkan, tetapi karena tidak digunakan untuk membangun objek secara langsung, itu tidak dapat dielakkan. Dalam C ++ 03, hasil di atas dalam konten sementara disalin ke some_value. Di C ++ 11, ia dipindahkan ke some_value, yang pada dasarnya gratis.


Untuk efek penuh dari hal di atas, Anda memerlukan kompiler yang mensintesis pemindahan konstruktor dan tugas untuk Anda.

MSVC 2013 mengimplementasikan gerakan konstruktor dalam stdwadah, tetapi tidak mensintesis gerakan konstruktor pada tipe Anda.

Jadi tipe yang mengandung std::vectordan sejenisnya tidak mendapatkan peningkatan seperti itu di MSVC2013, tetapi akan mulai mendapatkannya di MSVC2015.

dentang dan gcc telah lama diimplementasikan konstruktor bergerak implisit. Kompiler Intel 2013 akan mendukung pembuatan konstruktor pemindahan implisit jika Anda lulus -Qoption,cpp,--gen_move_operations(mereka tidak melakukannya secara default dalam upaya untuk kompatibel dengan MSVC2013).

Yakk - Adam Nevraumont
sumber
1
@ besarkan ya. Tetapi untuk memindahkan konstruktor menjadi berkali-kali lebih efisien daripada menyalin konstruktor, biasanya harus memindahkan sumber daya daripada menyalinnya. Tanpa menulis konstruktor pemindahan Anda sendiri (dan hanya mengkompilasi ulang program C ++ 03), semua stdkontainer perpustakaan akan diperbarui dengan movekonstruktor "gratis", dan (jika Anda tidak memblokirnya) konstruksi yang menggunakan objek tersebut ( dan kata benda) akan mulai mendapatkan konstruksi gerakan bebas dalam sejumlah situasi. Banyak dari situasi tersebut dicakup oleh elision di C ++ 03: tidak semua.
Yakk - Adam Nevraumont
5
Itu adalah implementasi pengoptimal yang buruk, kemudian, karena objek yang diberi nama berbeda dikembalikan tidak memiliki masa hidup yang tumpang tindih, RVO secara teoritis masih mungkin.
Ben Voigt
2
@alarge Ada tempat-tempat di mana elision gagal, seperti ketika dua objek dengan masa hidup yang tumpang tindih dapat dielusi menjadi sepertiga, tetapi tidak satu sama lain. Kemudian pindah diperlukan di C ++ 11, dan salin di C ++ 03 (mengabaikan as-if). Elisi sering rapuh dalam praktik. Penggunaan stdkontainer di atas sebagian besar karena mereka murah untuk memindahkan exoensive untuk menyalin jenis yang Anda dapatkan 'gratis' di C ++ 11 ketika mengkompilasi ulang C ++ 03. The vector::resizeadalah pengecualian: menggunakan moveC ++ 11.
Yakk - Adam Nevraumont
27
Saya hanya melihat 1 kategori umum yang bergerak semantik, dan 5 kasus khusus itu.
Johannes Schaub - litb
3
@ sws Saya mengerti, Anda tidak menganggap "menyebabkan program tidak mengalokasikan banyak 1000s alokasi kilobyte banyak, dan alih-alih memindahkan pointer ke sekitar" sudah mencukupi. Anda ingin hasil waktunya. Microbenchmark tidak lebih merupakan bukti peningkatan kinerja daripada bukti yang secara mendasar Anda lakukan lebih sedikit. Pendek dari beberapa 100 aplikasi dunia nyata dalam berbagai industri yang diprofilkan dengan profil tugas dunia nyata tidak benar-benar bukti. Saya mengambil klaim yang tidak jelas tentang "kinerja gratis" dan membuatnya menjadi fakta spesifik tentang perbedaan perilaku program di bawah C ++ 03 dan C ++ 11.
Yakk - Adam Nevraumont
46

jika Anda memiliki sesuatu seperti:

std::vector<int> foo(); // function declaration.
std::vector<int> v;

// some code

v = foo();

Anda mendapat salinan di C ++ 03, sedangkan Anda mendapat tugas pemindahan di C ++ 11. sehingga Anda memiliki optimasi gratis dalam hal ini.

Jarod42
sumber
4
@ Yakk: Bagaimana copy elision terjadi dalam penugasan?
Jarod42
2
@ Jarod42 Saya juga percaya bahwa salin salinan tidak mungkin dalam tugas, karena sisi kiri sudah dibangun dan tidak ada cara yang masuk akal bagi kompiler untuk mengetahui apa yang harus dilakukan dengan data "lama" setelah mencuri sumber daya dari kanan sisi tangan. Tapi mungkin aku salah, aku ingin sekali menemukan jawabannya. Menyalin elisi masuk akal saat Anda menyalin konstruk, karena objeknya "segar" dan tidak ada masalah memutuskan apa yang harus dilakukan dengan data lama. Sejauh yang saya tahu, satu-satunya pengecualian adalah ini: "Penugasan hanya dapat dihapus berdasarkan aturan as-if"
vsoftco
4
Good C ++ 03 kode sudah melakukan langkah dalam hal ini, melaluifoo().swap(v);
Ben Voigt
@ BenVoigt yakin, tetapi tidak semua kode dioptimalkan, dan tidak semua tempat yang mudah dijangkau.
Yakk - Adam Nevraumont
Salinan salinan dapat berfungsi dalam tugas, seperti kata @BenVoigt. Istilah yang lebih baik adalah RVO (optimasi nilai balik) dan hanya berfungsi jika foo () telah diterapkan seperti itu.
DrumM