Apakah praktik yang baik untuk mengganti pembagian dengan perkalian jika memungkinkan?

73

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:

  1. Pembagian dengan nol

  2. Meluap ketika oldValuesangat kecil

Apakah itu benar? Apakah ada masalah dengan kebiasaan ini?

ocomfd
sumber
41
Hati-hati dengan angka negatif, kedua versi memeriksa hal yang sama sekali berbeda. Apakah Anda yakin itu oldValue >= 0?
user2313067
37
Bergantung pada bahasanya (tetapi terutama dengan C), optimasi apa pun yang dapat Anda pikirkan, kompiler biasanya dapat melakukannya dengan lebih baik, -OR- , memiliki cukup akal untuk tidak melakukannya sama sekali.
Mark Benningfield
63
Tidak pernah "praktik yang baik" untuk selalu mengganti kode X dengan kode Y ketika X dan Y tidak secara semantik setara. Tetapi selalu merupakan ide yang baik untuk melihat X dan Y, mengaktifkan otak, memikirkan apa persyaratannya , dan kemudian membuat keputusan mana dari dua alternatif yang lebih tepat. Dan setelah itu, Anda juga harus berpikir tentang tes mana yang diperlukan untuk memverifikasi Anda mendapatkan perbedaan semantik yang benar.
Doc Brown
12
@ MarkBenningfield: Tidak apa-apa, kompiler tidak dapat mengoptimalkan pembagian jauh dengan nol. "Optimasi" yang Anda pikirkan adalah "optimisasi kecepatan". OP sedang memikirkan jenis optimasi lain - penghindaran bug.
Slebetman
25
Poin 2 adalah palsu. Versi asli bisa meluap untuk nilai-nilai kecil, tetapi versi baru bisa melimpah untuk nilai-nilai besar, jadi tidak ada yang lebih aman dalam kasus umum.
JacquesB

Jawaban:

74

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 #:

