Apakah aman untuk memeriksa nilai floating point untuk kesetaraan ke 0?

100

Saya tahu Anda tidak dapat mengandalkan persamaan antara nilai tipe ganda atau desimal secara normal, tetapi saya bertanya-tanya apakah 0 adalah kasus khusus.

Meskipun saya dapat memahami ketidaktepatan antara 0,000000000001 dan 0,0000000000000002, 0 sendiri tampaknya cukup sulit untuk dikacaukan karena tidak ada artinya. Jika Anda tidak tepat dalam hal apa pun, itu bukan apa-apa lagi.

Tapi saya tidak tahu banyak tentang topik ini jadi bukan hak saya untuk mengatakannya.

double x = 0.0;
return (x == 0.0) ? true : false;

Akankah itu selalu benar?

Gene Roberts
sumber
69
Operator terner berlebihan dalam kode itu :)
Joel Coehoorn
5
LOL kamu benar. Pergi aku
Gene Roberts
Saya tidak akan melakukannya karena Anda tidak tahu bagaimana x disetel ke nol. Jika Anda masih ingin melakukannya, Anda mungkin ingin membulatkan atau lantai x untuk menyingkirkan 1e-12 atau semacamnya yang mungkin ditandai di akhir.
Rex Logan

Jawaban:

115

Hal ini aman untuk mengharapkan bahwa perbandingan akan kembali truejika dan hanya jika variabel ganda memiliki nilai tepat 0.0(yang pada potongan kode asli Anda, tentu saja, kasus ini). Ini konsisten dengan semantik ==operator. a == bberarti " asama dengan b".

Tidaklah aman (karena tidak benar ) untuk mengharapkan bahwa hasil dari beberapa kalkulasi akan menjadi nol dalam aritmatika ganda (atau lebih umum, floating point) setiap kali hasil kalkulasi yang sama dalam Matematika murni adalah nol. Ini karena ketika kalkulasi dilakukan di lapangan, kesalahan presisi floating point muncul - sebuah konsep yang tidak ada dalam aritmatika bilangan riil dalam Matematika.

Daniel Daranas
sumber
51

Jika Anda perlu melakukan banyak perbandingan "kesetaraan", mungkin ada baiknya untuk menulis sedikit fungsi pembantu atau metode ekstensi di .NET 3.5 untuk membandingkan:

public static bool AlmostEquals(this double double1, double double2, double precision)
{
    return (Math.Abs(double1 - double2) <= precision);
}

Ini dapat digunakan dengan cara berikut:

double d1 = 10.0 * .1;
bool equals = d1.AlmostEquals(0.0, 0.0000001);
Dirk Vollmar
sumber
4
Anda mungkin mengalami kesalahan pembatalan subtraktif dengan membandingkan double1 dan double2, jika angka-angka ini memiliki nilai yang sangat dekat satu sama lain. Saya akan menghapus Math.Abs ​​dan memeriksa setiap cabang secara individual d1> = d2 - e dan d1 <= d2 + e
Theodore Zographos
"Karena Epsilon mendefinisikan ekspresi minimum dari nilai positif yang jangkauannya mendekati nol, margin perbedaan antara dua nilai yang serupa harus lebih besar dari Epsilon. Biasanya, ini berkali-kali lebih besar daripada Epsilon. Oleh karena itu, kami menyarankan Anda untuk melakukannya tidak menggunakan Epsilon saat membandingkan nilai ganda untuk kesetaraan. " - msdn.microsoft.com/en-gb/library/ya2zha7s(v=vs.110).aspx
Rafael Costa
15

Untuk sampel sederhana Anda, pengujian itu tidak masalah. Tapi bagaimana dengan ini:

bool b = ( 10.0 * .1 - 1.0 == 0.0 );

Ingatlah bahwa .1 adalah desimal berulang dalam biner dan tidak dapat direpresentasikan dengan tepat. Kemudian bandingkan dengan kode ini:

double d1 = 10.0 * .1; // make sure the compiler hasn't optimized the .1 issue away
bool b = ( d1 - 1.0 == 0.0 );

