Kesalahpahaman aritmatika titik apung dan kedatangan singkatnya adalah penyebab utama kejutan dan kebingungan dalam pemrograman (pertimbangkan jumlah pertanyaan pada Stack Overflow yang berkaitan dengan "angka tidak ditambahkan dengan benar"). Mengingat banyak programmer belum memahami implikasinya, ia memiliki potensi untuk memperkenalkan banyak bug halus (terutama ke dalam perangkat lunak keuangan). Apa yang bisa bahasa pemrograman lakukan untuk menghindari perangkap bagi mereka yang tidak terbiasa dengan konsep, sementara masih menawarkan kecepatan ketika akurasi tidak penting bagi mereka yang melakukan memahami konsep?
language-design
Adam Paynter
sumber
sumber
Jawaban:
Anda mengatakan "terutama untuk perangkat lunak keuangan", yang memunculkan salah satu kencing kesayangan saya: uang bukan pelampung, ini int .
Tentu, itu terlihat seperti pelampung. Ia memiliki titik desimal di sana. Tapi itu hanya karena Anda terbiasa dengan unit yang membingungkan masalah. Uang selalu datang dalam jumlah bilangan bulat. Di Amerika, itu sen. (Dalam konteks tertentu saya pikir ini bisa jadi pabrik , tapi abaikan saja untuk saat ini.)
Jadi ketika Anda mengatakan $ 1,23, itu benar-benar 123 sen. Selalu, selalu, selalu lakukan perhitungan Anda dengan istilah itu, dan Anda akan baik-baik saja. Untuk informasi lebih lanjut, lihat:
Menjawab pertanyaan secara langsung, bahasa pemrograman seharusnya hanya memasukkan jenis Uang sebagai primitif yang masuk akal.
memperbarui
Ok, saya seharusnya hanya mengatakan "selalu" dua kali, bukan tiga kali. Uang memang selalu int; mereka yang berpikir sebaliknya dipersilakan untuk mencoba mengirim saya 0,3 sen dan menunjukkan kepada saya hasilnya pada laporan bank Anda. Tetapi seperti yang ditunjukkan komentator, ada pengecualian langka ketika Anda perlu melakukan matematika floating point pada angka seperti uang. Misalnya, jenis harga tertentu atau perhitungan bunga. Bahkan kemudian, itu harus diperlakukan seperti pengecualian. Uang masuk dan keluar sebagai jumlah bilangan bulat, sehingga semakin dekat sistem Anda dengan itu, semakin waras itu.
sumber
Decimal
adalah satu-satunya sistem yang waras untuk menangani hal ini, dan komentar Anda "abaikan itu untuk saat ini" adalah pertanda malapetaka bagi para programmer di mana pun: PMemberikan dukungan untuk tipe Desimal membantu dalam banyak kasus. Banyak bahasa memiliki tipe desimal, tetapi mereka kurang dimanfaatkan.
Memahami perkiraan yang terjadi ketika bekerja dengan representasi bilangan real adalah penting. Menggunakan tipe desimal dan floating point
9 * (1/9) != 1
adalah pernyataan yang benar. Ketika konstanta pengoptimal dapat mengoptimalkan perhitungan sehingga itu benar.Memberikan operator perkiraan akan membantu. Namun, perbandingan seperti itu bermasalah. Perhatikan bahwa 0,9999 triliun dolar kira-kira sama dengan 1 triliun dolar. Bisakah Anda menyimpan perbedaan di rekening bank saya?
sumber
0.9999...
triliun dolar sebenarnya sama dengan 1 triliun dolar sebenarnya.0.99999...
. Mereka semua terpotong di beberapa titik yang menghasilkan ketimpangan.0.9999
cukup setara untuk teknik. Untuk tujuan finansial bukan.Kami diberitahu apa yang harus dilakukan pada kuliah tahun pertama (tingkat dua) dalam ilmu komputer ketika saya pergi ke universitas, (kursus ini merupakan prasyarat untuk sebagian besar kursus sains juga)
Saya ingat dosen itu berkata, "Angka titik apung adalah perkiraan. Gunakan tipe integer untuk uang. Gunakan FORTRAN atau bahasa lain dengan nomor BCD untuk perhitungan yang akurat." (dan kemudian dia menunjukkan perkiraannya, menggunakan contoh klasik 0,2 yang tidak mungkin untuk mewakili secara akurat dalam floating point biner). Ini juga muncul minggu itu di latihan laboratorium.
Kuliah yang sama: "Jika Anda harus mendapatkan akurasi lebih dari titik mengambang, urutkan istilah Anda. Tambahkan angka kecil bersama-sama, bukan ke angka besar." Itu melekat di pikiran saya.
Beberapa tahun yang lalu saya memiliki beberapa geometri bola yang perlu sangat akurat, dan masih cepat. 80 bit ganda pada PC tidak memotongnya, jadi saya menambahkan beberapa jenis ke program yang mengurutkan istilah sebelum melakukan operasi komutatif. Masalah terpecahkan.
Sebelum Anda mengeluh tentang kualitas gitar, belajarlah bermain.
Saya memiliki rekan kerja empat tahun lalu yang pernah bekerja untuk JPL. Dia menyatakan tidak percaya bahwa kami menggunakan FORTRAN untuk beberapa hal. (Kami membutuhkan simulasi numerik super akurat yang dihitung secara offline.) "Kami mengganti semua FORTRAN itu dengan C ++," katanya bangga. Saya berhenti bertanya-tanya mengapa mereka merindukan sebuah planet.
sumber
1.0 + 0.1 + ... + 0.1
(diulang 10 kali) kembali1.0
karena setiap hasil antara dibulatkan. Melakukannya babak cara lain, Anda mendapatkan hasil antara dari0.2
,0.3
, ...,1.0
dan akhirnya2.0
. Ini adalah contoh ekstrem, tetapi dengan angka floating point realistis, masalah serupa terjadi. Ide dasarnya adalah bahwa menambahkan angka dengan ukuran yang sama menyebabkan kesalahan terkecil. Mulailah dengan angka terkecil karena jumlah mereka lebih besar dan oleh karena itu lebih cocok untuk penambahan ke jumlah yang lebih besar.Saya tidak percaya apa pun bisa atau harus dilakukan pada tingkat bahasa.
sumber
Decimal
ketika menyangkut pengujian kesetaraan. Perbedaan antara1.0m/7.0m*7.0m
dan1.0m
mungkin banyak urutan besarnya kurang dari perbedaan antara1.0/7.0*7.0
, tetapi itu bukan nol.Secara default, bahasa harus menggunakan rasional presisi arbitrer untuk angka yang bukan bilangan bulat.
Mereka yang perlu mengoptimalkan selalu dapat meminta mengapung. Menggunakannya sebagai default masuk akal dalam bahasa pemrograman sistem C dan lainnya, tetapi tidak dalam kebanyakan bahasa populer saat ini.
sumber
double
. Jika suatu perhitungan perlu akurat untuk satu bagian per juta, lebih baik menghabiskan satu mikrodetik menghitungnya dalam beberapa bagian per miliar, daripada menghabiskan satu detik menghitungnya dengan sangat tepat.Dua masalah terbesar yang melibatkan angka floating point adalah:
Jenis kegagalan pertama hanya dapat diatasi dengan menyediakan jenis komposit yang mencakup nilai dan informasi unit. Misalnya, nilai
length
atauarea
yang menggabungkan unit (meter atau meter persegi atau kaki dan kaki persegi masing-masing). Kalau tidak, Anda harus rajin selalu bekerja dengan satu jenis unit pengukuran dan hanya mengkonversi ke yang lain ketika kami membagikan jawabannya dengan manusia.Jenis kegagalan kedua adalah kegagalan konseptual. Kegagalan memanifestasikan diri ketika orang menganggapnya sebagai angka absolut . Ini mempengaruhi operasi kesetaraan, kesalahan pembulatan kumulatif, dll. Misalnya, mungkin benar bahwa untuk satu sistem dua pengukuran setara dalam margin kesalahan tertentu. Yaitu .999 dan 1.001 kira-kira sama dengan 1.0 ketika Anda tidak peduli tentang perbedaan yang lebih kecil dari +/- .1. Namun, tidak semua sistem toleran.
Jika ada fasilitas tingkat bahasa yang dibutuhkan, maka saya akan menyebutnya presisi kesetaraan . Di NUnit, JUnit, dan kerangka pengujian yang dibuat serupa Anda dapat mengontrol presisi yang dianggap benar. Sebagai contoh:
Jika, misalnya, C # atau Java diubah untuk menyertakan operator presisi, mungkin terlihat seperti ini:
Namun, jika Anda menyediakan fitur seperti itu, Anda juga harus mempertimbangkan kasus di mana kesetaraan baik jika sisi +/- tidak sama. Misalnya, +1 / -10 akan mempertimbangkan dua angka yang setara jika salah satu dari mereka berada dalam 1 lebih, atau 10 kurang dari angka pertama. Untuk menangani kasus ini, Anda mungkin perlu menambahkan
range
kata kunci juga:sumber
Apa yang bisa dilakukan oleh bahasa pemrograman? Tidak tahu apakah ada satu jawaban untuk pertanyaan itu, karena apa pun yang dilakukan kompiler / penerjemah atas nama programmer untuk membuat hidupnya lebih mudah biasanya bekerja melawan kinerja, kejelasan, dan keterbacaan. Saya pikir kedua cara C ++ (hanya membayar untuk apa yang Anda butuhkan) dan cara Perl (prinsip kejutan terkecil) keduanya valid, tetapi itu tergantung pada aplikasi.
Pemrogram masih perlu bekerja dengan bahasa dan memahami bagaimana ia menangani floating point, karena jika tidak, mereka akan membuat asumsi, dan suatu hari perilaku yang ditentukan tidak akan cocok dengan asumsi mereka.
Pandangan saya tentang apa yang perlu diketahui oleh programmer:
sumber
Gunakan default yang masuk akal, misalnya dukungan bawaan untuk decmial.
Groovy melakukan ini dengan cukup baik, meskipun dengan sedikit usaha Anda masih bisa menulis kode untuk memperkenalkan ketidaktepatan floating point.
sumber
Saya setuju tidak ada yang bisa dilakukan di tingkat bahasa. Pemrogram harus memahami bahwa komputer itu terpisah dan terbatas, dan bahwa banyak konsep matematika yang diwakili di dalamnya hanyalah perkiraan.
Jangankan floating point. Kita harus memahami bahwa setengah dari pola bit digunakan untuk bilangan negatif dan bahwa 2 ^ 64 sebenarnya cukup kecil untuk menghindari masalah tipikal dengan aritmatika integer.
sumber
x
==y
tidak menyiratkan bahwa melakukan perhitungan padax
akan menghasilkan hasil yang sama dengan melakukan perhitungan yang sama paday
).Satu hal yang bisa dilakukan oleh bahasa - menghapus perbandingan kesetaraan dari tipe floating point selain dari perbandingan langsung dengan nilai NAN.
Pengujian kesetaraan hanya akan ada sebagai pemanggilan fungsi yang mengambil dua nilai dan delta, atau untuk bahasa seperti C # yang memungkinkan tipe memiliki metode EqualsTo yang mengambil nilai lain dan delta.
sumber
Saya merasa aneh bahwa tidak ada yang menunjukkan trik nomor rasional keluarga Lisp.
Serius, buka sbcl, dan lakukan ini:
(+ 1 3)
dan Anda mendapat 4. Jika*( 3 2)
Anda mendapatkan 6. Sekarang coba(/ 5 3)
dan Anda dapatkan 5/3, atau 5 pertiga.Itu seharusnya agak membantu dalam beberapa situasi, bukan?
sumber
Satu hal yang saya ingin melihat akan menjadi pengakuan bahwa
double
untukfloat
harus dianggap sebagai konversi pelebaran, sementarafloat
untukdouble
adalah penyempitan (*). Itu mungkin tampak kontra-intuisi, tetapi pertimbangkan apa arti sebenarnya tipe-tipe ini:Jika seseorang memiliki
double
yang memegang representasi terbaik dari kuantitas "sepersepuluh" dan mengubahnya menjadifloat
, hasilnya akan menjadi "13.421.773,5 / 134.217.728, plus atau minus 1 / 268.435.456 atau lebih", yang merupakan deskripsi nilai yang benar.Sebaliknya, jika seseorang memiliki
float
yang memegang representasi terbaik dari kuantitas "sepersepuluh" dan mengubahnya menjadidouble
, hasilnya akan menjadi "13.421.773,5 / 134.217.728, plus atau minus 1 / 72.057.594.037.927.936 atau lebih" - tingkat akurasi yang tersirat yang salah dengan faktor lebih dari 53 juta.Meskipun standar IEEE-744 mensyaratkan bahwa matematika titik-mengambang dilakukan seolah-olah setiap angka titik-mengambang mewakili kuantitas numerik tepat tepat di pusat jangkauannya, yang tidak boleh dianggap menyiratkan bahwa nilai-nilai titik-mengambang sebenarnya mewakili yang tepat jumlah numerik. Sebaliknya, persyaratan bahwa nilai-nilai diasumsikan berada di pusat rentang mereka berasal dari tiga fakta: (1) perhitungan harus dilakukan seolah-olah operan memiliki beberapa nilai tepat tertentu; (2) asumsi yang konsisten dan terdokumentasi lebih bermanfaat daripada yang tidak konsisten atau tidak berdokumen; (3) jika seseorang akan membuat asumsi konsisten, tidak ada asumsi konsisten lain yang cenderung lebih baik daripada mengasumsikan kuantitas mewakili pusat kisarannya.
Kebetulan, saya ingat sekitar 25 tahun yang lalu, seseorang datang dengan paket numerik untuk C yang menggunakan "berbagai jenis", masing-masing terdiri dari sepasang pelampung 128-bit; semua perhitungan akan dilakukan sedemikian rupa untuk menghitung nilai minimum dan maksimum yang mungkin untuk setiap hasil. Jika seseorang melakukan perhitungan berulang yang panjang dan muncul dengan nilai [12.53401391134 12.53902812673], orang dapat yakin bahwa sementara banyak digit presisi hilang dari kesalahan pembulatan, hasilnya masih bisa dinyatakan secara wajar sebagai 12,54 (dan tidak t benar-benar 12.9 atau 53.2). Saya terkejut saya belum melihat adanya dukungan untuk jenis seperti itu dalam bahasa mainstream, terutama karena mereka tampaknya cocok dengan unit matematika yang dapat beroperasi pada berbagai nilai secara paralel.
(*) Dalam praktiknya, sering kali bermanfaat untuk menggunakan nilai presisi ganda untuk memegang perhitungan menengah ketika bekerja dengan angka presisi tunggal, jadi harus menggunakan tipografi untuk semua operasi semacam itu bisa mengganggu. Bahasa dapat membantu dengan memiliki tipe "fuzzy double", yang akan melakukan komputasi sebagai ganda, dan dapat dengan bebas dilemparkan ke dan dari tunggal; ini akan sangat membantu jika fungsi yang mengambil parameter tipe
double
dan kembalidouble
dapat ditandai sehingga mereka secara otomatis menghasilkan kelebihan yang menerima dan mengembalikan "fuzzy double" sebagai gantinya.sumber
Jika lebih banyak bahasa pemrograman mengambil halaman dari basis data dan memungkinkan pengembang untuk menentukan panjang dan ketepatan tipe data numerik mereka, mereka secara substansial dapat mengurangi kemungkinan kesalahan terkait titik apung. Jika bahasa memungkinkan pengembang untuk mendeklarasikan variabel sebagai Float (2), yang menunjukkan bahwa mereka membutuhkan angka floating point dengan dua digit desimal presisi, itu bisa melakukan operasi matematika lebih aman. Jika itu dilakukan dengan mewakili variabel sebagai integer secara internal dan membaginya dengan 100 sebelum mengekspos nilai, itu bisa meningkatkan kecepatan dengan menggunakan jalur aritmatika integer yang lebih cepat. Semantik Float (2) juga akan membiarkan pengembang menghindari kebutuhan konstan untuk membulatkan data sebelum mengeluarkannya karena Float (2) akan secara inheren membulatkan data ke dua titik desimal.
Tentu saja, Anda harus mengizinkan pengembang untuk meminta nilai floating point dengan ketelitian maksimum saat pengembang harus memiliki ketepatan itu. Dan Anda akan memperkenalkan masalah di mana ekspresi yang sedikit berbeda dari operasi matematika yang sama menghasilkan hasil yang berpotensi berbeda karena operasi pembulatan menengah ketika pengembang tidak membawa cukup presisi dalam variabel mereka. Tapi setidaknya di dunia basis data, itu sepertinya bukan masalah besar. Kebanyakan orang tidak melakukan perhitungan ilmiah yang membutuhkan banyak ketepatan dalam hasil-hasil antara.
sumber
Float(2)
seperti yang Anda usulkan tidak boleh dipanggilFloat
, karena tidak ada yang mengambang di sini, tentu saja bukan "titik desimal".Ini di atas berlaku dalam beberapa kasus, tetapi tidak benar-benar solusi umum untuk berurusan dengan nilai float. Solusi nyata adalah memahami masalah dan belajar bagaimana menghadapinya. Jika Anda menggunakan perhitungan titik float, Anda harus selalu memeriksa apakah algoritma Anda stabil secara numerik . Ada bidang besar matematika / ilmu komputer yang berhubungan dengan masalah tersebut. Ini disebut Analisis Numerik .
sumber
Seperti jawaban lain telah dicatat, satu-satunya cara nyata untuk menghindari jebakan floating point dalam perangkat lunak keuangan adalah tidak menggunakannya di sana. Ini sebenarnya mungkin layak - jika Anda menyediakan perpustakaan yang dirancang dengan baik yang didedikasikan untuk matematika keuangan .
Fungsi yang dirancang untuk mengimpor estimasi titik apung harus diberi label dengan jelas seperti itu, dan dilengkapi dengan parameter yang sesuai dengan operasi itu, misalnya:
Satu-satunya cara nyata untuk menghindari jebakan floating point pada umumnya adalah pendidikan - programmer perlu membaca dan memahami sesuatu seperti Apa Yang Harus Setiap Programmer Ketahui Tentang Aritmatika Floating-Point .
Namun, beberapa hal yang mungkin membantu:
isNear()
fungsi.sumber
Sebagian besar programmer akan terkejut bahwa COBOL melakukan hal yang benar ... dalam versi pertama COBOL tidak ada floating point, hanya desimal, dan tradisi di COBOL berlanjut hingga hari ini bahwa hal pertama yang Anda pikirkan ketika mendeklarasikan angka adalah desimal. .. floating point hanya akan digunakan jika Anda benar-benar membutuhkannya. Ketika C datang, untuk beberapa alasan, tidak ada tipe desimal primitif, jadi menurut saya, di situlah semua masalah dimulai.
sumber