Mengapa menambahkan 0,1 beberapa kali tetap tanpa kerugian?

152

Saya tahu angka 0.1desimal tidak dapat diwakili secara tepat dengan angka biner yang terbatas ( penjelasan ), sehingga double n = 0.1akan kehilangan presisi dan tidak akan persis 0.1. Di sisi lain 0.5dapat diwakili tepat karena itu 0.5 = 1/2 = 0.1b.

Setelah mengatakan bahwa dapat dimengerti bahwa menambahkan 0.1 tiga kali tidak akan memberikan dengan tepat 0.3sehingga kode berikut dicetak false:

double sum = 0, d = 0.1;
for (int i = 0; i < 3; i++)
    sum += d;
System.out.println(sum == 0.3); // Prints false, OK

Tapi bagaimana mungkin menambahkan 0.1 lima kali akan memberi dengan tepat 0.5? Kode berikut dicetak true:

double sum = 0, d = 0.1;
for (int i = 0; i < 5; i++)
    sum += d;
System.out.println(sum == 0.5); // Prints true, WHY?

Jika 0.1tidak dapat direpresentasikan secara tepat, bagaimana cara menambahkannya 5 kali dengan tepat 0.5yang dapat direpresentasikan dengan tepat?

icza
sumber
7
Jika Anda benar-benar meneliti itu, saya yakin Anda bisa mengetahuinya, tetapi floating point sarat dengan "kejutan", dan kadang-kadang lebih baik hanya melihat dengan heran.
Hot Licks
3
Anda memikirkan hal ini dengan cara yang matematis. Floating point aritmetics bukanlah matematika dengan cara apa pun.
Jakob
13
@HotLicks yang sangat banyak sikap yang salah untuk memiliki.
hobbs
2
@RussellBorogove bahkan jika itu dioptimalkan jauh, itu hanya akan menjadi optimasi yang valid jika summemiliki nilai akhir yang sama seperti jika loop benar-benar dieksekusi. Dalam standar C ++ ini disebut "as-jika aturan" atau "perilaku yang dapat diamati sama".
hobbs
7
@ Jakob tidak benar sama sekali. Aritmatika titik-mengambang didefinisikan dengan ketat, dengan perlakuan matematika yang baik untuk batas kesalahan dan semacamnya. Hanya saja banyak programmer yang tidak mau menindaklanjuti analisis, atau mereka secara keliru percaya bahwa "floating-point is inexact" adalah semua yang perlu diketahui dan analisis itu tidak layak untuk diganggu.
hobbs

Jawaban:

155

Kesalahan pembulatan tidak acak dan cara penerapannya berupaya meminimalkan kesalahan. Ini berarti bahwa kadang-kadang kesalahan tidak terlihat, atau tidak ada kesalahan.

Misalnya 0.1tidak persis 0.1yaitu new BigDecimal("0.1") < new BigDecimal(0.1)tetapi 0.5persis1.0/2

Program ini menunjukkan kepada Anda nilai-nilai sebenarnya yang terlibat.

BigDecimal _0_1 = new BigDecimal(0.1);
BigDecimal x = _0_1;
for(int i = 1; i <= 10; i ++) {
    System.out.println(i+" x 0.1 is "+x+", as double "+x.doubleValue());
    x = x.add(_0_1);
}

cetakan

0.1000000000000000055511151231257827021181583404541015625, as double 0.1
0.2000000000000000111022302462515654042363166809082031250, as double 0.2
0.3000000000000000166533453693773481063544750213623046875, as double 0.30000000000000004
0.4000000000000000222044604925031308084726333618164062500, as double 0.4
0.5000000000000000277555756156289135105907917022705078125, as double 0.5
0.6000000000000000333066907387546962127089500427246093750, as double 0.6000000000000001
0.7000000000000000388578058618804789148271083831787109375, as double 0.7000000000000001
0.8000000000000000444089209850062616169452667236328125000, as double 0.8
0.9000000000000000499600361081320443190634250640869140625, as double 0.9
1.0000000000000000555111512312578270211815834045410156250, as double 1.0

Catatan: itu 0.3sedikit mati, tetapi ketika Anda mendapatkan 0.4bit harus bergeser ke bawah agar sesuai dengan batas 53-bit dan kesalahan dibuang. Sekali lagi, kesalahan merayap kembali untuk 0.6dan 0.7tetapi untuk 0.8ke 1.0kesalahan dibuang.

Menambahkannya 5 kali harus mengumpulkan kesalahan, bukan membatalkannya.

