Pertimbangkan kode berikut:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Mengapa ketidakakuratan ini terjadi?
Pertimbangkan kode berikut:
0.1 + 0.2 == 0.3 -> false
0.1 + 0.2 -> 0.30000000000000004
Mengapa ketidakakuratan ini terjadi?
Jawaban:
Biner floating point matematika seperti ini. Dalam sebagian besar bahasa pemrograman, ini didasarkan pada standar IEEE 754 . Inti masalahnya adalah bahwa angka-angka direpresentasikan dalam format ini sebagai bilangan bulat dikalikan dengan kekuatan dua; bilangan rasional (seperti 0.1
, yang 1/10
) yang penyebutnya bukan kekuatan dua tidak dapat secara tepat direpresentasikan.
Karena 0.1
dalam binary64
format standar , representasi dapat ditulis persis seperti
0.1000000000000000055511151231257827021181583404541015625
dalam desimal, atau0x1.999999999999ap-4
dalam notasi C99 hexfloat .Sebaliknya, bilangan rasional 0.1
, yaitu 1/10
, dapat ditulis persis seperti
0.1
dalam desimal, atau0x1.99999999999999...p-4
dalam analog notasi hexfloat C99, di mana ...
mewakili urutan tanpa akhir dari 9's.Konstanta 0.2
dan 0.3
program Anda juga akan menjadi perkiraan nilai sebenarnya. Kebetulan yang terdekat double
dengan 0.2
lebih besar dari bilangan rasional 0.2
tetapi yang paling dekat double
dengan 0.3
lebih kecil dari bilangan rasional 0.3
. Jumlah 0.1
dan 0.2
akhirnya menjadi lebih besar dari angka rasional 0.3
dan karenanya tidak setuju dengan konstanta dalam kode Anda.
Perlakuan yang cukup komprehensif dari masalah aritmatika floating-point adalah Apa Yang Harus Setiap Ilmuwan Komputer Tahu Tentang Aritmatika Floating-Point . Untuk penjelasan yang lebih mudah dicerna, lihat floating-point-gui.de .
Catatan Sisi: Semua sistem angka posisi (base-N) berbagi masalah ini dengan presisi
Angka desimal biasa (basis 10) memiliki masalah yang sama, itulah sebabnya angka seperti 1/3 berakhir sebagai 0,333333333 ...
Anda baru saja menemukan angka (3/10) yang kebetulan mudah direpresentasikan dengan sistem desimal, tetapi tidak cocok dengan sistem biner. Ini berlaku dua arah (sedikit banyak) juga: 1/16 adalah angka jelek dalam desimal (0,0625), tetapi dalam biner kelihatannya rapi seperti yang ke 10.000 dalam desimal (0,0001) ** - jika kita berada di kebiasaan menggunakan sistem nomor-2 basis dalam kehidupan sehari-hari kita, Anda bahkan akan melihat nomor itu dan secara naluriah memahami Anda bisa tiba di sana dengan membagi dua sesuatu, membagi dua itu, dan mengulanginya lagi, dan lagi dan lagi.
** Tentu saja, itu bukan bagaimana angka floating-point disimpan dalam memori (mereka menggunakan bentuk notasi ilmiah). Namun, ia mengilustrasikan poin bahwa kesalahan presisi floating-point biner cenderung muncul karena angka "dunia nyata" yang biasanya kita tertarik untuk bekerja adalah kekuatan sepuluh - tetapi hanya karena kita menggunakan sistem angka desimal hari- hari ini. Ini juga mengapa kita akan mengatakan hal-hal seperti 71% bukannya "5 dari setiap 7" (71% adalah perkiraan, karena 5/7 tidak dapat diwakili persis dengan angka desimal apa pun).
Jadi tidak: angka-angka floating point biner tidak rusak, mereka kebetulan tidak sempurna seperti setiap sistem nomor base-N lainnya :)
Catatan Sisi Samping: Bekerja dengan Mengapung dalam Pemrograman
Dalam praktiknya, masalah presisi ini berarti Anda harus menggunakan fungsi pembulatan untuk membulatkan angka floating point Anda ke sebanyak mungkin tempat desimal yang Anda minati sebelum Anda menampilkannya.
Anda juga perlu mengganti tes kesetaraan dengan perbandingan yang memungkinkan sejumlah toleransi, yang berarti:
Jangan tidak melakukanif (x == y) { ... }
Sebaliknya lakukan if (abs(x - y) < myToleranceValue) { ... }
.
di mana abs
nilai absolut. myToleranceValue
perlu dipilih untuk aplikasi khusus Anda - dan itu akan banyak berkaitan dengan berapa banyak "ruang gerak" yang Anda siapkan untuk memungkinkan, dan berapa jumlah terbesar yang akan Anda bandingkan (karena hilangnya masalah presisi) ). Waspadalah terhadap konstanta gaya "epsilon" dalam bahasa pilihan Anda. Ini tidak boleh digunakan sebagai nilai toleransi.
Saya percaya saya harus menambahkan perspektif perancang perangkat keras untuk ini karena saya merancang dan membangun perangkat keras floating point. Mengetahui asal kesalahan dapat membantu dalam memahami apa yang terjadi dalam perangkat lunak, dan pada akhirnya, saya harap ini membantu menjelaskan alasan mengapa kesalahan floating point terjadi dan tampaknya menumpuk dari waktu ke waktu.
Dari perspektif teknik, sebagian besar operasi floating point akan memiliki beberapa elemen kesalahan karena perangkat keras yang melakukan perhitungan floating point hanya diperlukan memiliki kesalahan kurang dari setengah dari satu unit di tempat terakhir. Oleh karena itu, banyak perangkat keras akan berhenti pada ketelitian yang hanya diperlukan untuk menghasilkan kesalahan kurang dari setengah dari satu unit di tempat terakhir untuk operasi tunggal yang sangat bermasalah dalam divisi floating point. Apa yang merupakan operasi tunggal tergantung pada berapa banyak operan yang diambil unit. Bagi sebagian besar, itu dua, tetapi beberapa unit mengambil 3 atau lebih operan. Karena itu, tidak ada jaminan bahwa operasi yang berulang akan menghasilkan kesalahan yang diinginkan karena kesalahan bertambah seiring waktu.
Sebagian besar prosesor mengikuti standar IEEE-754 tetapi beberapa menggunakan standar denormalized, atau berbeda. Sebagai contoh, ada mode denormalized di IEEE-754 yang memungkinkan representasi angka floating point yang sangat kecil dengan mengorbankan presisi. Namun, yang berikut ini akan mencakup mode normal IEEE-754 yang merupakan mode operasi khas.
Dalam standar IEEE-754, perancang perangkat keras diperbolehkan nilai kesalahan / epsilon selama itu kurang dari setengah dari satu unit di tempat terakhir, dan hasilnya hanya harus kurang dari setengah dari satu unit di yang terakhir tempat untuk satu operasi. Ini menjelaskan mengapa ketika ada operasi berulang, kesalahan bertambah. Untuk presisi ganda IEEE-754, ini adalah bit ke-54, karena 53 bit digunakan untuk mewakili bagian numerik (dinormalisasi), juga disebut mantissa, dari angka floating point (misalnya 5.3 dalam 5.3e5). Bagian selanjutnya membahas lebih rinci tentang penyebab kesalahan perangkat keras pada berbagai operasi floating point.
Penyebab utama kesalahan dalam pembagian floating point adalah algoritma pembagian yang digunakan untuk menghitung hasil bagi. Sebagian besar sistem komputer menghitung pembagian menggunakan perkalian dengan invers, terutama dalam Z=X/Y
,Z = X * (1/Y)
. Pembagian dihitung secara iteratif yaitu setiap siklus menghitung beberapa bit hasil bagi sampai presisi yang diinginkan tercapai, yang untuk IEEE-754 adalah apa saja dengan kesalahan kurang dari satu unit di tempat terakhir. Tabel kebalikan dari Y (1 / Y) dikenal sebagai tabel pemilihan hasil bagi (QST) dalam pembagian yang lambat, dan ukuran dalam bit dari tabel pemilihan hasil bagi biasanya adalah lebar radix, atau sejumlah bit dari hasil bagi yang dihitung dalam setiap iterasi, ditambah beberapa bit penjaga. Untuk standar IEEE-754, presisi ganda (64-bit), itu akan menjadi ukuran radix pembagi, ditambah beberapa bit penjaga k, di mana k>=2
. Jadi misalnya, Tabel Pemilihan Quotient khas untuk pembagi yang menghitung 2 bit hasil bagi pada suatu waktu (radix 4) akan menjadi 2+2= 4
bit (ditambah beberapa bit opsional).
3.1 Kesalahan Pembulatan Divisi: Perkiraan Timbal Balik
Apa yang ada dalam tabel pemilihan hasil bagi tergantung pada metode pembagian : divisi lambat seperti divisi SRT, atau divisi cepat seperti divisi Goldschmidt; setiap entri dimodifikasi sesuai dengan algoritma divisi dalam upaya untuk menghasilkan kesalahan serendah mungkin. Bagaimanapun, dalam semua kasus, semua timbal balik adalah perkiraandari timbal balik yang sebenarnya dan memperkenalkan beberapa elemen kesalahan. Metode pembagian lambat dan pembagian cepat menghitung hasil bagi secara iteratif, yaitu beberapa jumlah bit hasil bagi dihitung setiap langkah, kemudian hasilnya dikurangi dari dividen, dan pembagi mengulangi langkah-langkah sampai kesalahan kurang dari setengah dari satu unit di tempat terakhir. Metode pembagian lambat menghitung jumlah digit hasil bagi pada setiap langkah dan biasanya lebih murah untuk dibangun, dan metode pembagian cepat menghitung jumlah variabel digit per langkah dan biasanya lebih mahal untuk dibangun. Bagian terpenting dari metode pembagian adalah bahwa kebanyakan dari mereka bergantung pada perkalian berulang dengan perkiraan timbal balik, sehingga mereka rentan terhadap kesalahan.
Penyebab lain dari kesalahan pembulatan dalam semua operasi adalah mode pemotongan yang berbeda dari jawaban akhir yang dimungkinkan oleh IEEE-754. Ada truncate, bulat-ke-nol, bulat-ke-terdekat (default), bulat-bawah, dan bulat-atas. Semua metode memperkenalkan elemen kesalahan kurang dari satu unit di tempat terakhir untuk satu operasi. Seiring waktu dan operasi berulang, pemotongan juga menambah kumulatif untuk kesalahan yang dihasilkan. Kesalahan pemotongan ini sangat bermasalah dalam eksponensial, yang melibatkan beberapa bentuk perkalian berulang.
Karena perangkat keras yang melakukan perhitungan floating point hanya perlu menghasilkan hasil dengan kesalahan kurang dari setengah dari satu unit di tempat terakhir untuk satu operasi, kesalahan akan tumbuh selama operasi berulang jika tidak ditonton. Ini adalah alasan bahwa dalam perhitungan yang memerlukan kesalahan terikat, ahli matematika menggunakan metode seperti menggunakan digit genap bulat ke terdekat di tempat terakhir IEEE-754, karena, seiring waktu, kesalahan lebih cenderung untuk saling membatalkan keluar, dan Interval Aritmatika dikombinasikan dengan variasi mode pembulatan IEEE 754untuk memprediksi kesalahan pembulatan, dan memperbaikinya. Karena kesalahan relatifnya yang rendah dibandingkan dengan mode pembulatan lainnya, pembulatan ke digit genap terdekat (di tempat terakhir), adalah mode pembulatan default dari IEEE-754.
Perhatikan bahwa mode pembulatan default, angka genap bulat ke terdekat di tempat terakhir , menjamin kesalahan kurang dari setengah dari satu unit di tempat terakhir untuk satu operasi. Menggunakan pemotongan, pembulatan ke atas, dan pembulatan ke bawah saja dapat menyebabkan kesalahan yang lebih besar dari setengah dari satu unit di tempat terakhir, tetapi kurang dari satu unit di tempat terakhir, sehingga mode ini tidak direkomendasikan kecuali mereka digunakan dalam Aritmatika Interval.
Singkatnya, alasan mendasar untuk kesalahan dalam operasi floating point adalah kombinasi dari pemotongan dalam perangkat keras, dan pemotongan dari suatu timbal balik dalam kasus pembagian. Karena standar IEEE-754 hanya membutuhkan kesalahan kurang dari setengah dari satu unit di tempat terakhir untuk satu operasi, kesalahan floating point atas operasi yang berulang akan bertambah kecuali terkoreksi.
Ketika Anda mengonversi .1 atau 1/10 ke basis 2 (biner) Anda mendapatkan pola berulang setelah titik desimal, sama seperti mencoba untuk mewakili 1/3 di basis 10. Nilainya tidak tepat, dan oleh karena itu Anda tidak dapat melakukan matematika persis dengan itu menggunakan metode floating point normal.
Sebagian besar jawaban di sini menjawab pertanyaan ini dengan istilah yang sangat kering dan teknis. Saya ingin membahas hal ini dalam istilah yang manusia normal dapat mengerti.
Bayangkan Anda mencoba mengiris pizza. Anda memiliki pemotong pizza robot yang dapat memotong irisan pizza persis dua. Itu bisa membagi dua pizza utuh, atau bisa membagi dua irisan yang ada, tetapi dalam hal apapun, setengahnya selalu tepat.
Pemotong pizza itu memiliki gerakan yang sangat baik, dan jika Anda mulai dengan pizza utuh, lalu membagi dua itu, dan terus membagi dua irisan terkecil setiap kali, Anda dapat melakukan separuh hingga 53 kali sebelum irisan terlalu kecil bahkan untuk kemampuan presisi tinggi. . Pada titik itu, Anda tidak lagi dapat membagi dua irisan yang sangat tipis itu, tetapi harus memasukkan atau mengeluarkannya apa adanya.
Sekarang, bagaimana Anda memotong semua irisan sedemikian rupa sehingga akan menambahkan hingga sepersepuluh (0,1) atau seperlima (0,2) pizza? Benar-benar memikirkannya, dan cobalah mengatasinya. Anda bahkan dapat mencoba menggunakan pizza sungguhan, jika Anda memiliki pemotong pizza presisi mitis di tangan. :-)
Kebanyakan programmer berpengalaman, tentu saja, tahu jawaban sebenarnya, yaitu bahwa tidak ada cara untuk menyatukan kepingan tepat sepersepuluh atau seperlima dari pizza menggunakan mereka iris, tidak peduli seberapa halus Anda mengiris mereka. Anda dapat melakukan perkiraan yang cukup baik, dan jika Anda menambahkan perkiraan 0,1 dengan perkiraan 0,2, Anda mendapatkan perkiraan yang cukup baik 0,3, tetapi masih saja itu, perkiraan.
Untuk angka presisi ganda (yang merupakan presisi yang memungkinkan Anda membagi dua pizza Anda sebanyak 53 kali), angka yang segera berkurang dan lebih besar dari 0,1 adalah 0,09999999999999999167332731531132594682276248931884765625 dan 0.100000000000000000000550011151231257827021181583404510 Yang terakhir ini sedikit lebih dekat ke 0,1 dari yang sebelumnya, jadi parser numerik akan, diberi input 0,1, mendukung yang terakhir.
(Perbedaan antara kedua angka itu adalah "irisan terkecil" yang harus kita putuskan untuk dimasukkan, yang menimbulkan bias ke atas, atau mengecualikan, yang menghasilkan bias ke bawah. Istilah teknis untuk irisan terkecil adalah ulp .)
Dalam kasus 0,2, angkanya semuanya sama, hanya ditingkatkan dengan faktor 2. Sekali lagi, kami menyukai nilai yang sedikit lebih tinggi dari 0,2.
Perhatikan bahwa dalam kedua kasus, perkiraan untuk 0,1 dan 0,2 memiliki sedikit bias ke atas. Jika kita menambahkan cukup bias ini, mereka akan mendorong angka lebih jauh dan lebih jauh dari apa yang kita inginkan, dan pada kenyataannya, dalam kasus 0,1 + 0,2, biasnya cukup tinggi sehingga jumlah yang dihasilkan tidak lagi angka terdekat ke 0,3.
Secara khusus, 0,1 + 0,2 benar-benar 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125, sedangkan jumlah yang paling dekat dengan 0,3 sebenarnya 0,299999999999999988897769753748434595763683319091796875.
PS Beberapa bahasa pemrograman juga menyediakan pemotong pizza yang dapat membagi irisan menjadi persepuluh yang tepat . Meskipun pemotong pizza seperti itu tidak umum, jika Anda memiliki akses ke salah satunya, Anda harus menggunakannya ketika penting untuk bisa mendapatkan sepersepuluh atau seperlima dari sepotong.
Kesalahan pembulatan titik mengambang. 0,1 tidak dapat direpresentasikan secara akurat dalam basis-2 seperti pada basis-10 karena faktor prima yang hilang dari 5. Sama seperti 1/3 mengambil jumlah digit tak terbatas untuk mewakili dalam desimal, tetapi "0,1" pada basis-3, 0,1 mengambil jumlah digit tak terbatas di basis-2 di mana ia tidak di basis-10. Dan komputer tidak memiliki jumlah memori yang tak terbatas.
Selain jawaban yang benar lainnya, Anda mungkin ingin mempertimbangkan penskalaan nilai Anda untuk menghindari masalah dengan aritmatika floating-point.
Sebagai contoh:
var result = 1.0 + 2.0; // result === 3.0 returns true
... dari pada:
var result = 0.1 + 0.2; // result === 0.3 returns false
Ekspresi 0.1 + 0.2 === 0.3
kembali false
dalam JavaScript, tapi untungnya aritmatika integer dalam floating-point tepat, sehingga kesalahan representasi desimal dapat dihindari dengan penskalaan.
Sebagai contoh praktis, untuk menghindari masalah floating-point di mana akurasi adalah yang terpenting, disarankan 1 untuk menangani uang sebagai bilangan bulat yang mewakili jumlah sen: 2550
sen, bukan 25.50
dolar.
1 Douglas Crockford: JavaScript: Bagian Yang Baik : Lampiran A - Bagian yang Mengerikan (halaman 105) .
Jawaban saya cukup panjang, jadi saya membaginya menjadi tiga bagian. Karena pertanyaannya adalah tentang floating point matematika, saya telah menekankan apa yang sebenarnya dilakukan mesin. Saya juga membuatnya spesifik untuk menggandakan (64 bit) presisi, tetapi argumennya berlaku sama untuk setiap aritmatika floating point.
Pembukaan
Nomor format biner floating-point (binary64) IEEE 754 presisi ganda mewakili sejumlah formulir
nilai = (-1) ^ s * (1.m 51 m 50 ... m 2 m 1 m 0 ) 2 * 2 e-1023
dalam 64 bit:
1
jika angkanya negatif, 0
jika tidak 1 .1.
selalu 2 diabaikan karena bit paling signifikan dari nilai biner apa pun adalah 1
.1 - IEEE 754 memungkinkan untuk konsep nol yang ditandatangani - +0
dan -0
diperlakukan secara berbeda: 1 / (+0)
infinity positif; 1 / (-0)
adalah infinity negatif. Untuk nilai nol, bit mantissa dan eksponen semuanya nol. Catatan: nilai nol (+0 dan -0) secara eksplisit tidak diklasifikasikan sebagai denormal 2 .
2 - Ini bukan kasus untuk angka-angka denormal , yang memiliki eksponen offset nol (dan tersirat 0.
). Kisaran angka presisi ganda tidak normal adalah d min ≤ | x | ≤ d max , di mana d min (terkecil representable nomor nol) adalah 2 -1.023-51 (≈ 4,94 * 10 -324 ) dan d max (jumlah denormal terbesar, yang mantissa seluruhnya terdiri dari 1
s) adalah 2 -1023 + 1 - 2 -1023 - 51 (≈ 2.225 * 10 -308 ).
Mengubah angka presisi ganda menjadi biner
Banyak konverter online ada untuk mengubah angka floating point presisi ganda menjadi biner (misalnya di binaryconvert.com ), tetapi di sini ada beberapa contoh kode C # untuk mendapatkan representasi IEEE 754 untuk angka presisi ganda (saya memisahkan tiga bagian dengan titik dua ( :
) :
public static string BinaryRepresentation(double value)
{
long valueInLongType = BitConverter.DoubleToInt64Bits(value);
string bits = Convert.ToString(valueInLongType, 2);
string leadingZeros = new string('0', 64 - bits.Length);
string binaryRepresentation = leadingZeros + bits;
string sign = binaryRepresentation[0].ToString();
string exponent = binaryRepresentation.Substring(1, 11);
string mantissa = binaryRepresentation.Substring(12);
return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}
Sampai ke titik: pertanyaan awal
(Lewati ke bawah untuk versi TL; DR)
Cato Johnston (penanya pertanyaan) bertanya mengapa 0,1 + 0,2! = 0,3.
Ditulis dalam biner (dengan titik dua memisahkan tiga bagian), representasi IEEE 754 dari nilai-nilai tersebut adalah:
0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010
Perhatikan bahwa mantissa terdiri dari angka berulang 0011
. Ini adalah kunci mengapa ada kesalahan pada perhitungan - 0,1, 0,2 dan 0,3 tidak dapat diwakili dalam biner tepatnya dalam jumlah bit biner yang terbatas lebih dari 1/9, 1/3 atau 1/7 dapat diwakili secara tepat dalam angka desimal .
Perhatikan juga bahwa kita dapat mengurangi daya dalam eksponen sebanyak 52 dan menggeser titik dalam representasi biner ke kanan sebanyak 52 tempat (seperti 10 -3 * 1.23 == 10 -5 * 123). Ini kemudian memungkinkan kita untuk mewakili representasi biner sebagai nilai tepat yang diwakilinya dalam bentuk a * 2 p . di mana 'a' adalah bilangan bulat.
Mengubah eksponen menjadi desimal, menghapus offset, dan menambahkan kembali yang tersirat 1
(dalam kurung siku), 0,1 dan 0,2 adalah:
0.1 => 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 => 2^-3 * [1].1001100110011001100110011001100110011001100110011010
or
0.1 => 2^-56 * 7205759403792794 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125
Untuk menambahkan dua angka, eksponen harus sama, yaitu:
0.1 => 2^-3 * 0.1100110011001100110011001100110011001100110011001101(0)
0.2 => 2^-3 * 1.1001100110011001100110011001100110011001100110011010
sum = 2^-3 * 10.0110011001100110011001100110011001100110011001100111
or
0.1 => 2^-55 * 3602879701896397 = 0.1000000000000000055511151231257827021181583404541015625
0.2 => 2^-55 * 7205759403792794 = 0.200000000000000011102230246251565404236316680908203125
sum = 2^-55 * 10808639105689191 = 0.3000000000000000166533453693773481063544750213623046875
Karena jumlahnya bukan dari bentuk 2 n * 1. {bbb} kita menambah eksponen dengan satu dan menggeser titik desimal ( biner ) untuk mendapatkan:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
= 2^-54 * 5404319552844595.5 = 0.3000000000000000166533453693773481063544750213623046875
Sekarang ada 53 bit dalam mantissa (yang ke-53 adalah dalam tanda kurung di baris di atas). Mode pembulatan default untuk IEEE 754 adalah ' Round to Nearest ' - yaitu jika angka x jatuh antara dua nilai a dan b , nilai di mana bit paling signifikan adalah nol dipilih.
a = 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
= 2^-2 * 1.0011001100110011001100110011001100110011001100110011
x = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
b = 2^-2 * 1.0011001100110011001100110011001100110011001100110100
= 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125
Perhatikan bahwa a dan b hanya berbeda pada bit terakhir; ...0011
+ 1
= ...0100
. Dalam hal ini, nilai dengan bit nol paling signifikan adalah b , jadi jumlahnya adalah:
sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100
= 2^-54 * 5404319552844596 = 0.3000000000000000444089209850062616169452667236328125
sedangkan representasi biner 0,3 adalah:
0.3 => 2^-2 * 1.0011001100110011001100110011001100110011001100110011
= 2^-54 * 5404319552844595 = 0.299999999999999988897769753748434595763683319091796875
yang hanya berbeda dari representasi biner dari jumlah 0,1 dan 0,2 dengan 2 -54 .
Representasi biner 0,1 dan 0,2 adalah representasi paling akurat dari angka-angka yang diizinkan oleh IEEE 754. Penambahan representasi ini, karena mode pembulatan default, menghasilkan nilai yang berbeda hanya dalam bit-paling-signifikan.
TL; DR
Menulis 0.1 + 0.2
dalam representasi biner IEEE 754 (dengan titik dua memisahkan tiga bagian) dan membandingkannya dengan 0.3
ini, ini adalah (Saya telah memasukkan bit yang berbeda dalam tanda kurung siku):
0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3 => 0:01111111101:0011001100110011001100110011001100110011001100110[011]
Dikonversi kembali ke desimal, nilai-nilai ini adalah:
0.1 + 0.2 => 0.300000000000000044408920985006...
0.3 => 0.299999999999999988897769753748...
Perbedaannya persis 2 -54 , yaitu ~ 5.5511151231258 × 10 -17 - tidak signifikan (untuk banyak aplikasi) jika dibandingkan dengan nilai aslinya.
Membandingkan beberapa bit terakhir dari angka floating point secara inheren berbahaya, karena siapa pun yang membaca " Apa Yang Harus Diketahui Setiap Ilmuwan Komputer Tentang Aritmatika Titik Apung " (yang mencakup semua bagian utama dari jawaban ini) akan tahu.
Sebagian besar kalkulator menggunakan digit penjaga tambahan untuk mengatasi masalah ini, yang adalah bagaimana 0.1 + 0.2
memberi 0.3
: beberapa bit terakhir dibulatkan.
Nomor titik apung yang disimpan di komputer terdiri dari dua bagian, bilangan bulat dan eksponen yang dibawa ke dasar dan dikalikan dengan bagian bilangan bulat.
Jika komputer bekerja di basis 10, 0.1
akan 1 x 10⁻¹
, 0.2
akan 2 x 10⁻¹
, dan 0.3
akan 3 x 10⁻¹
. Matematika bilangan bulat mudah dan tepat, jadi menambahkan 0.1 + 0.2
jelas akan menghasilkan 0.3
.
Komputer biasanya tidak bekerja di basis 10, mereka bekerja di basis 2. Anda masih bisa mendapatkan hasil yang tepat untuk beberapa nilai, misalnya 0.5
ada 1 x 2⁻¹
dan 0.25
sedang 1 x 2⁻²
, dan menambahkannya 3 x 2⁻²
, atau 0.75
. Persis.
Masalahnya datang dengan angka-angka yang dapat diwakili tepat di basis 10, tetapi tidak di basis 2. Angka-angka itu harus dibulatkan ke persamaan terdekatnya. Dengan asumsi format floating point IEEE 64-bit yang sangat umum, nomor terdekat 0.1
adalah 3602879701896397 x 2⁻⁵⁵
, dan nomor terdekat 0.2
adalah 7205759403792794 x 2⁻⁵⁵
; menambahkannya bersama-sama menghasilkan 10808639105689191 x 2⁻⁵⁵
, atau nilai desimal tepat dari 0.3000000000000000444089209850062616169452667236328125
. Nomor titik apung umumnya dibulatkan untuk ditampilkan.
Kesalahan pembulatan titik mengambang. Dari Apa Yang Harus Diketahui Setiap Ilmuwan Tentang Aritmatika Titik Apung :
Meremas bilangan real tak terhingga ke dalam jumlah bit terbatas membutuhkan representasi perkiraan. Meskipun ada banyak bilangan bulat yang tak terhingga, dalam sebagian besar program hasil perhitungan bilangan bulat dapat disimpan dalam 32 bit. Sebaliknya, mengingat jumlah bit tetap apa pun, sebagian besar perhitungan dengan bilangan real akan menghasilkan jumlah yang tidak dapat direpresentasikan secara tepat menggunakan banyak bit tersebut. Oleh karena itu, hasil dari perhitungan titik-mengambang harus sering dibulatkan agar sesuai dengan representasi terbatasnya. Kesalahan pembulatan ini adalah fitur karakteristik perhitungan floating-point.
Banyak jawaban bagus telah diposting, tetapi saya ingin menambahkan satu lagi.
Tidak semua angka dapat direpresentasikan melalui float / doubles Sebagai contoh, angka "0,2" akan direpresentasikan sebagai "0,200000003" dalam presisi tunggal dalam standar IEEE754 float point.
Model untuk menyimpan bilangan real di bawah kap mewakili angka float sebagai
Meskipun Anda dapat mengetik 0.2
dengan mudah, FLT_RADIX
dan DBL_RADIX
2; bukan 10 untuk komputer dengan FPU yang menggunakan "Standar IEEE untuk Bith Floating-Point Arithmetic (ISO / IEEE Std 754-1985)".
Jadi agak sulit untuk menggambarkan angka-angka itu dengan tepat. Bahkan jika Anda menentukan variabel ini secara eksplisit tanpa perhitungan perantara.
Beberapa statistik terkait dengan pertanyaan presisi ganda yang terkenal ini.
Saat menambahkan semua nilai ( a + b ) menggunakan langkah 0,1 (dari 0,1 hingga 100) kami memiliki ~ 15% kemungkinan kesalahan presisi . Perhatikan bahwa kesalahan dapat menghasilkan nilai yang sedikit lebih besar atau lebih kecil. Berikut ini beberapa contohnya:
0.1 + 0.2 = 0.30000000000000004 (BIGGER)
0.1 + 0.7 = 0.7999999999999999 (SMALLER)
...
1.7 + 1.9 = 3.5999999999999996 (SMALLER)
1.7 + 2.2 = 3.9000000000000004 (BIGGER)
...
3.2 + 3.6 = 6.800000000000001 (BIGGER)
3.2 + 4.4 = 7.6000000000000005 (BIGGER)
Ketika mengurangi semua nilai ( a - b di mana a> b ) menggunakan langkah 0,1 (dari 100 menjadi 0,1) kita memiliki ~ 34% kemungkinan kesalahan presisi . Berikut ini beberapa contohnya:
0.6 - 0.2 = 0.39999999999999997 (SMALLER)
0.5 - 0.4 = 0.09999999999999998 (SMALLER)
...
2.1 - 0.2 = 1.9000000000000001 (BIGGER)
2.0 - 1.9 = 0.10000000000000009 (BIGGER)
...
100 - 99.9 = 0.09999999999999432 (SMALLER)
100 - 99.8 = 0.20000000000000284 (BIGGER)
* 15% dan 34% memang besar, jadi selalu gunakan BigDecimal ketika presisi sangat penting. Dengan 2 digit desimal (langkah 0,01) situasinya sedikit lebih buruk (18% dan 36%).
Ringkasan
Aritmatika floating point adalah tepat, sayangnya, itu tidak cocok dengan baik dengan biasa basis-10 nomor representasi kami, jadi ternyata kita sering memberikan masukan yang sedikit off dari apa yang kita tulis.
Bahkan bilangan sederhana seperti 0,01, 0,02, 0,03, 0,04 ... 0,24 tidak dapat direpresentasikan dengan tepat seperti pecahan biner. Jika Anda menghitung 0,01, .02, .03 ..., tidak sampai Anda mencapai 0,25 Anda akan mendapatkan fraksi pertama yang diwakili dalam basis 2 . Jika Anda mencoba menggunakan FP, 0,01 Anda akan sedikit mati, jadi satu-satunya cara untuk menambahkan 25 dari mereka ke tepat 0,25 akan membutuhkan rantai kausalitas panjang yang melibatkan bit penjaga dan pembulatan. Sulit diprediksi sehingga kami angkat tangan dan berkata "FP tidak tepat", tapi itu tidak sepenuhnya benar.
Kami terus-menerus memberikan perangkat keras FP sesuatu yang tampaknya sederhana di basis 10 tetapi merupakan pecahan berulang di basis 2.
Bagaimana ini bisa terjadi?
Ketika kita menulis dalam desimal, setiap fraksi (khususnya, setiap desimal terminating) adalah bilangan rasional dari formulir
a / (2 n x 5 m )
Dalam biner, kita hanya mendapatkan istilah 2 n , yaitu:
a / 2 n
Jadi dalam desimal, kita tidak dapat mewakili 1 / 3 . Karena basis 10 menyertakan 2 sebagai faktor prima, setiap angka yang dapat kita tulis sebagai fraksi biner juga dapat ditulis sebagai fraksi basis 10. Namun, hampir tidak ada apa pun yang kita tulis sebagai basis 10 yang dapat diwakili dalam biner. Dalam kisaran mulai 0,01, 0,02, 0,03 ... 0,99, hanya tiga angka yang dapat direpresentasikan dalam format FP kami: 0,25, 0,50, dan 0,75, karena semuanya 1/4, 1/2, dan 3/4, semua angka dengan faktor prima hanya menggunakan istilah 2 n .
Dalam basis 10 kita tidak dapat mewakili 1 / 3 . Namun dalam biner, kita tidak bisa melakukan 1 / 10 atau 1 / 3 .
Jadi sementara setiap pecahan biner dapat ditulis dalam desimal, kebalikannya tidak benar. Dan pada kenyataannya sebagian besar pecahan desimal berulang dalam biner.
Berhadapan dengannya
Pengembang biasanya diinstruksikan untuk melakukan perbandingan <epsilon , saran yang lebih baik mungkin untuk membulatkan ke nilai-nilai integral (dalam perpustakaan C: round () dan roundf (), yaitu, tetap dalam format FP) dan kemudian membandingkan. Pembulatan ke panjang fraksi desimal spesifik memecahkan sebagian besar masalah dengan output.
Juga, pada masalah angka-angka nyata (masalah-masalah yang ditemukan oleh FP pada komputer-komputer awal, yang sangat mahal), konstanta fisik alam semesta dan semua pengukuran lainnya hanya diketahui oleh angka-angka signifikan yang relatif kecil, sehingga seluruh ruang masalah "tidak eksak". FP "akurasi" bukan masalah dalam aplikasi semacam ini.
Seluruh masalah benar-benar muncul ketika orang mencoba menggunakan FP untuk penghitungan kacang. Itu memang bekerja untuk itu, tetapi hanya jika Anda tetap pada nilai-nilai integral, jenis yang mengalahkan titik menggunakannya. Inilah sebabnya kami memiliki semua pustaka perangkat lunak pecahan desimal itu.
Saya suka jawaban Pizza oleh Chris , karena itu menggambarkan masalah yang sebenarnya, bukan hanya handwaving biasa tentang "ketidaktepatan". Jika FP hanya "tidak akurat", kita bisa memperbaikinya dan akan melakukannya puluhan tahun yang lalu. Alasan kami belum melakukannya adalah karena format FP kompak dan cepat dan ini adalah cara terbaik untuk mengolah banyak angka. Juga, ini adalah warisan dari era ruang dan perlombaan senjata dan upaya awal untuk memecahkan masalah besar dengan komputer yang sangat lambat menggunakan sistem memori kecil. (Kadang-kadang, masing-masing inti magnetik untuk penyimpanan 1-bit, tapi itu cerita lain. )
Kesimpulan
Jika Anda hanya menghitung kacang di bank, solusi perangkat lunak yang menggunakan representasi string desimal di tempat pertama bekerja dengan sangat baik. Tetapi Anda tidak bisa melakukan kuantum chromodinamika atau aerodinamika dengan cara itu.
nextafter()
dengan kenaikan atau penurunan integer pada representasi biner dari pelampung IEEE. Selain itu, Anda dapat membandingkan float sebagai bilangan bulat dan mendapatkan jawaban yang benar kecuali bila keduanya negatif (karena tanda-magnitude vs komplemen 2's).
Apakah Anda mencoba solusi lakban?
Cobalah untuk menentukan kapan kesalahan terjadi dan memperbaikinya dengan pernyataan pendek jika, itu tidak cantik tetapi untuk beberapa masalah itu adalah satu-satunya solusi dan ini adalah salah satunya.
if( (n * 0.1) < 100.0 ) { return n * 0.1 - 0.000000000000001 ;}
else { return n * 0.1 + 0.000000000000001 ;}
Saya memiliki masalah yang sama dalam proyek simulasi ilmiah di c #, dan saya dapat memberitahu Anda bahwa jika Anda mengabaikan efek kupu-kupu itu akan berubah menjadi naga besar dan menggigit Anda di a **
Untuk menawarkan solusi terbaik saya dapat mengatakan saya menemukan metode berikut:
parseFloat((0.1 + 0.2).toFixed(10)) => Will return 0.3
Izinkan saya menjelaskan mengapa itu solusi terbaik. Seperti yang disebutkan di atas, ada baiknya menggunakan fungsi Javascript toFixed () yang siap pakai untuk menyelesaikan masalah. Tetapi kemungkinan besar Anda akan menghadapi beberapa masalah.
Bayangkan Anda akan menambahkan dua angka float seperti 0.2
dan 0.7
di sini adalah: 0.2 + 0.7 = 0.8999999999999999
.
Hasil yang Anda harapkan adalah 0.9
itu berarti Anda membutuhkan hasil dengan ketelitian 1 digit dalam hal ini. Jadi Anda seharusnya menggunakan (0.2 + 0.7).tofixed(1)
tetapi Anda tidak bisa hanya memberikan parameter tertentu untuk toFixed () karena itu tergantung pada angka yang diberikan, misalnya
`0.22 + 0.7 = 0.9199999999999999`
Dalam contoh ini Anda membutuhkan 2 digit presisi sehingga harus demikian toFixed(2)
, jadi apa yang harus paramter agar sesuai dengan setiap angka float yang diberikan?
Anda bisa mengatakan biarlah 10 dalam setiap situasi saat itu:
(0.2 + 0.7).toFixed(10) => Result will be "0.9000000000"
Sial! Apa yang akan Anda lakukan dengan angka nol yang tidak diinginkan setelah jam 9? Saatnya mengubahnya menjadi mengambang agar sesuai keinginan:
parseFloat((0.2 + 0.7).toFixed(10)) => Result will be 0.9
Sekarang setelah Anda menemukan solusinya, lebih baik menawarkannya sebagai fungsi seperti ini:
function floatify(number){
return parseFloat((number).toFixed(10));
}
Mari kita coba sendiri:
function floatify(number){
return parseFloat((number).toFixed(10));
}
function addUp(){
var number1 = +$("#number1").val();
var number2 = +$("#number2").val();
var unexpectedResult = number1 + number2;
var expectedResult = floatify(number1 + number2);
$("#unexpectedResult").text(unexpectedResult);
$("#expectedResult").text(expectedResult);
}
addUp();
input{
width: 50px;
}
#expectedResult{
color: green;
}
#unexpectedResult{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<input id="number1" value="0.2" onclick="addUp()" onkeyup="addUp()"/> +
<input id="number2" value="0.7" onclick="addUp()" onkeyup="addUp()"/> =
<p>Expected Result: <span id="expectedResult"></span></p>
<p>Unexpected Result: <span id="unexpectedResult"></span></p>
Anda dapat menggunakannya dengan cara ini:
var x = 0.2 + 0.7;
floatify(x); => Result: 0.9
Seperti yang disarankan W3SCHOOLS ada solusi lain juga, Anda dapat melipatgandakan dan membagi untuk memecahkan masalah di atas:
var x = (0.2 * 10 + 0.1 * 10) / 10; // x will be 0.3
Ingatlah bahwa (0.2 + 0.1) * 10 / 10
itu tidak akan bekerja sama sekali meskipun tampaknya sama! Saya lebih suka solusi pertama karena saya bisa menerapkannya sebagai fungsi yang mengubah float input ke float output akurat.
Angka-angka aneh itu muncul karena komputer menggunakan sistem angka biner (basis 2) untuk keperluan perhitungan, sementara kami menggunakan desimal (basis 10).
Ada sebagian besar bilangan pecahan yang tidak dapat direpresentasikan secara tepat dalam biner atau dalam desimal atau keduanya. Hasil - Hasil angka yang dibulatkan (tetapi tepat).
Banyak dari banyak duplikat pertanyaan ini bertanya tentang efek pembulatan titik mengambang pada angka tertentu. Dalam praktiknya, lebih mudah untuk merasakan bagaimana cara kerjanya dengan melihat hasil perhitungan bunga yang tepat daripada hanya dengan membacanya. Beberapa bahasa menyediakan cara untuk melakukan itu - seperti mengubah a float
atau double
ke BigDecimal
dalam Java.
Karena ini adalah pertanyaan bahasa-agnostik, maka perlu alat bahasa-agnostik, seperti Decimal to Floating-Point Converter .
Menerapkannya ke angka-angka dalam pertanyaan, diperlakukan sebagai ganda:
0,1 mengonversi menjadi 0,1000000000000000055511151231257827021181583404541015625,
0,2 mengonversi menjadi 0,200000000000000011102230246251565404236316680908203125,
0,3 dikonversi ke 0,299999999999999988897769753748434595763683319091796875, dan
0,3000000000000000000 dikonversi menjadi 0,3000000000000000444089209850062616169452667236328125.
Menambahkan dua angka pertama secara manual atau dalam kalkulator desimal seperti Full Precision Calculator , menunjukkan jumlah yang tepat dari input aktual adalah 0,3000000000000000166533453693773481063544750213623046875.
Jika dibulatkan menjadi setara dengan 0,3 kesalahan pembulatan akan menjadi 0,0000000000000000277555756156289135105907917022705078125. Pembulatan hingga setara dengan 0,30000000000000000004 juga memberikan kesalahan pembulatan 0,000000000000000000277555756156289135105907917022705078125. Pemutus dasi bulat-ke-rata berlaku.
Kembali ke konverter titik mengambang, heksadesimal mentah untuk 0,30000000000000004 adalah 3fd3333333333334, yang berakhir dengan angka genap dan karenanya merupakan hasil yang benar.
Mengingat bahwa tidak ada yang menyebutkan ini ...
Beberapa bahasa tingkat tinggi seperti Python dan Java datang dengan alat untuk mengatasi batasan titik mengambang biner. Sebagai contoh:
decimal
Modul Python dan BigDecimal
kelas Java , yang mewakili angka secara internal dengan notasi desimal (sebagai lawan dari notasi biner). Keduanya memiliki presisi terbatas, sehingga mereka masih rentan kesalahan, namun mereka memecahkan masalah yang paling umum dengan aritmatika floating point biner.
Desimal sangat baik ketika berhadapan dengan uang: sepuluh sen ditambah dua puluh sen selalu persis tiga puluh sen:
>>> 0.1 + 0.2 == 0.3
False
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True
decimal
Modul Python didasarkan pada standar IEEE 854-1987 .
fractions
Modul Python dan BigFraction
kelas Apache Common . Keduanya mewakili bilangan rasional sebagai (numerator, denominator)
pasangan dan mereka dapat memberikan hasil yang lebih akurat daripada aritmatika floating point desimal.
Tidak satu pun dari solusi ini yang sempurna (terutama jika kita melihat kinerja, atau jika kita membutuhkan presisi yang sangat tinggi), tetapi tetap saja mereka memecahkan sejumlah besar masalah dengan aritmatika floating point biner.
Bisakah saya menambahkan; orang selalu menganggap ini sebagai masalah komputer, tetapi jika Anda menghitung dengan tangan Anda (basis 10), Anda tidak bisa mendapatkan (1/3+1/3=2/3)=true
kecuali Anda memiliki infinity untuk menambahkan 0,333 ... menjadi 0,333 ... demikian pula dengan (1/10+2/10)!==3/10
masalah pada basis 2, Anda memotongnya menjadi 0,333 + 0,333 = 0,666 dan mungkin membulatkannya menjadi 0,667 yang juga secara teknis tidak akurat.
Hitung di ternary, dan pertiga bukan masalah - mungkin beberapa ras dengan 15 jari di setiap tangan akan bertanya mengapa matematika desimal Anda rusak ...
Jenis matematika floating-point yang dapat diimplementasikan dalam komputer digital harus menggunakan perkiraan dari bilangan real dan operasi pada mereka. (Versi standar berjalan hingga lebih dari lima puluh halaman dokumentasi dan memiliki komite untuk menangani errata dan penyempurnaan lebih lanjut.)
Perkiraan ini adalah campuran perkiraan dari berbagai jenis, yang masing-masing dapat diabaikan atau diperhitungkan dengan cermat karena cara penyimpangan khusus dari ketelitian. Ini juga melibatkan sejumlah kasus luar biasa eksplisit pada tingkat perangkat keras dan perangkat lunak yang kebanyakan orang jalani saat berpura-pura tidak menyadarinya.
Jika Anda membutuhkan ketelitian tak terbatas (menggunakan angka π, misalnya, alih-alih salah satu dari stand-in yang lebih pendek), Anda harus menulis atau menggunakan program matematika simbolik sebagai gantinya.
Tetapi jika Anda baik-baik saja dengan gagasan bahwa kadang-kadang matematika floating-point tidak jelas dalam nilai dan logika dan kesalahan dapat terakumulasi dengan cepat, dan Anda dapat menulis persyaratan dan tes untuk memungkinkannya, maka kode Anda sering dapat bertahan dengan apa yang ada di dalam FPU Anda.
Hanya untuk bersenang-senang, saya bermain dengan representasi float, mengikuti definisi dari Standard C99 dan saya menulis kode di bawah ini.
Kode mencetak representasi biner dari float dalam 3 kelompok yang terpisah
SIGN EXPONENT FRACTION
dan setelah itu mencetak jumlah, bahwa, ketika dijumlahkan dengan cukup presisi, itu akan menunjukkan nilai yang benar-benar ada dalam perangkat keras.
Jadi ketika Anda menulis float x = 999...
, kompiler akan mengubah angka itu dalam representasi bit yang dicetak oleh fungsi xx
sedemikian sehingga jumlah yang dicetak oleh fungsi yy
sama dengan angka yang diberikan.
Pada kenyataannya, jumlah ini hanya perkiraan. Untuk angka 999.999.999 kompiler akan memasukkan bit representasi float angka 1.000.000.000
Setelah kode saya lampirkan sesi konsol, di mana saya menghitung jumlah istilah untuk kedua konstanta (minus PI dan 999999999) yang benar-benar ada di perangkat keras, dimasukkan di sana oleh kompiler.
#include <stdio.h>
#include <limits.h>
void
xx(float *x)
{
unsigned char i = sizeof(*x)*CHAR_BIT-1;
do {
switch (i) {
case 31:
printf("sign:");
break;
case 30:
printf("exponent:");
break;
case 23:
printf("fraction:");
break;
}
char b=(*(unsigned long long*)x&((unsigned long long)1<<i))!=0;
printf("%d ", b);
} while (i--);
printf("\n");
}
void
yy(float a)
{
int sign=!(*(unsigned long long*)&a&((unsigned long long)1<<31));
int fraction = ((1<<23)-1)&(*(int*)&a);
int exponent = (255&((*(int*)&a)>>23))-127;
printf(sign?"positive" " ( 1+":"negative" " ( 1+");
unsigned int i = 1<<22;
unsigned int j = 1;
do {
char b=(fraction&i)!=0;
b&&(printf("1/(%d) %c", 1<<j, (fraction&(i-1))?'+':')' ), 0);
} while (j++, i>>=1);
printf("*2^%d", exponent);
printf("\n");
}
void
main()
{
float x=-3.14;
float y=999999999;
printf("%lu\n", sizeof(x));
xx(&x);
xx(&y);
yy(x);
yy(y);
}
Berikut adalah sesi konsol di mana saya menghitung nilai nyata float yang ada di perangkat keras. Saya biasa bc
mencetak jumlah persyaratan yang dihasilkan oleh program utama. Satu dapat memasukkan jumlah itu di python repl
atau yang serupa juga.
-- .../terra1/stub
@ qemacs f.c
-- .../terra1/stub
@ gcc f.c
-- .../terra1/stub
@ ./a.out
sign:1 exponent:1 0 0 0 0 0 0 fraction:0 1 0 0 1 0 0 0 1 1 1 1 0 1 0 1 1 1 0 0 0 0 1 1
sign:0 exponent:1 0 0 1 1 1 0 fraction:0 1 1 0 1 1 1 0 0 1 1 0 1 0 1 1 0 0 1 0 1 0 0 0
negative ( 1+1/(2) +1/(16) +1/(256) +1/(512) +1/(1024) +1/(2048) +1/(8192) +1/(32768) +1/(65536) +1/(131072) +1/(4194304) +1/(8388608) )*2^1
positive ( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
-- .../terra1/stub
@ bc
scale=15
( 1+1/(2) +1/(4) +1/(16) +1/(32) +1/(64) +1/(512) +1/(1024) +1/(4096) +1/(16384) +1/(32768) +1/(262144) +1/(1048576) )*2^29
999999999.999999446351872
Itu dia. Nilai 999999999 sebenarnya
999999999.999999446351872
Anda juga dapat memeriksa bc
bahwa -3.14 juga terganggu. Jangan lupa untuk mengatur scale
faktor bc
.
Jumlah yang ditampilkan adalah apa yang ada di dalam perangkat keras. Nilai yang Anda peroleh dengan menghitungnya tergantung pada skala yang Anda tetapkan. Saya memang mengatur scale
faktor ke 15. Secara matematis, dengan ketepatan tak terbatas, tampaknya 1.000.000.000.
Cara lain untuk melihatnya: Digunakan adalah 64 bit untuk mewakili angka. Akibatnya tidak ada cara lebih dari 2 ** 64 = 18.446.744.073.709.551.616 angka yang berbeda dapat diwakili secara tepat.
Namun, Math mengatakan sudah ada banyak desimal tak terhingga antara 0 dan 1. IEE 754 mendefinisikan pengkodean untuk menggunakan 64 bit ini secara efisien untuk ruang angka yang jauh lebih besar plus NaN dan +/- Infinity, sehingga ada kesenjangan antara angka-angka yang diwakili secara akurat yang diisi dengan angka hanya didekati.
Sayangnya 0,3 duduk di celah.
Bayangkan bekerja di basis sepuluh dengan, katakanlah, 8 digit akurasi. Anda memeriksa apakah
1/3 + 2 / 3 == 1
dan belajar bahwa ini kembali false
. Mengapa? Yah, seperti bilangan real yang kita miliki
1/3 = 0,333 .... dan 2/3 = 0,666 ....
Memotong di delapan tempat desimal, kita dapatkan
0.33333333 + 0.66666666 = 0.99999999
yang tentu saja berbeda dari yang 1.00000000
persis 0.00000001
.
Situasi untuk nomor biner dengan jumlah bit tetap persis analog. Sebagai bilangan real, kami punya
1/10 = 0,0001100110011001100 ... (basis 2)
dan
1/5 = 0,0011001100110011001 ... (basis 2)
Jika kita memotong ini menjadi, katakanlah, tujuh bit, maka kita akan mendapatkannya
0.0001100 + 0.0011001 = 0.0100101
sementara di sisi lain,
3/10 = 0,01001100110011 ... (basis 2)
yang, terpotong menjadi tujuh bit, adalah 0.0100110
, dan ini berbeda persis 0.0000001
.
Situasi tepatnya sedikit lebih halus karena angka-angka ini biasanya disimpan dalam notasi ilmiah. Jadi, misalnya, alih-alih menyimpan 1/10 karena 0.0001100
kami dapat menyimpannya sebagai sesuatu 1.10011 * 2^-4
, tergantung pada berapa banyak bit yang telah kami alokasikan untuk eksponen dan mantissa. Ini memengaruhi berapa banyak digit presisi yang Anda dapatkan untuk perhitungan Anda.
Hasilnya adalah bahwa karena kesalahan pembulatan ini Anda pada dasarnya tidak pernah ingin menggunakan == pada angka floating-point. Sebagai gantinya, Anda dapat memeriksa apakah nilai absolut dari selisihnya lebih kecil daripada beberapa angka kecil tetap.
Karena Python 3.5 Anda dapat menggunakan math.isclose()
fungsi untuk menguji perkiraan persamaan:
>>> import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
>>> 0.1 + 0.2 == 0.3
False
Karena utas ini bercabang sedikit menjadi diskusi umum tentang implementasi floating point saat ini, saya akan menambahkan bahwa ada proyek untuk memperbaiki masalah mereka.
Lihatlah https://posithub.org/ misalnya, yang menampilkan tipe nomor yang disebut posit (dan pendahulunya unum) yang berjanji untuk menawarkan akurasi yang lebih baik dengan bit yang lebih sedikit. Jika pemahaman saya benar, itu juga memperbaiki jenis masalah dalam pertanyaan. Proyek yang cukup menarik, orang di belakangnya adalah ahli matematika itu Dr. John Gustafson . Semuanya adalah open source, dengan banyak implementasi aktual di C / C ++, Python, Julia dan C # ( https://hastlayer.com/arithmetics ).
Ini sebenarnya cukup sederhana. Ketika Anda memiliki sistem basis 10 (seperti sistem kami), itu hanya dapat mengekspresikan pecahan yang menggunakan faktor utama basis. Faktor prima dari 10 adalah 2 dan 5. Jadi 1/2, 1/4, 1/5, 1/8, dan 1/10 semuanya dapat dinyatakan dengan bersih karena penyebut semuanya menggunakan faktor prima 10. Sebaliknya, 1 / 3, 1/6, dan 1/7 semuanya desimal berulang karena penyebutnya menggunakan faktor prima 3 atau 7. Dalam biner (atau basis 2), satu-satunya faktor prima adalah 2. Jadi Anda hanya dapat mengekspresikan pecahan dengan rapi yang hanya mengandung 2 sebagai faktor utama. Dalam biner, 1/2, 1/4, 1/8 semuanya akan dinyatakan dengan jelas sebagai desimal. Sementara, 1/5 atau 1/10 akan mengulangi desimal. Jadi 0,1 dan 0,2 (1/10 dan 1/5) saat membersihkan desimal dalam sistem basis 10, mengulangi desimal dalam sistem basis 2 yang dioperasikan komputer. Ketika Anda melakukan matematika pada desimal berulang ini,
Angka desimal seperti 0.1
, 0.2
, dan 0.3
tidak diwakili tepat dalam biner dikodekan tipe floating point. Jumlah perkiraan untuk 0.1
dan 0.2
berbeda dari perkiraan yang digunakan untuk 0.3
, maka kepalsuan 0.1 + 0.2 == 0.3
seperti dapat dilihat lebih jelas di sini:
#include <stdio.h>
int main() {
printf("0.1 + 0.2 == 0.3 is %s\n", 0.1 + 0.2 == 0.3 ? "true" : "false");
printf("0.1 is %.23f\n", 0.1);
printf("0.2 is %.23f\n", 0.2);
printf("0.1 + 0.2 is %.23f\n", 0.1 + 0.2);
printf("0.3 is %.23f\n", 0.3);
printf("0.3 - (0.1 + 0.2) is %g\n", 0.3 - (0.1 + 0.2));
return 0;
}
Keluaran:
0.1 + 0.2 == 0.3 is false
0.1 is 0.10000000000000000555112
0.2 is 0.20000000000000001110223
0.1 + 0.2 is 0.30000000000000004440892
0.3 is 0.29999999999999998889777
0.3 - (0.1 + 0.2) is -5.55112e-17
Agar perhitungan ini dapat dievaluasi lebih andal, Anda perlu menggunakan representasi berbasis desimal untuk nilai floating point. Standar C tidak menentukan tipe seperti itu secara default tetapi sebagai ekstensi yang dijelaskan dalam Laporan teknis .
Tipe _Decimal32
, _Decimal64
dan _Decimal128
mungkin tersedia di sistem Anda (misalnya, GCC mendukungnya pada target yang dipilih , tetapi Dentang tidak mendukungnya pada OS X ).
Math.sum (javascript) .... jenis penggantian operator
.1 + .0001 + -.1 --> 0.00010000000000000286
Math.sum(.1 , .0001, -.1) --> 0.0001
Object.defineProperties(Math, {
sign: {
value: function (x) {
return x ? x < 0 ? -1 : 1 : 0;
}
},
precision: {
value: function (value, precision, type) {
var v = parseFloat(value),
p = Math.max(precision, 0) || 0,
t = type || 'round';
return (Math[t](v * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
}
},
scientific_to_num: { // this is from https://gist.github.com/jiggzson
value: function (num) {
//if the number is in scientific notation remove it
if (/e/i.test(num)) {
var zero = '0',
parts = String(num).toLowerCase().split('e'), //split into coeff and exponent
e = parts.pop(), //store the exponential part
l = Math.abs(e), //get the number of zeros
sign = e / l,
coeff_array = parts[0].split('.');
if (sign === -1) {
num = zero + '.' + new Array(l).join(zero) + coeff_array.join('');
} else {
var dec = coeff_array[1];
if (dec)
l = l - dec.length;
num = coeff_array.join('') + new Array(l + 1).join(zero);
}
}
return num;
}
}
get_precision: {
value: function (number) {
var arr = Math.scientific_to_num((number + "")).split(".");
return arr[1] ? arr[1].length : 0;
}
},
sum: {
value: function () {
var prec = 0, sum = 0;
for (var i = 0; i < arguments.length; i++) {
prec = this.max(prec, this.get_precision(arguments[i]));
sum += +arguments[i]; // force float to convert strings to number
}
return Math.precision(sum, prec);
}
}
});
idenya adalah menggunakan Matematika sebagai gantinya operator untuk menghindari kesalahan float
Math.sum secara otomatis mendeteksi ketepatan untuk digunakan
Math.sum menerima sejumlah argumen
Pertimbangkan hasil berikut:
error = (2**53+1) - int(float(2**53+1))
>>> (2**53+1) - int(float(2**53+1))
1
Kita dapat dengan jelas melihat breakpoint ketika 2**53+1
- semua berfungsi dengan baik sampai 2**53
.
>>> (2**53) - int(float(2**53))
0
Ini terjadi karena biner presisi ganda: IEEE 754 format biner presisi ganda biner: binary64
Dari halaman Wikipedia untuk format floating-point presisi ganda :
Floating-point biner presisi ganda adalah format yang umum digunakan pada PC, karena jangkauannya yang lebih luas daripada floating point presisi tunggal, terlepas dari kinerja dan biaya bandwidth. Seperti dengan format floating-point presisi tunggal, ia tidak memiliki presisi pada bilangan bulat jika dibandingkan dengan format bilangan bulat dengan ukuran yang sama. Umumnya dikenal sebagai double. Standar IEEE 754 menentukan binary64 memiliki:
- Tanda bit: 1 bit
- Eksponen: 11 bit
- Presisi yang signifikan: 53 bit (52 disimpan secara eksplisit)
Nilai riil yang diasumsikan oleh datum presisi ganda 64-bit yang diberikan dengan eksponen yang bias dan fraksi 52-bit adalah
atau
Terima kasih kepada @a_guest karena menunjukkannya kepada saya.
Pertanyaan berbeda telah dinamai duplikat untuk pertanyaan ini:
Di C ++, mengapa hasil cout << x
berbeda dari nilai yang ditunjukkan oleh debugger x
?
The x
dalam pertanyaan adalah float
variabel.
Salah satu contohnya
float x = 9.9F;
Debugger menunjukkan 9.89999962
, output dari cout
operasi 9.9
.
Jawabannya ternyata adalah cout
presisi default untuk float
adalah 6, sehingga membulatkan ke 6 angka desimal.
Lihat di sini untuk referensi