Setiap kali saya membutuhkan pembagian, misalnya, pengecekan kondisi, saya ingin mengubah ekspresi pembagian menjadi penggandaan, misalnya:
Versi asli:
if(newValue / oldValue >= SOME_CONSTANT)
Versi baru:
if(newValue >= oldValue * SOME_CONSTANT)
Karena saya pikir itu dapat menghindari:
Pembagian dengan nol
Meluap ketika
oldValue
sangat kecil
Apakah itu benar? Apakah ada masalah dengan kebiasaan ini?
coding-style
language-agnostic
math
ocomfd
sumber
sumber
oldValue >= 0
?Jawaban:
Dua kasus umum untuk dipertimbangkan:
Aritmatika integer
Tentunya jika Anda menggunakan bilangan bulat aritmatika (yang memotong) Anda akan mendapatkan hasil yang berbeda. Berikut ini contoh kecil dalam C #:
Keluaran:
Aritmatika titik mengambang
Selain dari fakta bahwa pembagian dapat menghasilkan hasil yang berbeda ketika membagi dengan nol (itu menghasilkan pengecualian, sedangkan perkalian tidak), itu juga dapat mengakibatkan kesalahan pembulatan yang sedikit berbeda dan hasil yang berbeda. Contoh sederhana dalam C #:
Keluaran:
Jika Anda tidak percaya kepada saya, ini adalah Fiddle yang bisa Anda eksekusi dan lihat sendiri.
Bahasa lain mungkin berbeda; ingatlah, bagaimanapun, bahwa C #, seperti banyak bahasa, mengimplementasikan pustaka titik mengambang standar IEEE (IEEE 754) , jadi Anda harus mendapatkan hasil yang sama di waktu run standar lainnya.
Kesimpulan
Jika Anda bekerja greenfield , Anda mungkin baik-baik saja.
Jika Anda bekerja pada kode lawas, dan aplikasi tersebut adalah aplikasi keuangan atau sensitif lainnya yang melakukan aritmatika dan diharuskan untuk memberikan hasil yang konsisten, sangat berhati-hati ketika mengubah sekitar operasi. Jika Anda harus, pastikan bahwa Anda memiliki unit test yang akan mendeteksi setiap perubahan aritmatika.
Jika Anda hanya melakukan hal-hal seperti menghitung elemen dalam array atau fungsi komputasi umum lainnya, Anda mungkin akan baik-baik saja. Saya tidak yakin metode multiplikasi membuat kode Anda lebih jelas.
Jika Anda menerapkan algoritme ke spesifikasi, saya tidak akan mengubah apa pun, tidak hanya karena masalah kesalahan pembulatan, tetapi agar pengembang dapat meninjau kode dan memetakan setiap ekspresi kembali ke spesifikasi untuk memastikan tidak ada implementasi kekurangan.
sumber
Saya suka pertanyaan Anda karena berpotensi mencakup banyak ide. Secara keseluruhan, saya menduga jawabannya tergantung , mungkin pada jenis yang terlibat dan kisaran nilai yang mungkin dalam kasus spesifik Anda.
Naluri awal saya adalah merenungkan gaya , yaitu. versi baru Anda kurang jelas bagi pembaca kode Anda. Saya membayangkan saya harus berpikir selama satu atau dua detik (atau mungkin lebih lama) untuk menentukan niat versi baru Anda, sedangkan versi lama Anda segera jelas. Keterbacaan adalah atribut penting dari kode, sehingga ada biaya dalam versi baru Anda.
Anda benar bahwa versi baru menghindari pembagian dengan nol. Tentu saja Anda tidak perlu menambahkan penjaga (di sepanjang baris
if (oldValue != 0)
). Tetapi apakah ini masuk akal? Versi lama Anda mencerminkan rasio antara dua angka. Jika pembagi adalah nol, maka rasio Anda tidak ditentukan. Ini mungkin lebih bermakna dalam situasi Anda, yaitu. Anda seharusnya tidak membuahkan hasil dalam kasus ini.Perlindungan terhadap luapan masih bisa diperdebatkan. Jika Anda tahu itu
newValue
selalu lebih besar darioldValue
, maka mungkin Anda bisa membuat argumen itu. Namun mungkin ada kasus di mana(oldValue * SOME_CONSTANT)
juga akan meluap. Jadi saya tidak melihat banyak keuntungan di sini.Mungkin ada argumen bahwa Anda mendapatkan kinerja yang lebih baik karena perkalian bisa lebih cepat daripada pembagian (pada beberapa prosesor). Namun, harus ada banyak perhitungan seperti ini untuk mendapatkan hasil yang signifikan, yaitu. Waspadalah terhadap pengoptimalan prematur.
Berkaca pada semua hal di atas, secara umum saya tidak berpikir ada banyak yang bisa diperoleh dengan versi baru Anda dibandingkan dengan versi lama, khususnya mengingat pengurangan kejelasan. Namun mungkin ada kasus khusus di mana ada manfaatnya.
sumber
Tidak.
Saya mungkin akan menyebutnya optimasi prematur , dalam arti luas, terlepas dari apakah Anda mengoptimalkan kinerja , seperti frasa yang umumnya merujuk, atau hal lain yang dapat dioptimalkan, seperti jumlah tepi , baris kode , atau bahkan lebih luas lagi, hal-hal seperti "desain."
Menerapkan optimisasi semacam itu sebagai prosedur operasi standar membuat semantik kode Anda berisiko dan berpotensi menyembunyikan ujung-ujungnya. Kasus tepi yang Anda lihat cocok untuk diam-diam menghilangkan mungkin perlu secara eksplisit ditujukan pula . Dan, jauh lebih mudah untuk men-debug masalah di sekitar tepi yang bising (yang mengeluarkan pengecualian) dari yang gagal secara diam-diam.
Dan, dalam beberapa kasus, bahkan menguntungkan untuk "tidak mengoptimalkan" demi keterbacaan, kejelasan, atau kejelasan. Dalam kebanyakan kasus, pengguna Anda tidak akan melihat bahwa Anda telah menyimpan beberapa baris kode atau siklus CPU untuk menghindari penanganan kasus tepi atau penanganan pengecualian. Kode canggung atau gagal diam-diam, di sisi lain, akan mempengaruhi orang - rekan kerja Anda setidaknya. (Dan juga, oleh karena itu, biaya untuk membangun dan memelihara perangkat lunak.)
Default untuk apa pun yang lebih "alami" dan dapat dibaca sehubungan dengan domain aplikasi dan masalah spesifik. Tetap sederhana, eksplisit, dan idiomatik. Optimalkan seperlunya untuk keuntungan signifikan atau untuk mencapai ambang batas kegunaan yang sah.
Juga perhatikan: Kompiler sering mengoptimalkan pembagian untuk Anda - ketika aman untuk melakukannya.
sumber
Gunakan yang mana saja yang kurang buggy dan lebih masuk akal.
Biasanya , pembagian oleh variabel adalah ide yang buruk, karena biasanya, pembagi bisa nol.
Pembagian oleh konstanta biasanya hanya tergantung pada apa arti logisnya.
Berikut ini beberapa contoh untuk ditunjukkan tergantung pada situasinya:
Divisi bagus:
Buruk multiplikasi:
Multiplikasi yang baik:
Divisi buruk:
Multiplikasi yang baik:
Divisi buruk:
sumber
(ptr2 - ptr1) * 3 >= n
semudah memahami seperti ekspresiptr2 - ptr1 >= n / 3
? Itu tidak membuat otak Anda tersandung dan bangkit kembali mencoba menguraikan makna tiga kali lipat perbedaan antara dua petunjuk? Jika itu benar-benar jelas bagi Anda dan tim Anda, maka lebih banyak kekuatan untuk Anda, saya kira; Saya hanya harus menjadi minoritas yang lambat.n
dan angka arbitrer 3 membingungkan dalam kedua kasus tetapi, diganti dengan nama yang masuk akal, tidak, saya tidak menemukan salah satu yang lebih membingungkan daripada yang lain.Melakukan apa pun "jika memungkinkan" jarang merupakan ide yang bagus.
Prioritas nomor satu Anda harus benar, diikuti oleh keterbacaan dan pemeliharaan. Mengganti secara buta pembagian dengan perkalian jika memungkinkan akan sering gagal di departemen kebenaran, kadang-kadang hanya jarang dan karenanya sulit untuk menemukan kasus.
Lakukan apa yang benar dan paling mudah dibaca. Jika Anda memiliki bukti kuat bahwa menulis kode dengan cara yang paling mudah dibaca menyebabkan masalah kinerja, maka Anda dapat mempertimbangkan untuk mengubahnya. Peduli, matematika, dan ulasan kode adalah teman Anda.
sumber
Mengenai keterbacaan kode, saya pikir perkalian sebenarnya lebih mudah dibaca dalam beberapa kasus. Misalnya, jika ada sesuatu yang harus Anda periksa apakah
newValue
telah meningkat 5 persen atau lebih di atasoldValue
, maka1.05 * oldValue
merupakan ambang batas untuk diujinewValue
, dan wajar untuk menulisNamun waspadalah terhadap angka negatif ketika Anda melakukan refactor dengan cara ini (baik mengganti divisi dengan perkalian, atau mengganti perkalian dengan pembagian). Dua kondisi yang Anda anggap setara jika
oldValue
dijamin tidak negatif; tapi anggaplahnewValue
sebenarnya -13.5 danoldValue
-10.1. Kemudianmengevaluasi ke true , tetapi
mengevaluasi ke false .
sumber
Perhatikan Divisi kertas terkenal oleh Integer Invariant menggunakan Perkalian .
Compiler sebenarnya melakukan multiplikasi, jika integernya invarian! Bukan divisi. Ini terjadi bahkan untuk non power 2 nilai. Kekuatan 2 divisi jelas menggunakan pergeseran bit dan karenanya lebih cepat.
Namun, untuk bilangan bulat non-invarian, Anda bertanggung jawab untuk mengoptimalkan kode. Pastikan sebelum mengoptimalkan bahwa Anda benar-benar mengoptimalkan kemacetan asli, dan kebenaran itu tidak dikorbankan. Waspadalah terhadap integer overflow.
Saya peduli dengan optimasi mikro, jadi saya mungkin akan melihat kemungkinan optimasi.
Pikirkan juga tentang arsitektur tempat kode Anda berjalan. Terutama ARM memiliki divisi yang sangat lambat; Anda perlu memanggil fungsi untuk membagi, tidak ada instruksi pembagian di ARM.
Juga, pada arsitektur 32-bit, pembagian 64-bit tidak dioptimalkan, seperti yang saya ketahui .
sumber
Mengambil poin 2 Anda, itu memang akan mencegah meluap untuk yang sangat kecil
oldValue
. Namun jikaSOME_CONSTANT
juga sangat kecil maka metode alternatif Anda akan berakhir dengan underflow, di mana nilainya tidak dapat diwakili secara akurat.Dan sebaliknya, apa yang terjadi jika
oldValue
sangat besar? Anda memiliki masalah yang sama, hanya sebaliknya.Jika Anda ingin menghindari (atau meminimalkan) risiko overflow / underflow, cara terbaik adalah memeriksa apakah
newValue
jarak terdekatnya denganoldValue
atau keSOME_CONSTANT
. Anda kemudian dapat memilih operasi pembagian yang tepatatau
dan hasilnya akan paling akurat.
Untuk membagi-oleh-nol, dalam pengalaman saya ini hampir tidak pernah pantas untuk "diselesaikan" dalam matematika. Jika Anda memiliki pembagian dengan nol di pemeriksaan berkelanjutan Anda, maka hampir pasti Anda memiliki situasi yang memerlukan beberapa analisis dan perhitungan apa pun berdasarkan data ini tidak ada artinya. Pemeriksaan divide-by-zero yang eksplisit hampir selalu merupakan langkah yang tepat. (Perhatikan bahwa saya mengatakan "hampir" di sini, karena saya tidak mengklaim sebagai sempurna. Saya hanya mencatat bahwa saya tidak ingat melihat alasan yang bagus untuk ini dalam 20 tahun menulis perangkat lunak yang disematkan, dan melanjutkan .)
Namun jika Anda memiliki risiko nyata kelebihan / kekurangan dalam aplikasi Anda maka ini mungkin bukan solusi yang tepat. Kemungkinan besar, Anda umumnya harus memeriksa stabilitas numerik algoritma Anda, atau mungkin hanya pindah ke representasi presisi yang lebih tinggi.
Dan jika Anda tidak memiliki risiko overflow / underflow terbukti, maka Anda tidak khawatir tentang apa pun. Itu berarti Anda benar - benar perlu membuktikan bahwa Anda membutuhkannya, dengan angka, dalam komentar di sebelah kode yang menjelaskan kepada pengelola mengapa itu perlu. Sebagai seorang insinyur kepala sekolah yang meninjau kode orang lain, jika saya bertemu seseorang yang berusaha lebih keras untuk hal ini, saya pribadi tidak akan menerima apa pun yang kurang. Ini adalah kebalikan dari optimasi prematur, tetapi umumnya akan memiliki akar penyebab yang sama - obsesi dengan detail yang tidak membuat perbedaan fungsional.
sumber
Meringkas aritmatika bersyarat dalam metode dan properti yang bermakna. Tidak hanya akan penamaan yang baik memberitahu Anda apa "A / B" berarti , parameter memeriksa & penanganan kesalahan rapi dapat bersembunyi di sana juga.
Yang penting, karena metode-metode ini disusun menjadi logika yang lebih kompleks, kompleksitas ekstrinsiknya tetap sangat mudah dikelola.
Saya akan mengatakan penggantian multiplikasi sepertinya solusi yang masuk akal karena masalahnya tidak jelas.
sumber
Saya pikir itu bukan ide yang baik untuk mengganti perkalian dengan divisi karena CPU ALU (Arithmetic-Logic Unit) mengeksekusi algoritma, meskipun mereka diimplementasikan dalam perangkat keras. Teknik yang lebih canggih tersedia di prosesor yang lebih baru. Secara umum, prosesor berusaha untuk memparalelkan operasi pasangan-bit agar meminimalkan siklus jam yang diperlukan. Algoritma multiplikasi dapat diparalelkan dengan cukup efektif (walaupun lebih banyak transistor diperlukan). Algoritma pembagian tidak dapat diparalelkan secara efisien. Algoritma pembagian yang paling efisien cukup kompleks. Umumnya, mereka memerlukan lebih banyak siklus jam per bit.
sumber