Saya mendapat ide ini dari pertanyaan ini di stackoverflow.com
Pola berikut umum:
final x = 10;//whatever constant value
for(int i = 0; i < Math.floor(Math.sqrt(x)) + 1; i++) {
//...do something
}
Poin yang saya coba sampaikan adalah pernyataan kondisional adalah sesuatu yang rumit dan tidak berubah.
Apakah lebih baik untuk mendeklarasikannya di bagian inisialisasi loop, seperti itu?
final x = 10;//whatever constant value
for(int i = 0, j = Math.floor(Math.sqrt(x)) + 1; i < j; i++) {
//...do something
}
Apakah ini lebih jelas?
Bagaimana jika ekspresi kondisional sederhana seperti
final x = 10;//whatever constant value
for(int i = 0, j = n*n; i > j; j++) {
//...do something
}
java
c++
coding-style
language-agnostic
Celeritas
sumber
sumber
x
besarnya besar,Math.floor(Math.sqrt(x))+1
sama denganMath.floor(Math.sqrt(x))
. :-){ x=whatever; for (...) {...} }
atau, lebih baik lagi, pertimbangkan apakah ada cukup banyak hal yang perlu dilakukan sebagai fungsi terpisah.Jawaban:
Apa yang akan saya lakukan adalah seperti ini:
Jujur satu-satunya alasan yang baik untuk menjejalkan inisialisasi
j
(sekaranglimit
) ke header loop adalah untuk menjaga cakupannya benar. Yang diperlukan untuk membuat non masalah adalah cakupan tertutup yang bagus dan bagus.Saya dapat menghargai keinginan untuk cepat tetapi tidak mengorbankan keterbacaan tanpa alasan yang kuat.
Tentu, kompiler dapat mengoptimalkan, menginisialisasi beberapa vars mungkin legal, tetapi loop cukup sulit untuk di-debug. Mohon berbaik hati pada manusia. Jika ini benar-benar ternyata memperlambat kami, ada baiknya memahaminya cukup untuk memperbaikinya.
sumber
for(int i = 0; i < n*n; i++){...}
Anda tidak akan menetapkann*n
variabel, bukan?final
). Siapa yang peduli jika konstanta yang memiliki kompiler penegakan mencegahnya berubah dapat diakses nanti dalam fungsi?Kompiler yang baik akan menghasilkan kode yang sama, jadi jika Anda ingin kinerja, buat saja perubahan jika berada dalam loop kritis dan Anda benar-benar membuat profil dan menemukannya membuat perbedaan. Bahkan jika kompilator tidak dapat mengoptimalkannya, seperti yang ditunjukkan orang di komentar tentang kasus dengan pemanggilan fungsi, dalam sebagian besar situasi, perbedaan kinerja akan terlalu kecil untuk menjadi pertimbangan programmer.
Namun...
Kita tidak boleh lupa bahwa kode terutama merupakan media komunikasi antara manusia, dan kedua pilihan Anda tidak berkomunikasi dengan manusia lain dengan sangat baik. Yang pertama memberi kesan bahwa ekspresi perlu dihitung pada setiap iterasi, dan yang kedua di bagian inisialisasi menyiratkan itu akan diperbarui di suatu tempat di dalam loop, di mana itu benar-benar konstan di seluruh.
Saya benar-benar lebih suka itu ditarik keluar di atas loop dan dibuat
final
untuk membuatnya segera dan jelas bagi siapa pun yang membaca kode. Itu juga tidak ideal karena meningkatkan cakupan variabel, tetapi fungsi penutup Anda seharusnya tidak mengandung lebih dari itu.sumber
Seperti @Karl Bielefeldt katakan dalam jawabannya, ini biasanya bukan masalah.
Namun, itu pada suatu waktu merupakan masalah umum dalam C dan C ++, dan sebuah trik muncul untuk mengatasi masalah tanpa mengurangi keterbacaan kode - beralih mundur, turun ke
0
.Sekarang syarat dalam setiap iterasi adalah
>= 0
setiap kompiler akan mengkompilasi menjadi 1 atau 2 instruksi perakitan. Setiap CPU yang dibuat dalam beberapa dekade terakhir harus memiliki pemeriksaan dasar seperti ini; melakukan pemeriksaan cepat pada mesin x64 saya, saya melihat ini dapat diprediksi berubah menjadicmpl $0x0, -0x14(%rbp)
(nilai int-bandingkan-panjang 0 vs register rbp yang tidak terjual -14) danjl 0x100000f59
(lompat ke instruksi mengikuti loop jika perbandingan sebelumnya benar untuk "2nd-arg <1st-arg ”) .Perhatikan bahwa saya menghapus
+ 1
dariMath.floor(Math.sqrt(x)) + 1
; agar matematika bekerja, nilai awal seharusnyaint i = «iterationCount» - 1
. Juga patut dicatat adalah bahwa iterator Anda harus ditandatangani;unsigned int
tidak akan berfungsi dan cenderung akan memperingatkan kompiler.Setelah pemrograman dalam bahasa berbasis C selama ~ 20 tahun sekarang saya hanya menulis loop indeks-terbalik-iterating kecuali ada alasan khusus untuk meneruskan-indeks-iterate. Selain pemeriksaan yang lebih sederhana dalam kondisi, iterasi terbalik sering kali juga langkah-langkah apa yang dinyatakan akan menjadi array-mutasi-sembari-iterating.
sumber
unsigned
penghitung akan berfungsi di sini jika Anda memodifikasi cek (cara termudah adalah menambahkan nilai yang sama ke kedua sisi); misalnya, untuk setiap penguranganDec
, cek(i + Dec) >= Dec
harus selalu memiliki hasil yang sama dengansigned
ceki >= 0
, dengan keduanyasigned
danunsigned
penghitung, selama bahasa tersebut memiliki aturan sampul yang jelas untukunsigned
variabel (khususnya,-n + n == 0
harus benar untuk keduanyasigned
danunsigned
). Perhatikan, bagaimanapun, bahwa ini mungkin kurang efisien daripada>=0
cek yang ditandatangani , jika kompiler tidak memiliki optimasi untuk itu.Dec
konstanta untuk nilai awal dan nilai akhir berfungsi tetapi membuatnya kurang intuitif, dan jika menggunakani
sebagai indeks array maka Anda juga perlu melakukanunsigned int arrayI = i - Dec;
dalam tubuh loop. Saya hanya menggunakan forward-iteration ketika terjebak dengan iterator yang tidak ditandatangani; sering dengani <= count - 1
syarat untuk menjaga logika tetap sejajar dengan loop iterasi terbalik.Dec
nilai awal dan akhir secara khusus, tetapi mengubah kondisi pemeriksaanDec
di kedua sisi.for (unsigned i = N - 1; i + 1 >= 1; i--) /*...*/
Ini memungkinkan Anda untuk menggunakan secarai
normal di dalam loop, sambil menjamin bahwa nilai serendah mungkin di sisi kiri kondisi adalah0
(untuk mencegah sampul dari mengganggu). Ini jelas jauh lebih mudah untuk menggunakan iterasi maju ketika bekerja dengan penghitung yang tidak ditandatangani.Menjadi menarik setelah Math.sqrt (x) digantikan oleh Mymodule.SomeNonPureMethodWithSideEffects (x).
Pada dasarnya modus operandi saya adalah: jika sesuatu diharapkan selalu memberikan nilai yang sama, maka hanya mengevaluasinya sekali. Misalnya List.Count, jika daftar seharusnya tidak berubah selama operasi loop, maka dapatkan hitungan di luar loop ke variabel lain.
Beberapa "penghitungan" ini bisa sangat mahal terutama ketika Anda berurusan dengan database. Bahkan jika Anda bekerja pada dataset yang tidak seharusnya berubah selama iterasi daftar.
sumber
for( auto it = begin(dataset); !at_end(it); ++it )
Menurut saya ini sangat spesifik bahasa. Sebagai contoh, jika menggunakan C ++ 11 saya akan curiga bahwa jika pemeriksaan kondisi adalah
constexpr
fungsi, kompiler akan sangat mungkin untuk mengoptimalkan beberapa eksekusi karena tahu itu akan menghasilkan nilai yang sama setiap kali.Namun, jika pemanggilan fungsi adalah fungsi pustaka yang bukan
constexpr
kompiler, hampir pasti akan mengeksekusi pada setiap iterasi karena tidak dapat menyimpulkan ini (kecuali itu inline dan karenanya dapat disimpulkan sebagai murni).Saya kurang tahu tentang Java tetapi mengingat bahwa ini adalah JIT yang dikompilasi, saya kira kompiler memiliki informasi yang cukup saat runtime untuk kemungkinan inline dan mengoptimalkan kondisi. Tapi ini akan bergantung pada desain kompiler yang baik dan kompiler yang memutuskan loop ini adalah prioritas optimasi yang hanya bisa kita tebak.
Secara pribadi saya merasa sedikit lebih elegan untuk menempatkan kondisi di dalam for for loop jika Anda bisa, tetapi jika rumit saya akan menuliskannya ke dalam
constexpr
atauinline
fungsi, atau bahasa Anda yang setara untuk mengisyaratkan bahwa fungsi tersebut murni dan dapat dioptimalkan. Ini membuat maksudnya jelas dan mempertahankan gaya loop idiomatik tanpa membuat garis besar yang tidak dapat dibaca. Ini juga memberi kondisi memeriksa nama jika fungsinya sendiri sehingga pembaca dapat segera melihat secara logis untuk apa cek itu tanpa membacanya jika rumit.sumber