Saya biarkan Anda menjalankan pengujian untuk melihat hasil yang sebenarnya: Anda lebih mungkin mengingatnya seperti itu.

Joel Coehoorn
sumber
5
Sebenarnya, ini mengembalikan nilai true untuk beberapa alasan (di LINQPad, setidaknya).
Alexey Romanov
Apa "masalah .1" yang Anda bicarakan?
Teejay
14

Dari entri MSDN untuk Double.Equals :

Presisi dalam Perbandingan

Metode Equals harus digunakan dengan hati-hati, karena dua nilai yang tampaknya setara dapat menjadi tidak sama karena presisi yang berbeda dari kedua nilai tersebut. Contoh berikut melaporkan bahwa nilai ganda .3333 dan Ganda dikembalikan dengan membagi 1 dengan 3 tidak sama.

...

Daripada membandingkan kesetaraan, satu teknik yang direkomendasikan melibatkan penentuan margin perbedaan yang dapat diterima antara dua nilai (seperti 0,01% dari salah satu nilai). Jika nilai absolut dari perbedaan antara kedua nilai kurang dari atau sama dengan margin tersebut, perbedaan tersebut kemungkinan besar disebabkan oleh perbedaan presisi dan, oleh karena itu, nilainya cenderung sama. Contoh berikut menggunakan teknik ini untuk membandingkan 0,33333 dan 1/3, dua nilai ganda yang ditemukan contoh kode sebelumnya tidak sama.

Juga, lihat Double.Epsilon .

Stu Mackellar
sumber
1
Mungkin juga untuk nilai yang tidak cukup setara untuk dibandingkan sebagai sama. Orang akan berharap bahwa jika x.Equals(y), maka (1/x).Equals(1/y), tetapi bukan itu masalahnya jika xadalah 0dan yadalah 1/Double.NegativeInfinity. Nilai-nilai itu dinyatakan setara, meski timbal baliknya tidak.
supercat
@supercat: Mereka setara. Dan mereka tidak memiliki timbal balik. Anda dapat menjalankan pengujian lagi dengan x = 0dan y = 0, dan Anda masih akan menemukannya 1/x != 1/y.
Ben Voigt
@ BenVoigt: Dengan xdan ysebagai tipe double? Bagaimana Anda membandingkan hasil agar laporannya tidak sama? Perhatikan bahwa 1 / 0,0 bukan NaN.
supercat
@supercat: Ok, itu salah satu hal yang membuat IEEE-754 salah. (Pertama, itu 1.0/0.0gagal menjadi NaN sebagaimana mestinya, karena batasnya tidak unik. Kedua, bahwa ketidakterbatasan dibandingkan satu sama lain, tanpa memperhatikan derajat ketidakterbatasan)
Ben Voigt
@BenVoigt: Jika nol adalah hasil perkalian dua bilangan yang sangat kecil, maka membagi 1,0 menjadi bilangan yang akan menghasilkan nilai yang lebih besar dari bilangan apa pun yang memiliki tanda yang sama, dan lebih kecil dari bilangan apa pun jika salah satu dari bilangan kecil angka memiliki tanda yang berlawanan. IMHO, IEEE-754 akan lebih baik jika memiliki unsigned zero, tetapi infinitesimals positif dan negatif.
supercat
6

Masalahnya muncul saat Anda membandingkan berbagai jenis implementasi nilai floating point, misalnya membandingkan float dengan double. Tapi dengan tipe yang sama, seharusnya tidak menjadi masalah.

float f = 0.1F;
bool b1 = (f == 0.1); //returns false
bool b2 = (f == 0.1F); //returns true

Masalahnya, programmer terkadang lupa bahwa implisit type cast (double to float) terjadi untuk perbandingan dan hasilnya menjadi bug.

Yogee
sumber
3

Jika nomor itu langsung ditetapkan ke float atau double maka aman untuk menguji nol atau bilangan bulat apa pun yang dapat direpresentasikan dalam 53 bit untuk double atau 24 bit untuk float.

