"Optimalisasi prematur adalah akar dari semua kejahatan"
Saya pikir ini yang bisa kita sepakati bersama. Dan saya berusaha sangat keras untuk menghindari hal itu.
Tetapi baru-baru ini saya telah bertanya-tanya tentang praktik melewati parameter oleh const referensi, bukan oleh Value . Saya telah diajar / belajar bahwa argumen fungsi non-sepele (yaitu kebanyakan tipe non-primitif) sebaiknya dilewatkan dengan referensi const - cukup banyak buku yang saya baca merekomendasikan ini sebagai "praktik terbaik".
Namun saya tetap bertanya-tanya: Kompiler modern dan fitur bahasa baru dapat bekerja dengan sangat baik, sehingga pengetahuan yang saya pelajari mungkin sudah ketinggalan zaman, dan saya tidak pernah benar-benar peduli untuk membuat profil jika ada perbedaan kinerja antara
void fooByValue(SomeDataStruct data);
dan
void fooByReference(const SomeDataStruct& data);
Apakah praktik yang telah saya pelajari - melewati referensi const (secara default untuk tipe non-sepele) - optimasi prematur?
sumber
Jawaban:
"Optimalisasi prematur" bukan tentang menggunakan optimisasi awal . Ini adalah tentang mengoptimalkan sebelum masalah dipahami, sebelum runtime dipahami, dan sering membuat kode kurang mudah dibaca dan kurang dapat dipelihara untuk hasil yang meragukan.
Menggunakan "const &" alih-alih melewati objek dengan nilai adalah optimasi yang dipahami dengan baik, dengan efek yang dipahami dengan baik pada runtime, dengan praktis tidak ada upaya, dan tanpa efek buruk pada keterbacaan dan pemeliharaan. Ini benar-benar meningkatkan keduanya, karena memberi tahu saya bahwa panggilan tidak akan mengubah objek yang dilewatkan. Jadi menambahkan "const &" tepat ketika Anda menulis kode BUKAN PREMATUR.
sumber
const&
, jadi saya pikir pertanyaannya cukup masuk akal.const& foo
mengatakan fungsi tidak akan memodifikasi foo, jadi pemanggil aman. Tetapi nilai yang disalin mengatakan bahwa tidak ada utas lain yang dapat mengubah foo, sehingga callee aman. Baik? Jadi, dalam aplikasi multi-utas, jawabannya tergantung pada kebenaran, bukan optimasi.TL; DR: Lewati referensi const masih merupakan ide bagus di C ++, semua hal dipertimbangkan. Bukan optimasi prematur.
TL; DR2: Sebagian besar pepatah tidak masuk akal, sampai mereka melakukannya.
Tujuan
Jawaban ini hanya mencoba untuk memperpanjang item yang ditautkan pada Pedoman Inti C ++ (pertama kali disebutkan dalam komentar amon) sedikit.
Jawaban ini tidak mencoba untuk mengatasi masalah bagaimana berpikir dan menerapkan dengan baik berbagai pepatah yang beredar luas di kalangan programmer, terutama masalah rekonsiliasi antara kesimpulan atau bukti yang saling bertentangan.
Penerapan
Jawaban ini berlaku untuk panggilan fungsi (cakupan bersarang yang tidak dapat dilepas pada utas yang sama) saja.
(Catatan tambahan.) Ketika hal-hal yang dapat dilewati dapat lolos dari ruang lingkup (yaitu memiliki masa pakai yang berpotensi melebihi lingkup luar), menjadi lebih penting untuk memenuhi kebutuhan aplikasi akan manajemen seumur hidup objek sebelum hal lain. Biasanya, ini memerlukan menggunakan referensi yang juga mampu manajemen seumur hidup, seperti pointer pintar. Alternatif mungkin menggunakan manajer. Perhatikan bahwa, lambda adalah semacam ruang lingkup yang bisa dilepas; menangkap lambda berperilaku seperti memiliki cakupan objek. Karena itu, berhati-hatilah dengan tangkapan lambda. Juga berhati-hatilah dengan bagaimana lambda itu sendiri diteruskan - dengan salinan atau referensi.
Kapan melewati nilai
Untuk nilai-nilai yang skalar (primitif standar yang sesuai dengan register mesin dan memiliki semantik nilai) yang tidak perlu komunikasi-oleh-mutabilitas (referensi bersama), berikan nilai.
Untuk situasi di mana callee membutuhkan kloning objek atau agregat, berikan nilai, di mana salinan callee memenuhi kebutuhan objek kloning.
Kapan melewati referensi, dll.
untuk semua situasi lain, lewati petunjuk, referensi, petunjuk cerdas, pegangan (lihat: idiom pegangan-tubuh), dll. Setiap kali nasihat ini diikuti, terapkan prinsip koreksi-kebenaran seperti biasa.
Benda-benda (agregat, objek, susunan, struktur data) yang cukup besar dalam tapak memori harus selalu dirancang untuk memfasilitasi referensi demi referensi, untuk alasan kinerja. Nasihat ini jelas berlaku ketika jumlahnya ratusan byte atau lebih. Saran ini adalah garis batas ketika puluhan byte.
Paradigma yang tidak biasa
Ada paradigma pemrograman tujuan khusus yang beratnya disalin oleh niat. Misalnya, pemrosesan string, serialisasi, komunikasi jaringan, isolasi, pembungkus perpustakaan pihak ketiga, komunikasi antar-memori berbagi-memori, dll. Dalam area aplikasi atau paradigma pemrograman ini, data disalin dari struct ke struct, atau kadang-kadang dikemas kembali ke dalam array byte.
Bagaimana spesifikasi bahasa memengaruhi jawaban ini, sebelum optimasi dipertimbangkan.
Sub-TL; DR Menyebarluaskan referensi tidak boleh meminta kode; melewati referensi-referensi memenuhi kriteria ini. Namun, semua bahasa lain memenuhi kriteria ini dengan mudah.
(Pemrogram C ++ pemula disarankan untuk melewati bagian ini sepenuhnya.)
(Awal bagian ini sebagian diilhami oleh jawaban gnasher729. Namun, kesimpulan yang berbeda tercapai.)
C ++ memungkinkan copy constructor dan operator penugasan yang ditentukan pengguna.
(Ini adalah pilihan berani yang menakjubkan dan disesalkan. Ini jelas merupakan perbedaan dari norma yang dapat diterima saat ini dalam desain bahasa.)
Bahkan jika programmer C ++ tidak mendefinisikan satu, kompiler C ++ harus menghasilkan metode seperti itu berdasarkan pada prinsip-prinsip bahasa, dan kemudian menentukan apakah kode tambahan perlu dieksekusi selain
memcpy
. Misalnya, aclass
/struct
yang mengandung astd::vector
anggota harus memiliki copy-constructor dan operator penugasan yang non-sepele.Dalam bahasa lain, konstruktor salin dan kloning objek tidak disarankan (kecuali jika benar-benar diperlukan dan / atau bermakna bagi semantik aplikasi), karena objek memiliki semantik referensi, berdasarkan desain bahasa. Bahasa-bahasa ini biasanya akan memiliki mekanisme pengumpulan sampah yang didasarkan pada jangkauan alih-alih kepemilikan berbasis lingkup atau penghitungan referensi.
Ketika referensi atau pointer (termasuk referensi const) diteruskan dalam C ++ (atau C), programmer yakin bahwa tidak ada kode khusus (fungsi yang ditentukan pengguna atau kompiler yang dihasilkan) yang akan dieksekusi, selain penyebaran nilai alamat (referensi atau penunjuk). Ini adalah kejelasan perilaku yang menurut programmer C ++ nyaman.
Namun, latar belakangnya adalah bahwa bahasa C ++ tidak rumit, sehingga kejelasan perilaku ini seperti oasis (habitat yang dapat bertahan) di suatu tempat di sekitar zona kejatuhan nuklir.
Untuk menambahkan lebih banyak berkah (atau penghinaan), C ++ memperkenalkan referensi universal (nilai-r) untuk memfasilitasi operator pemindahan yang ditentukan pengguna (pemindah-konstruktor dan operator pemindah-pindah) dengan kinerja yang baik. Ini menguntungkan penggunaan kasus yang sangat relevan (memindahkan (mentransfer) objek dari satu contoh ke yang lain), dengan cara mengurangi kebutuhan untuk menyalin dan kloning mendalam. Namun, dalam bahasa lain, tidak masuk akal untuk berbicara tentang perpindahan benda semacam itu.
(Bagian di luar topik) Bagian yang didedikasikan untuk artikel, "Ingin Kecepatan? Lewati Nilai!" ditulis sekitar tahun 2009.
Artikel itu ditulis pada tahun 2009 dan menjelaskan alasan desain untuk nilai-r dalam C ++. Artikel itu menyajikan argumen balasan yang valid untuk kesimpulan saya di bagian sebelumnya. Namun, contoh kode artikel dan klaim kinerja telah lama ditolak.
Sub-TL; DR Desain semantik nilai-r dalam C ++ memungkinkan semantik sisi-pengguna yang elegan pada sebuah
Sort
fungsi, misalnya. Elegan ini tidak mungkin untuk model (meniru) dalam bahasa lain.Fungsi pengurutan diterapkan pada keseluruhan struktur data. Seperti disebutkan di atas, akan lambat jika banyak penyalinan terlibat. Sebagai pengoptimalan kinerja (yang praktis relevan), fungsi pengurutan dirancang untuk bersifat merusak dalam beberapa bahasa selain C ++. Merusak berarti bahwa struktur data target dimodifikasi untuk mencapai tujuan penyortiran.
Dalam C ++, pengguna dapat memilih untuk memanggil salah satu dari dua implementasi: yang destruktif dengan kinerja yang lebih baik, atau yang normal yang tidak mengubah input. (Templat dihilangkan karena singkatnya.)
Selain penyortiran, keanggunan ini juga berguna dalam penerapan algoritma pencarian median destruktif dalam array (awalnya tidak disortir), dengan partisi rekursif.
Namun, perhatikan bahwa, sebagian besar bahasa akan menerapkan pendekatan pohon pencarian biner seimbang untuk menyortir, alih-alih menerapkan algoritma penyortiran destruktif ke array. Oleh karena itu, relevansi praktis dari teknik ini tidak setinggi kelihatannya.
Bagaimana optimasi kompiler memengaruhi jawaban ini
Ketika inlining (dan juga optimasi seluruh program / tautan-waktu) diterapkan di beberapa level pemanggilan fungsi, kompiler dapat melihat (kadang-kadang secara mendalam) aliran data. Ketika ini terjadi, kompiler dapat menerapkan banyak optimasi, beberapa di antaranya dapat menghilangkan penciptaan seluruh objek dalam memori. Biasanya, ketika situasi ini berlaku, tidak masalah jika parameter dilewatkan oleh nilai atau oleh referensi-konstanta, karena kompiler dapat menganalisis secara mendalam.
Namun, jika fungsi tingkat bawah memanggil sesuatu yang di luar analisis (misalnya sesuatu di pustaka yang berbeda di luar kompilasi, atau grafik panggilan yang terlalu rumit), maka kompiler harus mengoptimalkan secara defensif.
Objek yang lebih besar dari nilai register mesin dapat disalin dengan instruksi memuat / menyimpan memori eksplisit, atau dengan panggilan ke
memcpy
fungsi terhormat . Pada beberapa platform, kompiler menghasilkan instruksi SIMD untuk berpindah di antara dua lokasi memori, setiap instruksi bergerak puluhan byte (16 atau 32).Diskusi tentang masalah verbositas atau kekacauan visual
Pemrogram C ++ terbiasa dengan hal ini, yaitu selama seorang programmer tidak membenci C ++, overhead penulisan atau membaca referensi-referensi dalam kode sumber tidak mengerikan.
Analisis biaya-manfaat mungkin telah dilakukan berkali-kali sebelumnya. Saya tidak tahu apakah ada yang ilmiah yang harus dikutip. Saya kira sebagian besar analisis akan non-ilmiah atau tidak dapat direproduksi.
Inilah yang saya bayangkan (tanpa bukti atau referensi yang kredibel) ...
sumber
Ini tidak menyarankan programmer untuk menggunakan teknik paling lambat yang tersedia. Ini tentang fokus pada kejelasan saat menulis program. Seringkali, kejelasan dan efisiensi merupakan pertukaran: jika Anda harus memilih satu saja, pilihlah kejelasan. Tetapi jika Anda dapat mencapai keduanya dengan mudah, tidak perlu melumpuhkan kejelasan (seperti memberi sinyal bahwa ada sesuatu yang konstan) hanya untuk menghindari efisiensi.
sumber
Lewat oleh ([const] [rvalue] referensi) | (value) harus tentang maksud dan janji yang dibuat oleh antarmuka. Itu tidak ada hubungannya dengan kinerja.
Aturan Jempol Richy:
sumber
Secara teoritis, jawabannya harus ya. Dan, pada kenyataannya, itu adalah ya beberapa waktu - sebagai soal fakta, melewati dengan referensi const bukan hanya melewati nilai dapat menjadi pesimisasi, bahkan dalam kasus di mana nilai yang lewat terlalu besar untuk muat dalam satu register (atau sebagian besar heuristik lain yang coba digunakan orang untuk menentukan kapan harus lewat nilai atau tidak). Bertahun-tahun yang lalu, David Abrahams menulis sebuah artikel bernama "Want Speed? Pass by Value!" meliputi beberapa kasus ini. Tidak lagi mudah ditemukan, tetapi jika Anda dapat menggali salinannya, itu layak dibaca (IMO).
Dalam kasus tertentu lewat referensi const, namun, saya akan mengatakan idiom yang begitu mapan bahwa situasinya lebih atau kurang terbalik: kecuali Anda tahu jenis akan
char
/short
/int
/long
, orang berharap untuk melihatnya lewat const referensi secara default, jadi mungkin yang terbaik untuk mengikuti adalah kecuali Anda memiliki alasan yang cukup spesifik untuk melakukan sebaliknya.sumber