Apakah meneruskan argumen sebagai referensi konstanta optimasi prematur?

20

"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?

CharonX
sumber
1
Lihat juga: F.call di C + + Core Guidelines untuk diskusi tentang berbagai strategi passing parameter.
amon
1
@DocBrown Jawaban yang diterima untuk pertanyaan itu merujuk pada prinsip yang paling mengejutkan , yang mungkin berlaku di sini juga (yaitu menggunakan referensi const adalah standar industri, dll.). Yang mengatakan, saya tidak setuju bahwa pertanyaan itu adalah duplikat: Pertanyaan yang Anda rujuk bertanya apakah itu praktik yang buruk untuk (umumnya) mengandalkan optimasi kompiler. Pertanyaan ini menanyakan kebalikannya: Apakah memberikan referensi const merupakan pengoptimalan (prematur)?
CharonX
@CharonX: jika seseorang dapat mengandalkan optimisasi kompiler di sini, jawaban atas pertanyaan Anda jelas "ya, optimasi manual tidak diperlukan, itu terlalu dini". Jika seseorang tidak dapat mengandalkan itu (mungkin karena Anda tidak tahu sebelumnya kompiler mana yang akan digunakan untuk kode), jawabannya adalah "untuk objek yang lebih besar itu mungkin bukan prematur". Jadi, bahkan jika kedua pertanyaan itu secara harfiah tidak sama, mereka sepertinya cukup mirip untuk menghubungkan keduanya sebagai duplikat.
Doc Brown
1
@DocBrown: Jadi, sebelum Anda dapat mendeklarasikannya sebagai penipuan, tunjukkan di mana dalam pertanyaan itu dikatakan bahwa kompiler akan diizinkan dan dapat "mengoptimalkan" itu.
Deduplicator

Jawaban:

49

"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.

gnasher729
sumber
2
Saya setuju pada bagian "praktis tidak ada usaha" dari jawaban Anda. Tetapi optimasi prematur adalah yang pertama dan terutama tentang optimasi sebelum mereka adalah dampak kinerja yang terukur dan terukur. Dan saya tidak berpikir sebagian besar programmer C ++ (termasuk saya) melakukan pengukuran sebelum menggunakan const&, jadi saya pikir pertanyaannya cukup masuk akal.
Doc Brown
1
Anda mengukur sebelum mengoptimalkan untuk mengetahui apakah ada tradeoffs yang sepadan. Dengan const & upaya total adalah mengetik tujuh karakter, dan ia memiliki kelebihan lain. Ketika Anda tidak berniat untuk mengubah variabel yang dilewatkan, itu menguntungkan bahkan jika tidak ada peningkatan kecepatan.
gnasher729
3
Saya bukan ahli C, jadi pertanyaan :. const& foomengatakan 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.
user949300
1
@DocBrown Anda akhirnya dapat quetion motivasi pengembang yang menempatkan const &? Jika ia melakukannya hanya untuk kinerja tanpa mempertimbangkan sisanya, itu mungkin dianggap sebagai optimasi prematur. Sekarang jika dia meletakkannya karena dia tahu itu akan menjadi parameter const maka dia hanya mendokumentasikan sendiri kodenya dan memberikan kesempatan kepada kompiler untuk mengoptimalkan, yang lebih baik.
Walfrat
1
@ user949300: Beberapa fungsi memungkinkan argumen mereka untuk dimodifikasi secara bersamaan atau dengan panggilan balik yang digunakan, dan mereka secara eksplisit mengatakannya.
Deduplicator
16

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, a class/ structyang 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 sebuahSort 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.)

/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
    std::vector<T> result(std::move(input)); /* destructive move */
    std::sort(result.begin(), result.end()); /* in-place sorting */
    return result; /* return-value optimization (RVO) */
}

/*caller specifically passes in read-only argument*/ 
std::vector<T> my_sort(const std::vector<T>& input)
{
    /* reuse destructive implementation by letting it work on a clone. */
    /* Several things involved; e.g. expiring temporaries as r-value */
    /* return-value optimization, etc. */
    return my_sort(std::vector<T>(input));  
}

/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/    

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 memcpyfungsi 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) ...

  • Ya, itu mempengaruhi kinerja perangkat lunak yang ditulis dalam bahasa ini.
  • Jika kompiler dapat memahami tujuan kode, itu berpotensi cukup pintar untuk mengotomatisasi itu
  • Sayangnya, dalam bahasa yang mendukung mutabilitas (sebagai lawan kemurnian fungsional), kompiler akan mengklasifikasikan sebagian besar hal sebagai yang termutasi, oleh karena itu deduksi otomatis dari constness akan menolak sebagian besar hal sebagai non-konstanta.
  • Overhead mental tergantung pada orang; orang-orang yang menganggap ini sebagai overhead mental yang tinggi akan menolak C ++ sebagai bahasa pemrograman yang layak.
rwong
sumber
Ini adalah salah satu situasi di mana saya berharap saya dapat menerima dua jawaban daripada harus memilih hanya satu ... sigh
CharonX
8

Dalam makalah DonaldKnuth "StructuredProgrammingWithGoToStatements", ia menulis: "Programmer menghabiskan banyak waktu untuk memikirkan, atau mengkhawatirkan, kecepatan bagian nonkritis dari program mereka, dan upaya efisiensi ini sebenarnya memiliki dampak negatif yang kuat ketika debugging dan pemeliharaan dipertimbangkan Kita harus melupakan efisiensi kecil, katakanlah sekitar 97% dari waktu: optimasi prematur adalah akar dari semua kejahatan. Namun kita tidak boleh melewatkan peluang kita dalam 3% kritis itu. " - Optimalisasi Prematur

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.

Lawrence
sumber
3
"Jika kamu harus memilih hanya satu, pilihlah kejelasan." Yang kedua lebih disukai sebagai gantinya, karena Anda mungkin terpaksa memilih yang lain.
Deduplicator
@Dupuplikator Terima kasih. Namun, dalam konteks OP, programmer memiliki kebebasan untuk memilih.
Lawrence
Jawaban Anda berbunyi lebih umum daripada itu ...
Deduplicator
@Dupuplikator Ah, tetapi konteks jawaban saya adalah (juga) yang programmer pilih. Jika pilihan itu dipaksakan pada programmer, bukan "kamu" yang memilih :). Saya menganggap perubahan yang Anda sarankan dan tidak akan keberatan jika Anda mengedit jawaban saya, tetapi saya lebih suka kata-kata yang ada untuk kejelasannya.
Lawrence
7

Lewat oleh ([const] [rvalue] referensi) | (value) harus tentang maksud dan janji yang dibuat oleh antarmuka. Itu tidak ada hubungannya dengan kinerja.

Aturan Jempol Richy:

void foo(X x);          // I intend to own the x you gave me, whether by copy, move or direct initialisation on the call stack.     

void foo(X&& x);        // I intend to steal x from you. Do not use it other than to re-assign to it after calling me.

void foo(X const& x);   // I guarantee not to change your x

void foo(X& x);         // I may modify your x and I will leave it in a defined state
Richard Hodges
sumber
3

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.

Jerry Coffin
sumber