Apakah membandingkan persamaan angka float menyesatkan pengembang junior bahkan jika tidak ada kesalahan pembulatan terjadi dalam kasus saya?

31

Misalnya, saya ingin menampilkan daftar tombol mulai 0,0,5, ... 5, yang melompat untuk setiap 0,5. Saya menggunakan loop for untuk melakukan itu, dan memiliki warna berbeda di tombol STANDARD_LINE:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0;i<=MAX;i=i+DIFF){
    button.text=i+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

Pada kasus ini seharusnya tidak ada kesalahan pembulatan karena setiap nilai tepat di IEEE 754.Tapi saya berjuang jika saya harus mengubahnya untuk menghindari perbandingan kesetaraan floating point:

var MAX=10;
var STANDARD_LINE=3;

for(var i=0;i<=MAX;i++){
    button.text=i/2.0+'';
    if(i==STANDARD_LINE/2.0){
      button.color='red';
    }
}

Di satu sisi, kode asli lebih sederhana dan maju ke saya. Tetapi ada satu hal yang saya pertimbangkan: apakah saya == STANDARD_LINE menyesatkan rekan satu tim junior? Apakah itu menyembunyikan fakta bahwa angka floating point mungkin memiliki kesalahan pembulatan? Setelah membaca komentar dari posting ini:

https://stackoverflow.com/questions/33646148/is-hardcode-float-precise-if-it-can-be-represented-by-binary-format-in-ieee-754

sepertinya ada banyak pengembang yang tidak tahu beberapa angka float tepat. Haruskah saya menghindari perbandingan kesetaraan angka float meskipun valid dalam kasus saya? Atau apakah saya terlalu memikirkan hal ini?

ocomfd
sumber
23
Perilaku daftar kode dua ini tidak setara. 3 / 2.0 adalah 1,5 tetapi ihanya akan menjadi angka utuh dalam daftar kedua. Coba hapus yang kedua /2.0.
candied_orange
27
Jika Anda benar-benar perlu membandingkan dua FP untuk kesetaraan (yang tidak diperlukan seperti yang ditunjukkan oleh orang lain dalam jawaban mereka karena Anda bisa menggunakan perbandingan penghitung lingkaran dengan bilangan bulat), tetapi jika Anda melakukannya, maka komentar harus cukup. Secara pribadi saya telah bekerja dengan IEEE FP untuk waktu yang lama dan saya masih bingung jika saya melihat, katakanlah, perbandingan langsung SPFP tanpa komentar atau apapun. Itu hanya kode yang sangat halus - layak komentar setidaknya setiap kali IMHO.
14
Apa pun yang Anda pilih, inilah salah satu kasus di mana komentar yang menjelaskan bagaimana dan mengapa sangat penting. Pengembang selanjutnya mungkin bahkan tidak mempertimbangkan seluk beluk tanpa komentar untuk menarik perhatian mereka. Juga, saya sangat terganggu oleh fakta bahwa buttontidak berubah di mana pun di lingkaran Anda. Bagaimana daftar tombol diakses? Melalui indeks ke array atau mekanisme lain? Jika itu dengan indeks akses ke array, ini adalah argumen lain yang mendukung beralih ke bilangan bulat.
jpmc26
9
Tulis kode itu. Sampai seseorang berpikir bahwa 0,6 akan menjadi ukuran langkah yang lebih baik dan hanya mengubah konstan itu.
tofro
11
"... menyesatkan pengembang junior" Anda juga akan menyesatkan pengembang senior. Terlepas dari jumlah pemikiran yang telah Anda masukkan ke dalam ini, mereka akan menganggap bahwa Anda tidak tahu apa yang Anda lakukan, dan kemungkinan akan mengubahnya ke versi integer.
GrandOpener

Jawaban:

116

Saya akan selalu menghindari operasi floating-point berturut-turut kecuali model yang saya hitung membutuhkannya. Aritmatika floating-point tidak intuitif untuk sebagian besar dan merupakan sumber kesalahan utama. Dan menceritakan kasus-kasus yang menyebabkan kesalahan dari mereka yang tidak melakukannya adalah perbedaan yang bahkan lebih halus!

Oleh karena itu, menggunakan pelampung sebagai penghitung lingkaran adalah cacat yang menunggu untuk terjadi dan paling tidak memerlukan komentar latar belakang yang gemuk menjelaskan mengapa boleh saja menggunakan 0,5 di sini, dan bahwa ini tergantung pada nilai numerik tertentu. Pada titik itu, menulis ulang kode untuk menghindari counter float mungkin akan menjadi opsi yang lebih mudah dibaca. Dan keterbacaan berada di sebelah kebenaran dalam hierarki persyaratan profesional.

Kilian Foth
sumber
48
Saya suka "menunggu cacat terjadi". Tentu, itu mungkin bekerja sekarang , tetapi angin sepoi-sepoi dari seseorang yang lewat akan mematahkannya.
AakashM
10
Misalnya, misalkan persyaratan berubah sehingga alih-alih 11 tombol dengan jarak yang sama dari 0 ke 5 dengan "garis standar" pada tombol ke-4, Anda memiliki 16 tombol dengan spasi sama dari 0 hingga 5 dengan "garis standar" pada tanggal 6 tombol. Jadi, siapa pun yang mewarisi kode ini dari Anda, ubah 0,5 ke 1,0 / 3,0 dan perubahan 1,5 hingga 5,0 / 3,0. Lalu apa yang terjadi?
David K
8
Ya, saya tidak nyaman dengan gagasan bahwa mengubah apa yang tampaknya menjadi nomor arbitrer (sebagai "normal" sebagai nomor bisa) ke nomor arbitrer lain (yang tampaknya sama "normal") sebenarnya memperkenalkan cacat.
Alexander - Reinstate Monica
7
@Alexander: benar, Anda perlu komentar yang mengatakan DIFF must be an exactly-representable double that evenly divides STANDARD_LINE. Jika Anda tidak ingin menulis komentar itu (dan mengandalkan semua pengembang masa depan untuk mengetahui cukup tentang floating point IEEE754 binary64 untuk memahaminya), maka jangan menulis kode dengan cara ini. yaitu jangan menulis kode dengan cara ini. Terutama karena itu mungkin bahkan tidak lebih efisien: Penambahan FP memiliki latensi yang lebih tinggi daripada penambahan bilangan bulat, dan itu adalah ketergantungan loop-carry. Juga, kompiler (bahkan kompiler JIT?) Mungkin lebih baik dalam membuat loop dengan counter integer.
Peter Cordes
39

Sebagai aturan umum, loop harus ditulis sedemikian rupa untuk berpikir melakukan sesuatu n kali. Jika Anda menggunakan indeks floating point, itu bukan lagi pertanyaan melakukan sesuatu n kali melainkan menjalankan sampai kondisi terpenuhi. Jika kondisi ini sangat mirip dengan i<nyang diharapkan oleh banyak programmer, maka kode tersebut tampaknya melakukan satu hal ketika sebenarnya melakukan hal lain yang dapat dengan mudah disalahartikan oleh programmer yang membaca kode.

Ini agak subyektif, tetapi menurut pendapat saya, jika Anda dapat menulis ulang loop untuk menggunakan indeks integer untuk mengulang beberapa kali, Anda harus melakukannya. Jadi pertimbangkan alternatif berikut:

var DIFF=0.5;                           // pixel increment
var MAX=Math.floor(5.0/DIFF);           // 5.0 is max pixel width
var STANDARD_LINE=Math.floor(1.5/DIFF); // 1.5 is pixel width

for(var i=0;i<=MAX;i++){
    button.text=(i*DIFF)+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

Loop bekerja dalam hal bilangan bulat. Dalam hal ini iadalah bilangan bulat dan STANDARD_LINEdipaksa ke bilangan bulat juga. Ini tentu saja akan mengubah posisi garis standar Anda jika kebetulan ada pembulatan dan juga untuk MAX, jadi Anda harus tetap berusaha untuk mencegah pembulatan untuk rendering yang akurat. Namun Anda masih memiliki keuntungan mengubah parameter dalam hal piksel dan bukan bilangan bulat tanpa harus khawatir tentang perbandingan poin mengambang.

Neil
sumber
3
Anda mungkin juga ingin mempertimbangkan pembulatan alih-alih lantai dalam tugas, tergantung pada apa yang Anda inginkan. Jika divisi seharusnya memberikan hasil bilangan bulat, lantai mungkin memberikan kejutan jika Anda menemukan angka-angka di mana divisi tersebut sedikit tidak aktif.
ilkkachu
1
@ilkkachu Benar. Pikiranku adalah bahwa jika Anda menetapkan 5.0 sebagai jumlah piksel maksimum, kemudian melalui pembulatan, Anda lebih suka berada di sisi bawah dari 5.0 itu daripada sedikit lebih banyak. 5.0 secara efektif akan maksimal. Padahal pembulatan mungkin lebih disukai menurut apa yang perlu Anda lakukan. Dalam kedua kasus tersebut, tidak ada bedanya jika divisi tetap membuat angka keseluruhan.
Neil
4
Saya sangat tidak setuju. Cara terbaik untuk menghentikan loop adalah dengan kondisi yang paling alami mengekspresikan logika bisnis. Jika logika bisnis adalah bahwa Anda memerlukan 11 tombol, loop harus berhenti di iterasi 11. Jika logika bisnis adalah bahwa tombol adalah 0,5 terpisah sampai garis penuh, loop harus berhenti ketika garis penuh. Ada pertimbangan lain yang dapat mendorong pilihan menuju satu mekanisme atau yang lain tetapi tanpa pertimbangan tersebut, pilihlah mekanisme yang paling dekat dengan kebutuhan bisnis.
Pasang kembali Monica
Penjelasan Anda akan sepenuhnya benar untuk Java / C ++ / Ruby / Python / ... Tapi Javascript tidak memiliki bilangan bulat, jadi idan STANDARD_LINEhanya terlihat seperti bilangan bulat. Tidak ada paksaan sama sekali, dan DIFF, MAXdan STANDARD_LINEsemuanya hanya Numbers. Numbers digunakan sebagai bilangan bulat harus aman di bawah ini 2**53, mereka masih angka floating point.
Eric Duminil
@EricDuminil Ya, tapi ini setengahnya. Setengah lainnya adalah keterbacaan. Saya menyebutkannya sebagai alasan utama untuk melakukannya dengan cara ini, bukan untuk optimasi.
Neil
20

Saya setuju dengan semua jawaban lain yang menggunakan variabel loop non-integer umumnya gaya buruk bahkan dalam kasus seperti ini di mana ia akan bekerja dengan benar. Tapi menurut saya ada alasan lain mengapa gaya ini buruk.

Kode Anda "tahu" bahwa lebar garis yang tersedia adalah kelipatan 0,5 dari 0 hingga 5,0. Haruskah itu Sepertinya itu adalah keputusan antarmuka pengguna yang mungkin dengan mudah berubah (misalnya, mungkin Anda ingin kesenjangan antara lebar yang tersedia menjadi lebih besar seperti yang dilakukan lebar. 0,25, 0,5, 0,75, 1.0, 1.5, 2.0, 2.5, 3.0, 3.0, 4.0, 5.0 atau sesuatu).

Kode Anda "tahu" bahwa lebar garis yang tersedia semuanya memiliki representasi "bagus" baik sebagai angka floating-point dan sebagai desimal. Itu juga sepertinya sesuatu yang mungkin berubah. (Anda mungkin ingin 0,1, 0,2, 0,3, ... di beberapa titik.)

Kode Anda "tahu" bahwa teks yang akan dimasukkan pada tombol hanyalah Javascript yang mengubah nilai-nilai titik-mengambang itu. Itu juga sepertinya sesuatu yang mungkin berubah. (Misalnya, mungkin suatu hari Anda akan menginginkan lebar seperti 1/3, yang Anda mungkin tidak ingin ditampilkan sebagai 0,33333333333333 atau apa pun. Atau mungkin Anda ingin melihat "1.0" alih-alih "1" untuk konsistensi dengan "1,5" .)

Semua ini bagi saya terasa seperti manifestasi dari kelemahan tunggal, yang merupakan semacam campuran lapisan. Angka-angka floating-point adalah bagian dari logika internal perangkat lunak. Teks yang ditampilkan pada tombol adalah bagian dari antarmuka pengguna. Mereka harus lebih terpisah daripada yang ada dalam kode di sini. Pengertian seperti "mana di antara ini yang merupakan standar yang harus disorot?" adalah masalah antarmuka pengguna, dan mereka mungkin tidak boleh terikat dengan nilai-nilai floating-point tersebut. Dan loop Anda di sini benar-benar (atau setidaknya seharusnya) loop atas tombol , bukan atas lebar garis . Ditulis seperti itu, godaan untuk menggunakan variabel loop mengambil nilai-nilai non-integer menghilang: Anda hanya akan menggunakan integer yang berurutan atau for ... in / for ... of loop.

Perasaan saya adalah bahwa sebagian besar kasus di mana seseorang mungkin tergoda untuk mengulangi angka-angka non-integer adalah seperti ini: ada alasan lain, sama sekali tidak terkait dengan masalah numerik, mengapa kode harus diatur secara berbeda. (Tidak semua kasus; Saya dapat membayangkan beberapa algoritma matematika mungkin paling jelas dinyatakan dalam bentuk loop atas nilai-nilai non-integer.)

Gareth McCaughan
sumber
8

Satu kode bau menggunakan floats in loop seperti itu.

Looping dapat dilakukan dalam banyak cara, tetapi dalam 99,9% dari kasus Anda harus tetap pada kenaikan 1 atau pasti akan ada kebingungan, tidak hanya oleh pengembang junior.

Pieter B
sumber
Saya tidak setuju, saya pikir kelipatan bilangan bulat dari 1 tidak membingungkan dalam for-loop. Saya tidak akan menganggap itu sebagai bau kode. Hanya sebagian kecil.
CodeMonkey
3

Ya, Anda ingin menghindari ini.

Angka floating point adalah salah satu jebakan terbesar bagi programmer yang tidak menaruh curiga (yang berarti, dalam pengalaman saya, hampir semua orang). Dari tergantung pada tes kesetaraan floating point, untuk mewakili uang sebagai floating point, itu semua adalah masalah besar. Menambahkan satu pelampung di atas yang lain adalah salah satu pelanggar terbesar. Ada banyak volume literatur ilmiah tentang hal-hal seperti ini.

Gunakan angka floating point tepat di tempat-tempat yang sesuai, misalnya ketika melakukan perhitungan matematika aktual di mana Anda membutuhkannya (seperti trigonometri, merencanakan fungsi grafik dll.) Dan berhati-hatilah saat melakukan operasi serial. Kesetaraan benar. Pengetahuan tentang set angka tertentu mana yang tepat menurut standar IEEE sangat misterius dan saya tidak akan pernah bergantung padanya.

Dalam kasus Anda, ada akan , dengan Undang-Undang Murphys, datang titik di mana manajemen ingin Anda tidak memiliki 0,0, 0,5, 1,0 ... tapi 0,0, 0,4, 0,8 ... atau apa pun; Anda Anda akan segera borked, dan programmer junior Anda (atau diri Anda sendiri) akan men-debug panjang dan keras sampai Anda menemukan masalah.

Dalam kode khusus Anda, saya memang akan memiliki variabel loop integer. Ini mewakili itombol th, bukan angka yang sedang berjalan.

Dan saya mungkin, demi kejelasan ekstra, tidak menulis i/2tetapi i*0.5yang membuatnya sangat jelas apa yang sedang terjadi.

var BUTTONS=11;
var STANDARD_LINE=3;

for(var i=0; i<BUTTONS; i++) {
    button.text = (i*0.5)+'';
    if (i==STANDARD_LINE) {
      button.color='red';
    }
}

Catatan: seperti yang ditunjukkan dalam komentar, JavaScript sebenarnya tidak memiliki tipe terpisah untuk bilangan bulat. Tetapi bilangan bulat hingga 15 digit dijamin akurat / aman (lihat https://www.ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer ), maka untuk argumen seperti ini ("apakah itu lebih membingungkan / rawan kesalahan untuk bekerja dengan bilangan bulat atau non-bilangan bulat ") ini hampir tepat untuk memiliki tipe terpisah" dalam semangat "; dalam penggunaan sehari-hari (loop, koordinat layar, indeks array, dll.) tidak akan ada kejutan dengan angka integer diwakili Numbersebagai JavaScript.

AnoE
sumber
Saya akan mengubah nama BUTTONS menjadi sesuatu yang lain - ada 11 tombol dan bukan 10. Mungkin FIRST_BUTTON = 0, LAST_BUTTON = 10, STANDARD_LINE_BUTTON = 3. Selain itu, ya, begitulah Anda harus melakukannya.
gnasher729
Memang benar, @EricDuminil, dan saya telah menambahkan sedikit tentang ini ke dalam jawabannya. Terima kasih!
AnoE
1

Saya kira saran Anda tidak bagus. Sebagai gantinya, saya akan memperkenalkan variabel untuk jumlah tombol berdasarkan pada nilai maksimum dan spasi. Kemudian, cukup sederhana untuk mengulang indeks tombol itu sendiri.

function precisionRound(number, precision) {
  let factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

var maxButtonValue = 5.0;
var buttonSpacing = 0.5;

let countEstimate = precisionRound(maxButtonValue / buttonSpacing, 5);
var buttonCount = Math.floor(countEstimate) + 1;

var highlightPosition = 3;
var highlightColor = 'red';

for (let i=0; i < buttonCount; i++) {
    let buttonValue = i / buttonSpacing;
    button.text = buttonValue.toString();
    if (i == highlightPosition) {
        button.color = highlightColor;
    }
}

Mungkin lebih banyak kode, tetapi juga lebih mudah dibaca dan lebih kuat.

Jared Goguen
sumber
0

Anda dapat menghindari semuanya dengan menghitung nilai yang ditampilkan daripada menggunakan penghitung lingkaran sebagai nilai:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0; (i*DIFF) < MAX ; i=i+1){
    var val = i * DIFF

    button.text=val+'';

    if(val==STANDARD_LINE){
      button.color='red';
    }
}
Arnab Datta
sumber
-1

Aritmatika titik apung lambat dan aritmatika bilangan bulat cepat, jadi ketika saya menggunakan titik apung, saya tidak akan menggunakannya secara tidak perlu di mana bilangan bulat dapat digunakan. Sangat berguna untuk selalu memikirkan angka floating point, bahkan konstanta, sebagai perkiraan, dengan beberapa kesalahan kecil. Ini sangat berguna selama debugging untuk mengganti angka floating point asli dengan objek floating point plus / minus di mana Anda memperlakukan setiap angka sebagai rentang, bukan titik. Dengan cara itu Anda mengungkap ketidakakuratan tumbuh progresif setelah setiap operasi aritmatika. Jadi "1,5" harus dianggap sebagai "beberapa angka antara 1,45 dan 1,55", dan "1,50" harus dianggap sebagai "beberapa angka antara 1,495 dan 1,505".

Jacquez
sumber
5
Perbedaan kinerja antara bilangan bulat dan float adalah penting ketika menulis kode C untuk mikroprosesor kecil, tetapi CPU yang diturunkan x86 modern melakukan floating point dengan begitu cepat sehingga setiap penalti mudah dikalahkan oleh overhead menggunakan bahasa dinamis. Secara khusus, bukankah Javascript benar-benar mewakili setiap angka sebagai floating-point, menggunakan muatan NaN bila perlu?
leftaroundabout
1
"Aritmatika titik apung lambat dan aritmatika bilangan bulat cepat" adalah disangkal historis yang tidak boleh Anda pertahankan karena Injil terus maju. Untuk menambahkan apa yang dikatakan @leftaroundabout, bukan hanya benar bahwa hukumannya akan hampir tidak relevan, Anda mungkin menemukan operasi floating-point lebih cepat daripada operasi integer setara mereka, berkat keajaiban kompiler otomatisasi dan set instruksi yang dapat berderak sejumlah besar pelampung dalam satu siklus. Untuk pertanyaan ini tidak relevan, tetapi asumsi dasar "integer lebih cepat daripada float" tidak berlaku untuk beberapa waktu.
Jeroen Mostert
1
@JeroenMostert SSE / AVX memiliki operasi vektor untuk bilangan bulat dan mengapung, dan Anda mungkin dapat menggunakan bilangan bulat yang lebih kecil (karena tidak ada bit yang terbuang pada eksponen), jadi pada prinsipnya orang mungkin masih dapat memeras lebih banyak kinerja dari kode integer yang sangat dioptimalkan dibandingkan dengan mengapung. Tetapi sekali lagi, ini tidak relevan untuk sebagian besar aplikasi dan jelas bukan untuk pertanyaan ini.
leftaroundtentang
1
@leftaroundabout: Tentu. Maksud saya bukan tentang yang mana yang pasti lebih cepat dalam situasi tertentu, hanya saja "Saya tahu FP lambat dan integer cepat jadi saya akan menggunakan bilangan bulat jika memungkinkan" bukan motivasi yang baik bahkan sebelum Anda menangani pertanyaan apakah hal yang Anda lakukan perlu dioptimalkan.
Jeroen Mostert