Atau dengan kata lain Anda selalu dapat menetapkan dan nilai integer menjadi ganda dan kemudian membandingkan kembali ganda ke integer yang sama dan dijamin itu akan sama.

Anda juga dapat memulai dengan menetapkan bilangan bulat dan membuat perbandingan sederhana terus bekerja dengan tetap menambahkan, mengurangi, atau mengalikan dengan bilangan bulat (dengan asumsi hasilnya kurang dari 24 bit untuk float abd 53 bit untuk double). Jadi, Anda dapat memperlakukan pelampung dan ganda sebagai bilangan bulat dalam kondisi terkontrol tertentu.

Kevin Gale
sumber
Saya setuju dengan pernyataan Anda secara umum (dan upvoted itu) tapi saya percaya itu benar-benar tergantung apakah implementasi floating point IEEE 754 digunakan atau tidak. Dan saya percaya setiap komputer "modern" menggunakan IEEE 754, setidaknya untuk penyimpanan pelampung (ada aturan pembulatan aneh yang berbeda).
Mark Lakata
2

Tidak, ini tidak baik. Apa yang disebut nilai denormalisasi (subnormal), jika dibandingkan dengan 0,0, akan dibandingkan sebagai salah (bukan nol), tetapi bila digunakan dalam persamaan akan dinormalisasi (menjadi 0,0). Jadi, menggunakan ini sebagai mekanisme untuk menghindari pembagian-dengan-nol tidaklah aman. Sebagai gantinya, tambahkan 1.0 dan bandingkan dengan 1.0. Ini akan memastikan bahwa semua subnormal diperlakukan sebagai nol.


sumber
Subnormal juga dikenal sebagai denormals
Manuel
Subnormal tidak menjadi sama dengan nol saat digunakan, meskipun mereka mungkin atau mungkin tidak menghasilkan hasil yang sama tergantung pada operasi yang tepat.
wnoise
-2

Coba ini, dan Anda akan menemukan bahwa == tidak dapat diandalkan untuk double / float.
double d = 0.1 + 0.2; bool b = d == 0.3;

Inilah jawaban dari Quora.

rickyuu
sumber
-4

Sebenarnya, saya pikir lebih baik menggunakan kode berikut untuk membandingkan nilai ganda dengan 0,0:

double x = 0.0;
return (Math.Abs(x) < double.Epsilon) ? true : false;

Sama untuk float:

float x = 0.0f;
return (Math.Abs(x) < float.Epsilon) ? true : false;
David.Chu.ca
sumber
5
Tidak. Dari dokumen dari double.Epsilon: "Jika Anda membuat algoritme khusus yang menentukan apakah dua bilangan floating-point dapat dianggap sama, Anda harus menggunakan nilai yang lebih besar dari konstanta Epsilon untuk menetapkan margin perbedaan absolut yang dapat diterima agar kedua nilai dianggap sama. (Biasanya, margin perbedaan itu berkali-kali lebih besar dari Epsilon.) "
Alastair Maw
1
@AlastairMaw ini berlaku untuk memeriksa kesetaraan pada dua ukuran ganda berapa pun. Untuk memeriksa persamaan ke nol, double.Epsilon baik-baik saja.
jwg
4
Tidak, tidak . Sangat mungkin bahwa nilai yang Anda dapatkan melalui beberapa perhitungan berkali-kali lipat epsilon dari nol, tetapi tetap harus dianggap sebagai nol. Anda tidak secara ajaib mencapai sejumlah presisi ekstra dalam hasil antara Anda dari suatu tempat, hanya karena hasilnya mendekati nol.
Alastair Maw
4
Sebagai contoh: (1.0 / 5.0 + 1.0 / 5.0 - 1.0 / 10.0 - 1.0 / 10.0 - 1.0 / 10.0 - 1.0 / 10.0) <double.Epsilon == false (dan dalam hal besaran: 2.78E-17 vs 4.94E -324)
Alastair Maw
jadi, apa presisi yang direkomendasikan, jika double.Epsilon tidak ok? Akankah 10 kali epsilon baik-baik saja? 100 kali?
liang