Haruskah pernyataan bersyarat non-sepele dipindahkan ke bagian inisialisasi loop?

21

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
}
Celeritas
sumber
47
Mengapa tidak memindahkannya saja ke baris sebelum loop, maka Anda juga bisa memberinya nama yang masuk akal.
jonrsharpe
2
@Mehrdad: Mereka tidak setara. Jika xbesarnya besar, Math.floor(Math.sqrt(x))+1sama dengan Math.floor(Math.sqrt(x)). :-)
R ..
5
@jonrsharpe Karena itu akan memperluas cakupan variabel. Saya tidak perlu mengatakan bahwa itu adalah alasan yang baik untuk tidak melakukannya, tetapi itulah alasan beberapa orang tidak melakukannya.
Kevin Krumwiede
3
@KevinKrumwiede Jika ruang lingkup perhatian adalah batasnya dengan meletakkan kode di bloknya sendiri, misalnya, { x=whatever; for (...) {...} }atau, lebih baik lagi, pertimbangkan apakah ada cukup banyak hal yang perlu dilakukan sebagai fungsi terpisah.
Blrfl
2
@jonrsharpe Anda dapat memberikan nama yang masuk akal saat mendeklarasikannya di bagian init juga. Tidak mengatakan saya akan meletakkannya di sana; masih lebih mudah dibaca jika terpisah.
JollyJoker

Jawaban:

62

Apa yang akan saya lakukan adalah seperti ini:

void doSomeThings() {
    final x = 10;//whatever constant value
    final limit = Math.floor(Math.sqrt(x)) + 1;
    for(int i = 0; i < limit; i++) {
         //...do something
    }
}

Jujur satu-satunya alasan yang baik untuk menjejalkan inisialisasi j(sekarang limit) 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.

candied_orange
sumber
Poin bagus. Saya akan berasumsi untuk tidak repot-repot dengan variabel lain jika ekspresi adalah sesuatu yang sederhana, misalnya for(int i = 0; i < n*n; i++){...}Anda tidak akan menetapkan n*nvariabel, bukan?
Celeritas
1
Saya akan dan saya punya. Tetapi tidak untuk kecepatan. Agar mudah dibaca. Kode yang mudah dibaca cenderung cepat sendiri.
candied_orange
1
Bahkan masalah pelingkupan hilang jika Anda menandai sebagai konstan ( final). Siapa yang peduli jika konstanta yang memiliki kompiler penegakan mencegahnya berubah dapat diakses nanti dalam fungsi?
jpmc26
Saya pikir hal besar adalah apa yang Anda harapkan terjadi. Saya tahu contoh menggunakan sqrt tetapi bagaimana jika itu adalah fungsi lain? Apakah fungsinya murni? Apakah Anda selalu mengharapkan nilai yang sama? Apakah ada efek sampingnya? Apakah efek sampingnya adalah sesuatu yang Anda inginkan terjadi pada setiap iterasi?
Pieter B
38

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

Karl Bielefeldt
sumber
5
Setelah Anda mulai menyertakan pemanggilan fungsi, kompiler akan lebih sulit untuk dioptimalkan. Kompiler harus memiliki pengetahuan khusus bahwa Math.sqrt tidak memiliki efek samping.
Peter Green
@PeterGreen Jika ini Java, JVM dapat mengetahuinya, tetapi mungkin perlu waktu.
chrylis -pada mogok-
Pertanyaannya diberi tag C ++ dan Java. Saya tidak tahu seberapa maju Java JVM itu dan kapan ia bisa dan tidak bisa mengetahuinya, tapi saya tahu bahwa kompiler C ++ secara umum tidak bisa mengetahuinya fungsi belum murni kecuali jika ia memiliki anotasi non-standar yang memberitahu kompiler. jadi, atau badan fungsi terlihat dan semua fungsi yang secara tidak langsung dapat dideteksi sebagai murni dengan kriteria yang sama. Catatan: efek samping tidak cukup untuk memindahkannya keluar dari kondisi loop. Fungsi yang bergantung pada keadaan global tidak dapat dipindahkan dari loop baik jika badan loop dapat memodifikasi negara.
hvd
Menjadi menarik setelah Math.sqrt (x) digantikan oleh Mymodule.SomeNonPureMethodWithSideEffects (x).
Pieter B
9

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

final x = 10;//whatever constant value
for(int i = Math.floor(Math.sqrt(x)); i >= 0; i--) {
  //...do something
}