Alasan ada kesalahan adalah karena presisi yang terbatas. yaitu 53-bit. Ini berarti bahwa ketika angka menggunakan bit lebih banyak karena semakin besar, bit harus dijatuhkan dari ujung. Ini menyebabkan pembulatan yang dalam hal ini menguntungkan Anda.
Anda bisa mendapatkan efek sebaliknya ketika mendapatkan angka yang lebih kecil, misalnya 0.1-0.0999=> 1.0000000000000286E-4 dan Anda melihat lebih banyak kesalahan daripada sebelumnya.

Contoh dari ini adalah mengapa di Java 6 Mengapa Math.round (0.49999999999999994) kembali 1 Dalam hal ini hilangnya sedikit dalam perhitungan menghasilkan perbedaan besar untuk jawabannya.

Peter Lawrey
sumber
1
Di mana ini diterapkan?
EpicPandaForce
16
@Zhuinden CPU mengikuti standar IEEE-754. Java memberi Anda akses ke instruksi CPU yang mendasarinya dan tidak terlibat. en.wikipedia.org/wiki/IEEE_floating_point
Peter Lawrey
10
@PeterLawrey: Tidak harus CPU. Pada mesin tanpa floating point di CPU (dan tidak ada FPU terpisah yang digunakan), aritmatika IEEE akan dilakukan oleh perangkat lunak. Dan jika CPU host memiliki floating point tetapi tidak sesuai dengan persyaratan IEEE, saya pikir implementasi Java untuk CPU itu akan diwajibkan untuk menggunakan soft float juga ...
R .. GitHub STOP HELPING ICE
1
@ R .. dalam hal ini saya tidak tahu apa yang akan terjadi jika Anda menggunakan strictfp Waktu untuk mempertimbangkan integer titik tetap saya pikir. (atau BigDecimal)
Peter Lawrey
2
@ Eugene masalah utama adalah nilai-nilai terbatas yang bisa diwakili oleh floating point. Keterbatasan ini dapat mengakibatkan hilangnya informasi dan seiring bertambahnya jumlah kesalahan. Ini menggunakan pembulatan tetapi dalam kasus ini, dibulatkan ke bawah sehingga apa yang akan menjadi angka yang sedikit terlalu besar karena 0,1 sedikit terlalu besar, berubah menjadi nilai yang benar. Tepat 0,5
Peter Lawrey
47

Pembatasan overflow, dalam floating-point, x + x + xadalah persis angka floating-point yang dibulatkan dengan benar (yaitu terdekat) dengan 3 * yang sebenarnya x, x + x + x + xtepat 4 * x, dan x + x + x + x + xsekali lagi merupakan perkiraan titik mengambang yang dibulatkan dengan benar untuk 5 * x.

Hasil pertama, karena x + x + x, berasal dari fakta yang x + xtepat. x + x + xdengan demikian hasil dari hanya satu pembulatan.

Hasil kedua lebih sulit, satu demonstrasi itu dibahas di sini (dan Stephen Canon menyinggung bukti lain dengan analisis kasus pada 3 digit terakhir x). Untuk meringkas, 3 * xberada di binade yang sama dengan 2 * xatau di binade yang sama dengan 4 * x, dan dalam setiap kasus adalah mungkin untuk menyimpulkan bahwa kesalahan pada penambahan ketiga membatalkan kesalahan pada penambahan kedua ( Tambahan pertama adalah tepat, seperti yang sudah kami katakan).

Hasil ketiga, " x + x + x + x + xdibulatkan dengan benar", berasal dari yang kedua dengan cara yang sama dengan yang pertama berasal dari ketepatan x + x.


Hasil kedua menjelaskan mengapa 0.1 + 0.1 + 0.1 + 0.1persis angka floating-point 0.4: bilangan rasional 1/10 dan 4/10 dapat didekati dengan cara yang sama, dengan kesalahan relatif yang sama, ketika dikonversi ke floating-point. Angka floating-point ini memiliki rasio tepat 4 di antara mereka. Hasil pertama dan ketiga menunjukkan bahwa 0.1 + 0.1 + 0.1dan 0.1 + 0.1 + 0.1 + 0.1 + 0.1dapat diperkirakan memiliki lebih sedikit kesalahan daripada yang dapat disimpulkan dengan analisis kesalahan naif, tetapi, dalam diri mereka sendiri, mereka hanya mengaitkan hasilnya dengan masing 3 * 0.1- masing dan 5 * 0.1, yang dapat diharapkan dekat tetapi tidak selalu identik dengan 0.3dan 0.5.

