Saya bekerja di .Net, C # shop dan saya memiliki rekan kerja yang terus bersikeras bahwa kita harus menggunakan pernyataan Switch raksasa dalam kode kita dengan banyak "Kasus" daripada pendekatan yang lebih berorientasi objek. Argumennya secara konsisten kembali ke fakta bahwa pernyataan Switch mengkompilasi ke "tabel lompatan cpu" dan karena itu merupakan opsi tercepat (meskipun dalam hal lain tim kami diberitahu bahwa kami tidak peduli dengan kecepatan).
Sejujurnya aku tidak punya argumen menentang ini ... karena aku tidak tahu apa yang dia bicarakan.
Apakah dia benar
Apakah dia hanya berbicara di pantatnya?
Hanya berusaha belajar di sini.
c#
.net
switch-statement
James P. Wright
sumber
sumber
Jawaban:
Dia mungkin seorang peretas C tua dan ya, dia berbicara keluar dari pantatnya. .Net bukan C ++; kompiler .Net terus menjadi lebih baik dan sebagian besar peretas cerdas adalah kontra-produktif, jika tidak hari ini maka dalam versi .Net berikutnya. Fungsi-fungsi kecil lebih disukai karena .Net JIT-s setiap fungsi satu kali sebelum digunakan. Jadi, jika beberapa kasus tidak pernah terkena selama siklus program, jadi tidak ada biaya yang dikeluarkan dalam kompilasi JIT ini. Bagaimanapun, jika kecepatan bukan masalah, seharusnya tidak ada optimasi. Tulis untuk programmer terlebih dahulu, untuk compiler kedua. Rekan kerja Anda tidak akan mudah diyakinkan, jadi saya akan membuktikan secara empiris bahwa kode yang terorganisir lebih baik sebenarnya lebih cepat. Saya akan memilih salah satu contoh terburuknya, menulis ulang dengan cara yang lebih baik, dan kemudian memastikan bahwa kode Anda lebih cepat. Pilih Cherry jika Anda harus. Kemudian jalankan beberapa juta kali, profil dan tunjukkan padanya.
SUNTING
Bill Wagner menulis:
Butir 11: Memahami Ketertarikan Fungsi Kecil (Efektif C # Edisi Kedua) Ingatlah bahwa menerjemahkan kode C # Anda ke dalam kode yang dapat dieksekusi mesin adalah proses dua langkah. Compiler C # menghasilkan IL yang dikirimkan dalam kumpulan. Kompiler JIT menghasilkan kode mesin untuk setiap metode (atau kelompok metode, ketika inlining terlibat), sesuai kebutuhan. Fungsi kecil memudahkan kompiler JIT untuk mengamortisasi biaya itu. Fungsi kecil juga lebih cenderung menjadi kandidat untuk inlining. Ini bukan hanya kekecilan: Aliran kontrol yang lebih sederhana juga sama pentingnya. Lebih sedikit kontrol cabang di dalam fungsi membuatnya lebih mudah untuk kompiler JIT untuk mendaftarkan variabel. Bukan hanya praktik yang baik untuk menulis kode yang lebih jelas; ini adalah cara Anda membuat kode yang lebih efisien saat runtime.
EDIT2:
Jadi ... rupanya pernyataan switch lebih cepat dan lebih baik daripada sekelompok pernyataan if / else, karena satu perbandingan adalah logaritmik dan yang lainnya adalah linear. http://afterence-points.blogspot.com/2007/10/why-is-switch-statement-faster-than-if.html
Yah, pendekatan favorit saya untuk mengganti pernyataan switch besar adalah dengan kamus (atau kadang-kadang bahkan sebuah array jika saya mengaktifkan enum atau int kecil) yang memetakan nilai ke fungsi yang dipanggil untuk menanggapi mereka. Melakukan hal itu memaksa seseorang untuk menghapus banyak keadaan spageti bersama yang jahat, tetapi itu adalah hal yang baik. Pernyataan peralihan besar biasanya merupakan mimpi buruk pemeliharaan. Jadi ... dengan array dan kamus, pencarian akan memakan waktu yang konstan, dan akan ada sedikit memori tambahan yang terbuang.
Saya masih tidak yakin bahwa pergantian pernyataan lebih baik.
sumber
Kecuali kolega Anda dapat memberikan bukti, bahwa perubahan ini memberikan manfaat terukur aktual pada skala seluruh aplikasi, itu lebih rendah daripada pendekatan Anda (yaitu polimorfisme), yang sebenarnya memberikan manfaat seperti itu: pemeliharaan.
Optimalisasi mikro hanya boleh dilakukan, setelah kemacetan dijabarkan. Optimalisasi prematur adalah akar dari semua kejahatan .
Kecepatan dapat diukur. Ada sedikit informasi berguna dalam "pendekatan A lebih cepat daripada pendekatan B". Pertanyaannya adalah " Seberapa cepat? ".
sumber
Siapa yang peduli jika lebih cepat?
Kecuali jika Anda menulis perangkat lunak waktu nyata, kecil kemungkinan peningkatan kecepatan yang mungkin Anda dapatkan dari melakukan sesuatu dengan cara yang benar-benar gila akan membuat banyak perbedaan bagi klien Anda. Aku bahkan tidak mau bertempur melawan yang ini di depan, orang ini jelas tidak akan mendengarkan argumen tentang masalah ini.
Namun, kemampuan mempertahankan adalah tujuan dari permainan ini, dan pernyataan peralihan raksasa bahkan tidak dapat dipertahankan, bagaimana Anda menjelaskan jalur yang berbeda melalui kode ke orang baru? Dokumentasi harus sepanjang kode itu sendiri!
Plus, Anda kemudian mendapatkan ketidakmampuan lengkap untuk pengujian unit secara efektif (terlalu banyak jalur yang mungkin, belum lagi kemungkinan kurangnya antarmuka dll.), Yang membuat kode Anda semakin tidak bisa dirawat.
[Di sisi yang tertarik: JITter berkinerja lebih baik pada metode yang lebih kecil, jadi pernyataan peralihan raksasa (dan metode yang pada dasarnya besar) akan membahayakan kecepatan Anda di majelis besar, IIRC.]
sumber
Langkah menjauh dari pernyataan beralih ...
Pernyataan peralihan jenis ini harus dijauhi seperti wabah karena melanggar Prinsip Terbuka Terbuka . Ini memaksa tim untuk membuat perubahan pada kode yang ada ketika fungsionalitas baru perlu ditambahkan, sebagai lawan dari, hanya menambahkan kode baru.
sumber
Saya selamat dari mimpi buruk yang dikenal sebagai mesin negara berhingga besar yang dimanipulasi oleh pernyataan peralihan besar-besaran. Lebih buruk lagi, dalam kasus saya, FSM membentang tiga C ++ DLL dan itu cukup jelas kode itu ditulis oleh seseorang yang berpengalaman dalam C.
Metrik yang perlu Anda perhatikan adalah:
Saya diberi tugas untuk menambahkan fitur baru ke set DLL itu, dan mampu meyakinkan manajemen bahwa saya akan membutuhkan waktu lama untuk menulis ulang 3 DLL sebagai satu DLL yang berorientasi objek dengan benar sebagaimana bagi saya untuk menambal monyet dan juri rig solusi ke dalam apa yang sudah ada di sana. Penulisan ulang itu sukses besar, karena tidak hanya mendukung fungsi baru tetapi lebih mudah untuk diperluas. Bahkan, tugas yang biasanya akan memakan waktu seminggu untuk memastikan Anda tidak memecahkan apa pun akan berakhir dengan memakan waktu beberapa jam.
Lantas bagaimana dengan waktu eksekusi? Tidak ada peningkatan atau penurunan kecepatan. Agar adil, kinerja kami dibatasi oleh driver sistem, jadi jika solusi berorientasi objek sebenarnya lebih lambat, kami tidak akan mengetahuinya.
Apa yang salah dengan pernyataan peralihan besar-besaran untuk bahasa OO?
sumber
Saya tidak membeli argumen kinerja; ini semua tentang pemeliharaan kode.
TETAPI: kadang-kadang , pernyataan switch raksasa lebih mudah dipertahankan (lebih sedikit kode) daripada sekelompok kelas kecil yang mengesampingkan fungsi virtual dari kelas dasar abstrak. Misalnya, jika Anda mengimplementasikan emulator CPU, Anda tidak akan mengimplementasikan fungsionalitas dari setiap instruksi dalam kelas yang terpisah - Anda hanya akan memasukkannya ke dalam swtich raksasa pada opcode, mungkin memanggil fungsi pembantu untuk instruksi yang lebih kompleks.
Rule of thumb: jika switch entah bagaimana dilakukan pada TYPE, Anda mungkin harus menggunakan fungsi warisan dan virtual. Jika sakelar dilakukan pada VALUE dari tipe tetap (mis., Opcode instruksi, seperti di atas), tidak apa-apa untuk membiarkannya apa adanya.
sumber
Anda tidak dapat meyakinkan saya bahwa:
Secara signifikan lebih cepat daripada:
Selain itu versi OO lebih mudah dikelola.
sumber
Dia benar bahwa kode mesin yang dihasilkan mungkin akan lebih efisien. Essential compiler mengubah pernyataan switch menjadi serangkaian tes dan cabang, yang akan relatif sedikit instruksi. Ada kemungkinan besar bahwa kode yang dihasilkan dari pendekatan yang lebih abstrak akan membutuhkan lebih banyak instruksi.
NAMUN : Hampir pasti bahwa aplikasi khusus Anda tidak perlu khawatir tentang optimasi mikro semacam ini, atau Anda tidak akan menggunakan .net di tempat pertama. Untuk apa pun yang kurang dari aplikasi tertanam yang sangat terbatas, atau pekerjaan intensif CPU Anda harus selalu membiarkan kompiler menangani optimasi. Berkonsentrasi pada penulisan kode yang bersih dan dapat dipelihara. Ini hampir selalu bernilai jauh lebih besar daripada beberapa persepuluh nano-detik dalam waktu eksekusi.
sumber
Salah satu alasan utama untuk menggunakan kelas alih-alih beralih pernyataan adalah bahwa pernyataan beralih cenderung mengarah ke satu file besar yang memiliki banyak logika. Ini adalah mimpi buruk pemeliharaan serta masalah dengan manajemen sumber karena Anda harus memeriksa dan mengedit file besar itu daripada file kelas kecil yang berbeda
sumber
pernyataan switch dalam kode OOP adalah indikasi kuat dari kelas yang hilang
cobalah keduanya dan jalankan beberapa tes kecepatan sederhana; kemungkinan perbedaannya tidak signifikan. Jika ya dan kode itu penting untuk waktu maka pertahankan pernyataan peralihan
sumber
Biasanya saya benci kata, "optimasi prematur", tetapi ini berbau itu. Perlu dicatat bahwa Knuth menggunakan kutipan terkenal ini dalam konteks mendorong untuk menggunakan
goto
pernyataan untuk mempercepat kode di area kritis . Itulah kuncinya: jalur kritis .Dia menyarankan untuk digunakan
goto
untuk mempercepat kode tetapi memperingatkan terhadap para programmer yang ingin melakukan hal-hal semacam ini berdasarkan firasat dan takhayul untuk kode yang bahkan tidak kritis.Untuk mendukung
switch
pernyataan sebanyak mungkin secara seragam di seluruh basis kode (terlepas dari apakah ada beban berat yang ditangani) adalah contoh klasik dari apa yang Knuth sebut sebagai programmer "penny-wise and pound-bodoh" yang menghabiskan sepanjang hari berjuang untuk mempertahankan "dioptimalkan" mereka "kode yang berubah menjadi mimpi buruk debugging sebagai hasil dari mencoba untuk menyimpan uang lebih dari pound. Kode seperti itu jarang dapat dipertahankan apalagi efisien di tempat pertama.Ia benar dari perspektif efisiensi yang sangat mendasar. Tidak ada kompiler yang setahu saya dapat mengoptimalkan kode polimorfik yang melibatkan objek dan pengiriman dinamis lebih baik daripada pernyataan switch. Anda tidak akan pernah berakhir dengan LUT atau tabel lompat ke kode inline dari kode polimorfik, karena kode seperti itu cenderung berfungsi sebagai penghalang pengoptimal untuk kompiler (ia tidak akan tahu fungsi untuk memanggil hingga waktu pengiriman dinamis terjadi).
Lebih berguna untuk tidak memikirkan biaya ini dalam hal tabel lompatan tetapi lebih dalam hal penghalang optimasi. Untuk polimorfisme, pemanggilan
Base.method()
tidak memungkinkan kompiler mengetahui fungsi mana yang sebenarnya akan dipanggil jikamethod
virtual, tidak disegel, dan dapat diganti. Karena tidak tahu fungsi mana yang sebenarnya akan dipanggil terlebih dahulu, ia tidak dapat mengoptimalkan pemanggilan fungsi dan memanfaatkan lebih banyak informasi dalam membuat keputusan optimisasi, karena ia tidak benar-benar tahu fungsi mana yang akan dipanggil di waktu kode dikompilasi.Pengoptimal adalah yang terbaik ketika mereka dapat mengintip ke dalam panggilan fungsi dan membuat optimasi yang benar-benar meratakan penelepon dan callee, atau setidaknya mengoptimalkan penelepon untuk bekerja paling efisien dengan callee. Mereka tidak dapat melakukan itu jika mereka tidak tahu fungsi mana yang sebenarnya akan dipanggil terlebih dahulu.
Menggunakan biaya ini, yang sering berjumlah uang, untuk membenarkan mengubah ini menjadi standar pengkodean yang diterapkan secara umum pada umumnya sangat bodoh, terutama untuk tempat-tempat yang memiliki kebutuhan ekstensibilitas. Itulah hal utama yang ingin Anda perhatikan dengan pengoptimal prematur asli: mereka ingin mengubah masalah kinerja kecil menjadi standar pengkodean yang diterapkan secara seragam di seluruh basis kode tanpa memperhatikan keberlanjutan apa pun.
Saya sedikit tersinggung dengan kutipan "hacker C lama" yang digunakan dalam jawaban yang diterima, karena saya salah satunya. Tidak semua orang yang telah melakukan koding selama beberapa dekade mulai dari perangkat keras yang sangat terbatas telah berubah menjadi pengoptimal dini. Namun saya juga pernah bertemu dan bekerja dengan mereka. Tetapi tipe-tipe itu tidak pernah mengukur hal-hal seperti misprediksi cabang atau cache misses, mereka pikir mereka tahu lebih baik, dan mendasarkan gagasan mereka tentang inefisiensi dalam basis kode produksi kompleks berdasarkan pada takhayul yang tidak berlaku hari ini dan kadang-kadang tidak pernah berlaku. Orang-orang yang benar-benar bekerja di bidang yang kritis terhadap kinerja sering memahami bahwa pengoptimalan yang efektif adalah prioritas yang efektif, dan mencoba untuk menggeneralisasi standar pengkodean yang menurunkan kemampuan pemeliharaan untuk menghemat uang adalah prioritas yang sangat tidak efektif.
Uang adalah penting ketika Anda memiliki fungsi murah yang tidak melakukan banyak pekerjaan yang disebut satu miliar kali dalam lingkaran yang sangat ketat, kinerja-kritis. Dalam hal ini, kami akhirnya menghemat 10 juta dolar. Tidak ada gunanya mencukur uang receh ketika Anda memiliki fungsi yang disebut dua kali yang biayanya saja mencapai ribuan dolar. Tidak bijaksana menghabiskan waktu Anda menawar uang selama pembelian mobil. Layak menawar lebih dari satu sen jika Anda membeli satu juta kaleng soda dari produsen. Kunci optimalisasi yang efektif adalah memahami biaya-biaya ini dalam konteksnya yang tepat. Seseorang yang mencoba menghemat uang untuk setiap pembelian dan menyarankan agar semua orang mencoba menawar lebih banyak uang, apa pun yang mereka beli bukanlah pengoptimal yang terampil.
sumber
Sepertinya rekan kerja Anda sangat memperhatikan kinerja. Mungkin dalam beberapa kasus struktur case / switch besar akan bekerja lebih cepat, tapi semoga kalian akan melakukan percobaan dengan melakukan tes waktu pada versi OO dan versi switch / case. Saya menduga versi OO memiliki lebih sedikit kode dan lebih mudah untuk mengikuti, memahami, dan memelihara. Saya akan memperdebatkan untuk versi OO pertama (karena pemeliharaan / keterbacaan awalnya harus lebih penting), dan hanya mempertimbangkan versi switch / case hanya jika versi OO memiliki masalah kinerja yang serius dan dapat ditunjukkan bahwa switch / case akan membuat perbaikan yang signifikan.
sumber
Salah satu keuntungan polymorphism yang tidak ada yang menyebutkan bahwa Anda akan dapat menyusun kode Anda lebih baik menggunakan pewarisan jika Anda selalu mengaktifkan daftar kasus yang sama, tetapi kadang-kadang beberapa kasus ditangani dengan cara yang sama dan kadang-kadang tidak
Misalnya. jika Anda beralih di antara
Dog
,Cat
danElephant
, dan kadang-kadangDog
danCat
memiliki kasus yang sama, Anda dapat membuat keduanya mewarisi dari kelas abstrakDomesticAnimal
dan meletakkan fungsi tersebut di kelas abstrak.Juga, saya terkejut bahwa beberapa orang menggunakan parser sebagai contoh di mana Anda tidak akan menggunakan polimorfisme. Untuk parser seperti pohon, ini jelas merupakan pendekatan yang salah, tetapi jika Anda memiliki sesuatu seperti perakitan, di mana setiap baris agak independen, dan mulai dengan opcode yang menunjukkan bagaimana sisa garis harus ditafsirkan, saya benar-benar akan menggunakan polimorfisme dan sebuah Pabrik. Setiap kelas dapat mengimplementasikan fungsi seperti
ExtractConstants
atauExtractSymbols
. Saya telah menggunakan pendekatan ini untuk mainan BASIC interpreter.sumber
"Kita harus melupakan efisiensi kecil, katakanlah sekitar 97% dari waktu: optimasi prematur adalah akar dari semua kejahatan"
Donald Knuth
sumber
Bahkan jika ini tidak buruk untuk pemeliharaan, saya tidak percaya itu akan lebih baik untuk kinerja. Panggilan fungsi virtual hanyalah satu tipuan ekstra (sama dengan kasus terbaik untuk pernyataan switch) sehingga bahkan di C ++ kinerjanya harus kira-kira sama. Di C #, di mana semua panggilan fungsi adalah virtual, pernyataan switch harus lebih buruk, karena Anda memiliki overhead panggilan fungsi virtual yang sama di kedua versi.
sumber
Rekan Anda tidak berbicara di belakangnya, sejauh komentar mengenai tabel lompat pergi. Namun, menggunakannya untuk membenarkan penulisan kode buruk adalah kesalahannya.
Compiler C # mengubah pernyataan switch dengan hanya beberapa case menjadi serangkaian if / else, jadi tidak lebih cepat daripada menggunakan if / else. Kompiler mengonversi pernyataan beralih yang lebih besar menjadi Kamus (tabel lompatan yang dimaksud rekan Anda). Silakan lihat jawaban ini untuk pertanyaan Stack Overflow pada topik untuk lebih jelasnya .
Pernyataan peralihan besar sulit dibaca dan dikelola. Kamus "kasing" dan fungsi jauh lebih mudah dibaca. Karena saklar itu berubah, Anda dan kolega Anda disarankan untuk menggunakan kamus secara langsung.
sumber
Dia belum tentu berbicara keluar dari pantatnya. Setidaknya dalam
switch
pernyataan C dan C ++ dapat dioptimalkan untuk melompat tabel sementara saya belum pernah melihatnya terjadi dengan pengiriman dinamis dalam fungsi yang hanya memiliki akses ke basis pointer. Paling tidak yang terakhir membutuhkan pengoptimal yang jauh lebih cerdas melihat lebih banyak kode di sekitarnya untuk mencari tahu apa subtipe yang digunakan dari panggilan fungsi virtual melalui pointer basis / referensi.Selain itu, pengiriman dinamis sering berfungsi sebagai "penghalang optimisasi", yang berarti kompiler sering tidak akan dapat menyejajarkan kode dan secara optimal mengalokasikan register untuk meminimalkan tumpahan tumpahan dan semua hal-hal mewah, karena tidak dapat mengetahui apa fungsi virtual akan dipanggil melalui basis pointer untuk memasukkannya dan melakukan semua optimasi optimisasinya. Saya tidak yakin Anda bahkan ingin pengoptimal menjadi sangat pintar dan mencoba untuk mengoptimalkan panggilan fungsi tidak langsung, karena itu berpotensi menyebabkan banyak cabang kode harus dihasilkan secara terpisah di tumpukan panggilan yang diberikan (fungsi yang panggilan
foo->f()
akan untuk menghasilkan kode mesin yang sama sekali berbeda dari kode yang memanggilbar->f()
melalui basis pointer, dan fungsi yang memanggil fungsi itu kemudian harus menghasilkan dua atau lebih versi kode, dan sebagainya - jumlah kode mesin yang dihasilkan akan meledak - mungkin tidak terlalu buruk dengan jejak JIT yang menghasilkan kode dengan cepat saat menelusuri melalui jalur eksekusi panas).Namun, karena banyak jawaban telah bergema, itu adalah alasan yang buruk untuk mendukung banyak
switch
pernyataan bahkan jika itu lebih cepat dijatuhkan dengan jumlah marjinal. Selain itu, ketika datang ke efisiensi mikro, hal-hal seperti percabangan dan inlining biasanya prioritas cukup rendah dibandingkan dengan hal-hal seperti pola akses memori.Yang mengatakan, saya melompat di sini dengan jawaban yang tidak biasa. Saya ingin membuat kasus untuk pemeliharaan
switch
pernyataan atas solusi polimorfik ketika, dan hanya ketika, Anda tahu pasti bahwa hanya akan ada satu tempat yang perlu melakukanswitch
.Contoh utama adalah pengendali acara pusat. Dalam hal ini, Anda biasanya tidak memiliki banyak tempat yang menangani acara, hanya satu (mengapa "pusat"). Untuk kasus-kasus tersebut, Anda tidak mendapatkan keuntungan dari perpanjangan yang disediakan oleh solusi polimorfik. Solusi polimorfik bermanfaat ketika ada banyak tempat yang akan melakukan
switch
pernyataan analogis . Jika Anda tahu pasti hanya akan ada satu,switch
pernyataan dengan 15 kasus bisa jauh lebih sederhana daripada merancang kelas dasar yang diwarisi oleh 15 subtipe dengan fungsi yang ditimpa dan pabrik untuk membuat instantiate, hanya untuk kemudian digunakan dalam satu fungsi di seluruh sistem. Dalam kasus tersebut, menambahkan subtipe baru jauh lebih membosankan daripada menambahkancase
pernyataan ke satu fungsi. Jika ada, saya berpendapat untuk rawatan, bukan kinerja,switch
pernyataan dalam satu kasus khusus di mana Anda tidak mendapat manfaat dari ekstensibilitas apa pun.sumber