Sekarang syarat dalam setiap iterasi adalah >= 0setiap 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 menjadi cmpl $0x0, -0x14(%rbp)(nilai int-bandingkan-panjang 0 vs register rbp yang tidak terjual -14) dan jl 0x100000f59(lompat ke instruksi mengikuti loop jika perbandingan sebelumnya benar untuk "2nd-arg <1st-arg ”) .

Perhatikan bahwa saya menghapus + 1dari Math.floor(Math.sqrt(x)) + 1; agar matematika bekerja, nilai awal seharusnya int i = «iterationCount» - 1. Juga patut dicatat adalah bahwa iterator Anda harus ditandatangani; unsigned inttidak 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.

Slipp D. Thompson
sumber
1
Semua yang Anda tulis secara teknis benar. Komentar tentang instruksi mungkin menyesatkan, karena semua CPU modern juga telah dirancang untuk bekerja dengan baik dengan pengulangan yang berulang, terlepas dari apakah mereka memiliki instruksi khusus untuknya. Bagaimanapun, sebagian besar waktu biasanya dihabiskan di dalam loop, tidak melakukan iterasi.
Jørgen Fogh
4
Perlu dicatat bahwa optimisasi kompiler modern dirancang dengan kode "normal". Dalam kebanyakan kasus, kompiler akan menghasilkan kode yang sama cepatnya terlepas dari apakah Anda menggunakan optimasi "trik" atau tidak. Tetapi beberapa trik sebenarnya dapat menghambat optimasi tergantung pada seberapa rumitnya mereka. Jika menggunakan trik tertentu membuat kode Anda lebih mudah dibaca atau membantu menangkap bug umum, itu bagus, tetapi jangan menipu diri sendiri untuk berpikir "jika saya menulis kode dengan cara ini akan lebih cepat." Tulis kode seperti biasanya, lalu profil untuk menemukan tempat yang perlu Anda optimalkan.
0x5453
Perhatikan juga bahwa unsignedpenghitung akan berfungsi di sini jika Anda memodifikasi cek (cara termudah adalah menambahkan nilai yang sama ke kedua sisi); misalnya, untuk setiap pengurangan Dec, cek (i + Dec) >= Decharus selalu memiliki hasil yang sama dengan signedcek i >= 0, dengan keduanya signeddan unsignedpenghitung, selama bahasa tersebut memiliki aturan sampul yang jelas untuk unsignedvariabel (khususnya, -n + n == 0harus benar untuk keduanya signeddan unsigned). Perhatikan, bagaimanapun, bahwa ini mungkin kurang efisien daripada >=0cek yang ditandatangani , jika kompiler tidak memiliki optimasi untuk itu.
Justin Time - Reinstate Monica
1
@JustinTime Ya, persyaratan yang ditandatangani adalah bagian yang paling sulit; menambahkan Deckonstanta untuk nilai awal dan nilai akhir berfungsi tetapi membuatnya kurang intuitif, dan jika menggunakan isebagai indeks array maka Anda juga perlu melakukan unsigned int arrayI = i - Dec;dalam tubuh loop. Saya hanya menggunakan forward-iteration ketika terjebak dengan iterator yang tidak ditandatangani; sering dengan i <= count - 1syarat untuk menjaga logika tetap sejajar dengan loop iterasi terbalik.
Slipp D. Thompson
1
@ SlippD.Thompson Saya tidak bermaksud menambahkan Decnilai awal dan akhir secara khusus, tetapi mengubah kondisi pemeriksaan Decdi kedua sisi. for (unsigned i = N - 1; i + 1 >= 1; i--) /*...*/ Ini memungkinkan Anda untuk menggunakan secara inormal di dalam loop, sambil menjamin bahwa nilai serendah mungkin di sisi kiri kondisi adalah 0(untuk mencegah sampul dari mengganggu). Ini jelas jauh lebih mudah untuk menggunakan iterasi maju ketika bekerja dengan penghitung yang tidak ditandatangani.
Waktu Justin - Kembalikan Monica
3

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.

Pieter B
sumber
Saat menghitung mahal, Anda seharusnya tidak menggunakannya sama sekali. Sebaliknya Anda harus melakukan yang setarafor( auto it = begin(dataset); !at_end(it); ++it )
Ben Voigt
@ BenVoigt Menggunakan iterator jelas merupakan cara terbaik untuk operasi-operasi tersebut. Saya hanya menyebutkannya untuk mengilustrasikan poin saya tentang menggunakan metode non-murni dengan efek samping.
Pieter B
0

Menurut saya ini sangat spesifik bahasa. Sebagai contoh, jika menggunakan C ++ 11 saya akan curiga bahwa jika pemeriksaan kondisi adalah constexprfungsi, 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 constexprkompiler, 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 constexpratau inlinefungsi, 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.

Vality
sumber