Ini adalah situasi yang sering saya temui sebagai programmer yang tidak berpengalaman dan saya bertanya-tanya terutama untuk proyek ambisius dan cepat yang saya coba optimalkan. Untuk bahasa C-like mayor (C, objC, C ++, Java, C #, dll) dan compiler biasanya, akankah kedua fungsi ini berjalan dengan efisien? Apakah ada perbedaan dalam kode yang dikompilasi?
void foo1(bool flag)
{
if (flag)
{
//Do stuff
return;
}
//Do different stuff
}
void foo2(bool flag)
{
if (flag)
{
//Do stuff
}
else
{
//Do different stuff
}
}
Pada dasarnya, ada yang pernah efisiensi langsung bonus / penalti ketika break
ing atau return
ing awal? Bagaimana stackframe terlibat? Apakah ada kasus khusus yang dioptimalkan? Apakah ada faktor (seperti sebaris atau ukuran "Lakukan sesuatu") yang dapat memengaruhi hal ini secara signifikan?
Saya selalu mendukung peningkatan keterbacaan atas pengoptimalan kecil (saya sering melihat banyak hal dengan validasi parameter), tetapi hal ini sering muncul sehingga saya ingin mengesampingkan semua kekhawatiran sekali dan untuk selamanya.
Dan saya menyadari jebakan pengoptimalan prematur ... ugh, itu adalah kenangan yang menyakitkan.
EDIT: Saya menerima jawaban, tetapi jawaban EJP menjelaskan dengan cukup singkat mengapa penggunaan a return
secara praktis dapat diabaikan (dalam perakitan, return
membuat 'cabang' ke akhir fungsi, yang sangat cepat. Cabang mengubah register PC dan juga dapat mempengaruhi cache dan pipeline, yang sangat kecil.) Khusus untuk kasus ini, tidak ada bedanya karena the if/else
dan the return
membuat cabang yang sama di akhir fungsi.
Jawaban:
Tidak ada perbedaan sama sekali:
Artinya tidak ada perbedaan dalam kode yang dihasilkan bahkan tanpa optimasi pada dua kompiler
sumber
something()
akan selalu dieksekusi. Dalam pertanyaan awal, OP memilikiDo stuff
danDo diffferent stuff
bergantung pada benderanya. Saya tidak yakin bahwa kode yang dihasilkan akan sama.Jawaban singkatnya adalah, tidak ada perbedaan. Bantulah diri Anda sendiri dan berhentilah mengkhawatirkan hal ini. Kompiler pengoptimal hampir selalu lebih pintar dari Anda.
Berkonsentrasi pada keterbacaan dan pemeliharaan.
Jika Anda ingin melihat apa yang terjadi, buat ini dengan optimisasi dan lihat keluaran assembler.
sumber
x = <some number>
daripadaif(<would've changed>) x = <some number>
cabang yang tidak dibutuhkan bisa sangat menyakitkan. Di sisi lain, kecuali ini berada di dalam loop utama dari operasi yang sangat intensif, saya juga tidak akan mengkhawatirkannya.Jawaban yang menarik: Meskipun saya setuju dengan semuanya (sejauh ini), ada kemungkinan konotasi untuk pertanyaan ini yang sampai sekarang benar-benar diabaikan.
Jika contoh sederhana di atas diperluas dengan alokasi sumber daya, dan kemudian pengecekan kesalahan dengan potensi pembebasan sumber daya, gambar mungkin berubah.
Pertimbangkan pendekatan naif yang mungkin diambil pemula:
Di atas akan mewakili versi ekstrim dari gaya kembali sebelum waktunya. Perhatikan bagaimana kode menjadi sangat berulang dan tidak dapat dipelihara seiring waktu ketika kompleksitasnya bertambah. Saat ini orang mungkin menggunakan penanganan pengecualian untuk menangkap ini.
Philip menyarankan, setelah melihat contoh goto di bawah ini, untuk menggunakan sakelar / casing tanpa pemutus di dalam blok penangkap di atas. Seseorang dapat beralih (typeof (e)) dan kemudian jatuh melalui
free_resourcex()
panggilan tetapi ini tidak sepele dan perlu pertimbangan desain . Dan ingat bahwa sakelar / casing tanpa jeda persis seperti goto dengan label rantai daisy di bawah ini ...Seperti yang ditunjukkan Mark B, di C ++, gaya yang baik untuk mengikuti Akuisisi Sumber Daya adalah prinsip Inisialisasi , RAII singkatnya . Inti dari konsep ini adalah menggunakan instansiasi objek untuk memperoleh sumber daya. Sumber daya kemudian secara otomatis dibebaskan segera setelah objek keluar dari ruang lingkup dan penghancurnya dipanggil. Untuk sumber daya yang saling bergantung, perhatian khusus harus dilakukan untuk memastikan urutan deallokasi yang benar dan untuk merancang jenis objek sedemikian rupa sehingga data yang diperlukan tersedia untuk semua destruktor.
Atau di hari-hari pra-pengecualian mungkin bisa:
Tetapi contoh yang terlalu disederhanakan ini memiliki beberapa kelemahan: Dapat digunakan hanya jika sumber daya yang dialokasikan tidak bergantung satu sama lain (misalnya tidak dapat digunakan untuk mengalokasikan memori, kemudian membuka filehandle, lalu membaca data dari pegangan ke dalam memori ), dan tidak memberikan kode kesalahan individual yang dapat dibedakan sebagai nilai pengembalian.
Untuk menjaga kode tetap cepat (!), Kompak, dan mudah dibaca dan diperluas, Linus Torvalds menerapkan gaya berbeda untuk kode kernel yang berhubungan dengan sumber daya, bahkan menggunakan goto yang terkenal dengan cara yang benar-benar masuk akal :
Inti dari diskusi di milis kernel adalah bahwa sebagian besar fitur bahasa yang "lebih disukai" daripada pernyataan goto adalah goto implisit, seperti if / else besar, penangan pengecualian, pernyataan loop / break / lanjutkan, dll. . Dan goto pada contoh di atas dianggap baik, karena mereka hanya melompat dalam jarak kecil, memiliki label yang jelas, dan membebaskan kode dari kekacauan lain untuk melacak kondisi kesalahan. Pertanyaan ini juga telah dibahas di sini di stackoverflow .
Namun apa yang hilang dalam contoh terakhir adalah cara yang bagus untuk mengembalikan kode kesalahan. Saya berpikir untuk menambahkan
result_code++
setelah setiapfree_resource_x()
panggilan, dan mengembalikan kode itu, tetapi ini mengimbangi beberapa peningkatan kecepatan dari gaya pengkodean di atas. Dan sulit untuk mengembalikan 0 jika berhasil. Mungkin saya hanya tidak imajinatif ;-)Jadi, ya, menurut saya ada perbedaan besar dalam pertanyaan tentang pengkodean pengembalian dini atau tidak. Tetapi saya juga berpikir itu hanya terlihat dalam kode yang lebih rumit yang lebih sulit atau tidak mungkin untuk direstrukturisasi dan dioptimalkan untuk kompiler. Yang biasanya terjadi setelah alokasi sumber daya mulai berlaku.
sumber
catch
berisi pernyataan break-lessswitch
pada kode kesalahan?Meskipun ini bukan jawaban yang bagus, kompiler produksi akan jauh lebih baik dalam pengoptimalannya daripada Anda. Saya lebih menyukai keterbacaan dan pemeliharaan atas jenis pengoptimalan ini.
sumber
Untuk lebih spesifik tentang ini,
return
akan dikompilasi menjadi cabang ke akhir metode, di mana akan adaRET
instruksi atau apapun itu. Jika Anda membiarkannya keluar, ujung blok sebelumnyaelse
akan dikompilasi menjadi cabang hingga akhirelse
blok. Jadi Anda dapat melihat dalam kasus khusus ini tidak ada bedanya sama sekali.sumber
Jika Anda benar-benar ingin tahu apakah ada perbedaan dalam kode yang dikompilasi untuk kompilator dan sistem Anda, Anda harus mengkompilasi dan melihat sendiri rakitannya.
Namun dalam skema besar, hampir pasti bahwa compiler dapat mengoptimalkan lebih baik daripada fine tuning Anda, dan bahkan jika tidak bisa, sangat kecil kemungkinannya untuk benar-benar penting bagi kinerja program Anda.
Sebaliknya, tulis kode dengan cara yang paling jelas untuk dibaca dan dipelihara oleh manusia, dan biarkan kompilator melakukan yang terbaik: Hasilkan perakitan terbaik yang dapat dilakukan dari sumber Anda.
sumber
Dalam contoh Anda, pengembaliannya terlihat. Apa yang terjadi pada orang yang men-debug ketika kembaliannya adalah satu atau dua halaman di atas / di bawah tempat // terjadi hal-hal yang berbeda? Jauh lebih sulit untuk ditemukan / dilihat ketika ada lebih banyak kode.
sumber
Saya sangat setuju dengan blueshift: keterbacaan dan pemeliharaan terlebih dahulu !. Tetapi jika Anda benar-benar khawatir (atau hanya ingin mempelajari apa yang dilakukan kompiler Anda, yang tentunya merupakan ide yang bagus dalam jangka panjang), Anda harus mencarinya sendiri.
Ini berarti menggunakan decompiler atau melihat output compiler level rendah (misalnya bahasa assembly). Dalam C #, atau bahasa .Net apa pun, alat didokumentasikan di sini akan memberi Anda apa yang Anda butuhkan.
Tetapi seperti yang telah Anda amati sendiri, ini mungkin pengoptimalan yang prematur.
sumber
Dari Kode Bersih: Buku Pegangan Pengerjaan Perangkat Lunak Tangkas
dalam kode hanya akan membuat pembaca menavigasi ke fungsi dan membuang waktu membaca foo (bendera boolean)
Basis kode terstruktur yang lebih baik akan memberi Anda kesempatan yang lebih baik untuk mengoptimalkan kode.
sumber
Satu aliran pemikiran (tidak dapat mengingat orang pintar yang mengusulkannya saat ini) adalah bahwa semua fungsi hanya boleh memiliki satu titik balik dari sudut pandang struktural untuk membuat kode lebih mudah dibaca dan di-debug. Itu, saya kira, lebih untuk memprogram debat agama.
Salah satu alasan teknis Anda mungkin ingin mengontrol kapan dan bagaimana fungsi keluar yang melanggar aturan ini adalah saat Anda mengkodekan aplikasi waktu nyata dan Anda ingin memastikan bahwa semua jalur kontrol melalui fungsi tersebut mengambil jumlah siklus jam yang sama untuk diselesaikan.
sumber
Saya senang Anda mengajukan pertanyaan ini. Anda harus selalu menggunakan cabang untuk pengembalian awal. Kenapa berhenti disana? Gabungkan semua fungsi Anda menjadi satu jika Anda bisa (setidaknya sebanyak yang Anda bisa). Ini bisa dilakukan jika tidak ada rekursi. Pada akhirnya, Anda akan memiliki satu fungsi utama yang sangat besar, tetapi itulah yang Anda butuhkan / inginkan untuk hal semacam ini. Setelah itu, ganti nama pengenal Anda menjadi sesingkat mungkin. Dengan begitu, saat kode Anda dieksekusi, lebih sedikit waktu yang dihabiskan untuk membaca nama. Selanjutnya lakukan ...
sumber