public static void TestIntegerArithmetic()
{
    int newValue = 101;
    int oldValue = 10;
    int SOME_CONSTANT = 10;

    if(newValue / oldValue > SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue > oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

Keluaran:

First comparison says it's not bigger.
Second comparison says it's bigger.

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 #:

public static void TestFloatingPoint()
{
    double newValue = 1;
    double oldValue = 3;
    double SOME_CONSTANT = 0.33333333333333335;

    if(newValue / oldValue >= SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue >= oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

Keluaran:

First comparison says it's not bigger.
Second comparison says it's bigger.

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.

John Wu
sumber
41
Kedua, sedikit finansial. Pergantian semacam ini meminta akuntan mengejar Anda dengan garpu rumput. Saya ingat 5.000 baris di mana saya harus berusaha lebih banyak untuk menjaga agar garpu rumput di teluk daripada dalam menemukan jawaban "benar" - yang sebenarnya umumnya sedikit salah. Tidak aktif, 0,01% tidak masalah, jawaban yang benar-benar konsisten wajib. Jadi saya terpaksa melakukan perhitungan dengan cara yang menyebabkan kesalahan pembulatan sistematis.
Loren Pechtel
8
Pikirkan untuk membeli permen 5 sen (tidak ada lagi.) Beli 20 buah, jawaban yang "benar" adalah tidak ada pajak karena tidak ada pajak untuk 20 pembelian satu potong.
Loren Pechtel
24
@LorenPechtel, itu karena sebagian besar sistem pajak menyertakan aturan (untuk alasan yang jelas) bahwa pajak dipungut per transaksi, dan pajak akan terjadi selisih tidak lebih kecil dari koin terkecil di dunia, dan jumlah fraksional dibulatkan sesuai keinginan pembayar pajak. Aturan-aturan itu "benar" karena sah dan konsisten. Para akuntan dengan garpu rumput mungkin tahu apa aturan sebenarnya dengan cara yang tidak akan dilakukan oleh programmer komputer (kecuali mereka juga akuntan berpengalaman). Kesalahan 0,01% kemungkinan akan menyebabkan kesalahan keseimbangan, dan merupakan pelanggaran hukum untuk memiliki kesalahan keseimbangan.
Steve
9
Karena saya belum pernah mendengar istilah greenfield sebelumnya, saya mencarinya. Wikipedia mengatakan itu "sebuah proyek yang tidak memiliki kendala yang dipaksakan oleh pekerjaan sebelumnya".
Henrik Ripa
9
@Steve: Bos saya baru-baru ini membandingkan "greenfield" dengan "brownfield". Saya berkomentar bahwa proyek-proyek tertentu lebih seperti "blackfield" ... :-D
DevSolar
25

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 newValueselalu lebih besar dari oldValue, 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.

dave
sumber
16
Ehm, perkalian sewenang-wenang menjadi lebih efisien daripada pembagian sewenang-wenang tidak benar-benar tergantung pada prosesor, untuk mesin dunia nyata.
Deduplicator
1
Ada juga masalah aritmatika integer vs floating point. Jika rasionya fraksional, pembagian harus dilakukan dalam floating point, membutuhkan gips. Melewatkan pemeran akan menyebabkan kesalahan yang tidak disengaja. Jika fraksi terjadi menjadi rasio antara dua bilangan bulat kecil, maka penyusunan ulang mereka memungkinkan perbandingan dilakukan dalam aritmatika bilangan bulat. (Pada titik mana argumen Anda akan berlaku.)
rwong
@ rwong Tidak selalu. Beberapa bahasa di luar sana melakukan pembagian integer dengan menjatuhkan bagian desimal, sehingga tidak ada pemeran yang diperlukan.
T. Sar - Reinstate Monica
@ T.Sar Teknik yang Anda gambarkan dan semantik yang dijelaskan dalam jawabannya berbeda. Semantik adalah apakah programmer bermaksud menjawab sebagai nilai floating-point atau fraksional; teknik yang Anda gambarkan adalah pembagian dengan perkalian timbal balik, yang kadang-kadang merupakan perkiraan sempurna (substitusi) untuk divisi bilangan bulat. Teknik terakhir biasanya diterapkan ketika pembagi diketahui sebelumnya, karena derivasi bilangan bulat bilangan bulat (bergeser 2 ** 32) dapat dilakukan pada waktu kompilasi. Melakukannya pada saat runtime tidak akan bermanfaat karena lebih mahal CPU.
rwong
22

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.

svidgen
sumber
11
-1 Jawaban ini tidak benar-benar cocok dengan pertanyaan, yaitu tentang potensi jebakan divisi - tidak ada hubungannya dengan optimisasi
Ben Cottrell
13
@ BenCottrell Sangat pas. Jebakannya adalah menempatkan nilai dalam optimisasi kinerja tanpa arti dengan biaya pemeliharaan. Dari pertanyaan "apakah ada masalah dengan kebiasaan ini?" - Iya. Ini akan dengan cepat mengarah pada penulisan omong kosong mutlak.
Michael
9
@Michael pertanyaannya adalah tidak menanyakan tentang hal-hal itu - tetapi secara khusus menanyakan tentang kebenaran dari dua ekspresi berbeda yang masing-masing memiliki semantik dan perilaku yang berbeda, tetapi keduanya dimaksudkan untuk memenuhi persyaratan yang sama.
Ben Cottrell
5
@ BenCottrell Mungkin Anda bisa menunjukkan kepada saya di mana dalam pertanyaan itu ada yang menyebutkan tentang kebenaran?
Michael
5
@ BenCottrell Anda seharusnya mengatakan 'Saya tidak bisa' :)
Michael
13

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:

if ((ptr2 - ptr1) >= n / 3)  // good: check if length of subarray is at least n/3
    ...

Buruk multiplikasi:

if ((ptr2 - ptr1) * 3 >= n)  // bad: confusing!! what is the intention of this code?
    ...

Multiplikasi yang baik:

if (j - i >= 2 * min_length)  // good: obviously checking for a minimum length
    ...

Divisi buruk:

if ((j - i) / 2 >= min_length)  // bad: confusing!! what is the intention of this code?
    ...

Multiplikasi yang baik:

if (new_length >= old_length * 1.5)  // good: is the new size at least 50% bigger?
    ...

Divisi buruk:

if (new_length / old_length >= 2)  // bad: BUGGY!! will fail if old_length = 0!
    ...
Mehrdad
sumber
2
Saya setuju bahwa itu tergantung pada konteks, tetapi dua pasang contoh Anda yang pertama sangat buruk. Saya tidak akan memilih salah satu dari yang lain.
Michael
6
@Michael: Uhm ... Anda merasa (ptr2 - ptr1) * 3 >= nsemudah memahami seperti ekspresi ptr2 - 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.
Mehrdad
2
Variabel yang dipanggil ndan 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.
Michael
1
Contoh-contoh ini tidak benar-benar buruk .. jelas bukan 'sangat miskin' - bahkan jika Anda memasukkan 'nama wajar' mereka masih kurang masuk akal ketika Anda menukar mereka dengan kasus buruk. Jika saya baru mengenal suatu proyek, saya lebih suka melihat kasus 'baik' yang tercantum dalam jawaban ini ketika saya pergi untuk memperbaiki beberapa kode produksi.
John-M
3

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.

gnasher729
sumber
1

Mengenai keterbacaan kode, saya pikir perkalian sebenarnya lebih mudah dibaca dalam beberapa kasus. Misalnya, jika ada sesuatu yang harus Anda periksa apakah newValuetelah meningkat 5 persen atau lebih di atas oldValue, maka 1.05 * oldValuemerupakan ambang batas untuk diuji newValue, dan wajar untuk menulis

    if (newValue >= 1.05 * oldValue)

Namun 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 oldValuedijamin tidak negatif; tapi anggaplah newValuesebenarnya -13.5 dan oldValue-10.1. Kemudian

newValue/oldValue >= 1.05

mengevaluasi ke true , tetapi

newValue >= 1.05 * oldValue

mengevaluasi ke false .

David K.
sumber
1

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 .

ahli hukum agama
sumber
1

Mengambil poin 2 Anda, itu memang akan mencegah meluap untuk yang sangat kecil oldValue. Namun jika SOME_CONSTANTjuga sangat kecil maka metode alternatif Anda akan berakhir dengan underflow, di mana nilainya tidak dapat diwakili secara akurat.

Dan sebaliknya, apa yang terjadi jika oldValuesangat besar? Anda memiliki masalah yang sama, hanya sebaliknya.

Jika Anda ingin menghindari (atau meminimalkan) risiko overflow / underflow, cara terbaik adalah memeriksa apakah newValuejarak terdekatnya dengan oldValueatau ke SOME_CONSTANT. Anda kemudian dapat memilih operasi pembagian yang tepat

    if(newValue / oldValue >= SOME_CONSTANT)

atau

    if(newValue / SOME_CONSTANT >= oldValue)

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.

Graham
sumber
0

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.

radarbob
sumber
0

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.

Ishan Shah
sumber