Jika Anda terus menambahkan 0.1setelah penambahan keempat, Anda akhirnya akan mengamati kesalahan pembulatan yang membuat " 0.1ditambahkan ke dirinya sendiri n kali" menyimpang dari n * 0.1, dan bahkan menyimpang lebih dari n / 10. Jika Anda memplot nilai "0,1 ditambahkan ke dirinya sendiri n kali" sebagai fungsi dari n, Anda akan mengamati garis kemiringan konstan oleh binades (segera setelah hasil penambahan n ditakdirkan untuk jatuh ke dalam binade tertentu, sifat-sifat tambahan dapat diharapkan mirip dengan penambahan sebelumnya yang menghasilkan hasil dalam binade yang sama). Di dalam binade yang sama, kesalahan akan tumbuh atau menyusut. Jika Anda melihat urutan lereng dari binade ke binade, Anda akan mengenali angka berulang dari0.1dalam biner untuk sementara waktu. Setelah itu, penyerapan akan mulai terjadi dan kurva akan mendatar.

Pascal Cuoq
sumber
1
Pada baris pertama Anda mengatakan bahwa x + x + x benar-benar benar, tetapi dari contoh dalam pertanyaan itu tidak.
Alboz
2
@Alboz saya katakan itu x + x + xpersis angka floating-point yang dibulatkan dengan benar ke 3 * x. "Dibulatkan dengan benar" berarti "terdekat" dalam konteks ini.
Pascal Cuoq
4
+1 Ini harus menjadi jawaban yang diterima. Ini sebenarnya menawarkan penjelasan / bukti tentang apa yang terjadi dan bukan hanya generalisasi yang tidak jelas.
R .. GitHub BERHENTI MEMBANTU ICE
1
@Alboz (semuanya dibayangkan oleh pertanyaan). Tetapi apa yang dijelaskan oleh jawaban ini adalah bagaimana kesalahan secara kebetulan dibatalkan daripada bertambah secara terburuk.
hobbs
1
@chebus 0,1 adalah 0x1.999999999999999999999 ... p-4 dalam heksadesimal (urutan digit tanpa batas). Diperkirakan dalam presisi ganda sebagai 0x1.99999ap-4. 0,2 adalah 0x1.999999999999999999999 ... p-3 dalam heksadesimal. Untuk alasan yang sama bahwa 0,1 diperkirakan sebagai 0x1.99999ap-4, 0,2 diperkirakan sebagai 0x1.99999ap-3. Sementara itu, 0x1.99999ap-3 juga tepat 0x1.99999ap-4 + 0x1.99999ap-4.
Pascal Cuoq
-1

Sistem floating point melakukan berbagai keajaiban termasuk memiliki beberapa bit ekstra presisi untuk pembulatan. Jadi kesalahan yang sangat kecil karena representasi tidak tepat dari 0,1 akhirnya dibulatkan menjadi 0,5.

Pikirkan floating point sebagai cara yang hebat tetapi INEXACT untuk mewakili angka. Tidak semua angka yang mungkin dengan mudah direpresentasikan dalam komputer. Bilangan irasional seperti PI. Atau seperti SQRT (2). (Sistem matematika simbolik dapat mewakili mereka, tetapi saya memang mengatakan "dengan mudah".)

Nilai floating point mungkin sangat dekat, tetapi tidak tepat. Mungkin sangat dekat sehingga Anda dapat menavigasi ke Pluto dan pergi dengan milimeter. Tetapi masih belum tepat dalam arti matematika.

Jangan gunakan floating point saat Anda harus tepat daripada perkiraan. Misalnya, aplikasi akuntansi ingin melacak dengan tepat jumlah uang tertentu dalam akun. Integer baik untuk itu karena mereka tepat. Masalah utama yang perlu Anda perhatikan dengan bilangan bulat adalah overflow.

Menggunakan BigDecimal untuk mata uang berfungsi dengan baik karena representasi yang mendasarinya adalah bilangan bulat, meskipun besar.

Menyadari bahwa angka floating point tidak tepat, mereka masih memiliki banyak kegunaan. Koordinasikan sistem untuk navigasi atau koordinat dalam sistem grafis. Nilai-nilai astronomi. Nilai-nilai ilmiah. (Anda mungkin tidak bisa mengetahui massa pasti bola dalam massa elektron, jadi ketidaktepatannya tidak terlalu penting.)

Untuk menghitung aplikasi (termasuk akuntansi) gunakan integer. Untuk menghitung jumlah orang yang melewati gerbang, gunakan int atau panjang.

DannyB
sumber
2
Pertanyaannya ditandai [java]. Definisi bahasa Java tidak memiliki ketentuan untuk "beberapa bit ekstra presisi", hanya untuk beberapa bit eksponen tambahan (dan itu hanya jika Anda tidak menggunakan strictfp). Hanya karena Anda telah menolak untuk memahami sesuatu tidak berarti itu tidak terduga atau bahwa orang lain harus menolak untuk memahaminya. Lihat stackoverflow.com/questions/18496560 sebagai contoh panjang implementasi Java untuk menerapkan definisi bahasa (yang tidak termasuk ketentuan untuk bit presisi ekstra atau, dengan strictfp, untuk bit exp tambahan)
Pascal Cuoq