Seberapa berbahaya membandingkan nilai floating point?

391

Saya tahu UIKitpenggunaan CGFloatkarena sistem koordinat independen resolusi.

Tapi setiap kali saya ingin memeriksa apakah misalnya frame.origin.xadalah 0itu membuat saya merasa sakit:

if (theView.frame.origin.x == 0) {
    // do important operation
}

Tidak CGFloatrentan terhadap positif palsu ketika membandingkan dengan ==, <=, >=, <, >? Ini adalah titik mengambang dan mereka memiliki masalah keputusan: 0.0000000000041misalnya.

Apakah Objective-Cmenangani ini secara internal ketika membandingkan atau dapatkah hal origin.xyang berbunyi nol tidak dibandingkan dengan yang 0benar?

Anggota yang Bangga
sumber

Jawaban:

466

Pertama-tama, nilai floating point tidak "acak" dalam perilaku mereka. Perbandingan yang tepat dapat dan memang masuk akal dalam banyak penggunaan di dunia nyata. Tetapi jika Anda akan menggunakan floating point, Anda perlu mengetahui cara kerjanya. Kesalahan di sisi asumsi floating point berfungsi seperti bilangan real akan membuat Anda mendapatkan kode yang cepat rusak. Kesalahan di sisi dengan asumsi hasil floating point memiliki fuzz acak besar yang terkait dengan mereka (seperti kebanyakan jawaban di sini menyarankan) akan membuat Anda kode yang tampaknya bekerja pada awalnya tetapi akhirnya memiliki kesalahan besar-besaran dan kasus sudut rusak.

Pertama-tama, jika Anda ingin memprogram dengan floating point, Anda harus membaca ini:

Yang Harus Diketahui Setiap Ilmuwan Komputer Tentang Aritmatika Titik Apung

Ya, baca semua itu. Jika itu terlalu banyak beban, Anda harus menggunakan bilangan bulat / titik tetap untuk perhitungan Anda sampai Anda punya waktu untuk membacanya. :-)

Sekarang, dengan itu, masalah terbesar dengan perbandingan floating point yang tepat turun ke:

  1. Fakta bahwa banyak nilai yang dapat Anda tulis di sumbernya, atau dibaca dengan scanfatau strtod, tidak ada sebagai nilai floating point dan dikonversi secara diam-diam ke perkiraan terdekat. Inilah yang dibicarakan jawaban iblis9733.

  2. Fakta bahwa banyak hasil bulat karena tidak memiliki cukup presisi untuk mewakili hasil yang sebenarnya. Contoh mudah di mana Anda dapat melihat ini adalah menambahkan x = 0x1fffffedan y = 1mengapung. Di sini, xmemiliki 24 bit presisi dalam mantissa (ok) dan yhanya 1 bit, tetapi ketika Anda menambahkannya, bit mereka tidak berada di tempat yang tumpang tindih, dan hasilnya akan membutuhkan 25 bit presisi. Sebaliknya, ia dibulatkan (ke0x2000000 dalam mode pembulatan default).

  3. Fakta bahwa banyak hasil bulat karena membutuhkan banyak tempat tanpa batas untuk nilai yang benar. Ini mencakup kedua hasil rasional seperti 1/3 (yang Anda kenal dari desimal di mana ia mengambil banyak tempat tak terhingga) tetapi juga 1/10 (yang juga mengambil banyak tempat tak terhingga dalam biner, karena 5 bukan kekuatan 2), serta hasil irasional seperti akar kuadrat dari apa pun yang bukan kuadrat sempurna.

  4. Pembulatan ganda. Pada beberapa sistem (terutama x86), ekspresi floating point dievaluasi dengan presisi lebih tinggi daripada tipe nominalnya. Ini berarti bahwa ketika salah satu dari jenis pembulatan di atas terjadi, Anda akan mendapatkan dua langkah pembulatan, pertama pembulatan hasil ke jenis presisi yang lebih tinggi, kemudian pembulatan ke jenis akhir. Sebagai contoh, perhatikan apa yang terjadi dalam desimal jika Anda membulatkan 1,49 ke bilangan bulat (1), dibandingkan apa yang terjadi jika Anda membulatkannya ke satu tempat desimal (1,5) kemudian bulatkan hasilnya menjadi bilangan bulat (2). Ini sebenarnya adalah salah satu area yang paling menjijikkan untuk ditangani dalam floating point, karena perilaku kompiler (terutama untuk kompiler buggy, non-conforming seperti GCC) tidak dapat diprediksi.

  5. Fungsi transendental ( trig, exp, log, dll) tidak ditentukan untuk memiliki hasil benar bulat; hasilnya hanya ditentukan untuk menjadi benar dalam satu unit di tempat terakhir presisi (biasanya disebut sebagai 1ulp ).

Saat Anda menulis kode floating point, Anda perlu mengingat apa yang Anda lakukan dengan angka-angka yang dapat menyebabkan hasil menjadi tidak tepat, dan membuat perbandingan yang sesuai. Sering kali masuk akal untuk membandingkan dengan "epsilon", tetapi epsilon itu harus didasarkan pada besarnya angka yang Anda bandingkan , bukan konstanta absolut. (Dalam kasus di mana epsilon konstan konstan akan bekerja, itu sangat menunjukkan bahwa titik tetap, bukan titik mengambang, adalah alat yang tepat untuk pekerjaan itu!)

Sunting: Secara khusus, cek epsilon dengan magnitudo relatif akan terlihat seperti:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

Di mana FLT_EPSILONkonstanta dari float.h(ganti dengan DBL_EPSILONuntuk doubles atau LDBL_EPSILONuntuk long doubles) dan Kadalah konstanta yang Anda pilih sedemikian rupa sehingga akumulasi kesalahan perhitungan Anda pasti dibatasi oleh Kunit di tempat terakhir (dan jika Anda tidak yakin Anda mendapatkan kesalahan perhitungan terikat benar, buat Kbeberapa kali lebih besar dari apa yang seharusnya perhitungan Anda katakan).

Akhirnya, perhatikan bahwa jika Anda menggunakan ini, beberapa perawatan khusus mungkin diperlukan mendekati nol, karena FLT_EPSILONtidak masuk akal untuk penyangkalan. Perbaikan cepat akan membuatnya:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

dan juga gantikan DBL_MINjika menggunakan ganda.

R .. GitHub BERHENTI MEMBANTU ICE
sumber
25
fabs(x+y)bermasalah jika xdan y(dapat) memiliki tanda yang berbeda. Namun, jawaban yang bagus terhadap gelombang perbandingan pemujaan kargo.
Daniel Fischer
27
Jika xdan ymemiliki tanda yang berbeda, itu tidak masalah. Sisi kanan akan "terlalu kecil", tetapi karena xdan ymemiliki tanda yang berbeda, mereka tidak boleh membandingkan sama saja. (Kecuali jika mereka sangat kecil sehingga tidak normal, tetapi kemudian kasus kedua menangkapnya)
R .. GitHub BERHENTI MEMBANTU ICE
4
Saya ingin tahu tentang pernyataan Anda: "terutama untuk kompiler buggy, non-conformant seperti GCC". Apakah benar-benar GCC buggy dan juga tidak sesuai?
Nicolás Ozimica
3
Karena pertanyaan ini ditandai iOS, perlu dicatat bahwa kompiler Apple (baik clang dan build gcc Apple) selalu menggunakan FLT_EVAL_METHOD = 0, dan berusaha untuk sepenuhnya ketat untuk tidak membawa kelebihan presisi. Jika Anda menemukan pelanggaran itu, silakan ajukan laporan bug.
Stephen Canon
17
"Pertama-tama, nilai floating point tidak" acak "dalam perilaku mereka. Perbandingan yang tepat dapat dan memang masuk akal dalam banyak penggunaan dunia nyata." - Hanya dua kalimat dan sudah mendapatkan +1! Itu salah satu kesalahpahaman paling mengganggu yang dibuat orang ketika bekerja dengan floating point.
Christian Rau
36

Karena 0 benar-benar direpresentasikan sebagai angka floating-point IEEE754 (atau menggunakan implementasi lain dari nomor fp yang pernah saya kerjakan) perbandingan dengan 0 mungkin aman. Namun, Anda mungkin tergigit jika program Anda menghitung nilai (seperti theView.frame.origin.x) yang Anda yakini bernilai 0 tetapi perhitungan Anda tidak dapat menjamin 0.

Untuk sedikit memperjelas, perhitungan seperti:

areal = 0.0

akan (kecuali bahasa atau sistem Anda rusak) membuat nilai sehingga (areal == 0,0) mengembalikan true tetapi perhitungan lain seperti

areal = 1.386 - 2.1*(0.66)

mungkin tidak.

Jika Anda dapat meyakinkan diri sendiri bahwa perhitungan Anda menghasilkan nilai yang 0 (dan bukan hanya bahwa mereka menghasilkan nilai yang seharusnya 0) maka Anda dapat melanjutkan dan membandingkan nilai fp dengan 0. Jika Anda tidak dapat meyakinkan diri Anda sendiri ke tingkat yang diperlukan , berpegang teguh pada pendekatan biasa 'kesetaraan yang ditoleransi'.

Dalam kasus terburuk, perbandingan nilai-nilai fp yang ceroboh bisa sangat berbahaya: pikirkan avionik, panduan senjata, operasi pembangkit listrik, navigasi kendaraan, hampir semua aplikasi yang perhitungannya memenuhi dunia nyata.

Bagi Angry Birds, tidak begitu berbahaya.

Tanda Kinerja Tinggi
sumber
11
Sebenarnya, 1.30 - 2*(0.65)adalah contoh sempurna dari ekspresi yang jelas mengevaluasi ke 0,0 jika kompiler Anda mengimplementasikan IEEE 754, karena ganda direpresentasikan sebagai 0.65dan 1.30memiliki signifikansi yang sama, dan perkalian dengan dua jelas tepat.
Pascal Cuoq
7
Masih mendapatkan perwakilan dari yang ini, jadi saya mengubah cuplikan contoh kedua.
Tanda Kinerja Tinggi
22

Saya ingin memberikan jawaban yang sedikit berbeda dari yang lain. Mereka bagus untuk menjawab pertanyaan Anda sebagaimana dinyatakan tetapi mungkin tidak untuk apa yang perlu Anda ketahui atau apa masalah sebenarnya Anda.

Titik apung dalam grafik baik-baik saja! Tetapi hampir tidak perlu membandingkan pelampung secara langsung. Mengapa Anda perlu melakukan itu? Grafik menggunakan float untuk menentukan interval. Dan membandingkan jika float berada dalam interval juga didefinisikan oleh float selalu didefinisikan dengan baik dan hanya perlu konsisten, tidak akurat atau tepat! Selama piksel (yang juga merupakan interval!) Dapat ditetapkan itu semua kebutuhan grafis.

Jadi jika Anda ingin menguji apakah poin Anda di luar rentang [0..width [ini baik-baik saja. Pastikan Anda mendefinisikan inklusi secara konsisten. Misalnya selalu mendefinisikan di dalam adalah (x> = 0 && x <lebar). Hal yang sama berlaku untuk tes persimpangan atau tekan.

Namun, jika Anda menyalahgunakan koordinat grafik sebagai semacam bendera, seperti misalnya untuk melihat apakah jendela merapat atau tidak, Anda tidak boleh melakukan ini. Gunakan bendera boolean yang terpisah dari lapisan presentasi grafik.

starmole
sumber
13

Membandingkan dengan nol dapat menjadi operasi yang aman, selama nol bukan nilai yang dihitung (seperti yang disebutkan dalam jawaban di atas). Alasan untuk ini adalah bahwa nol adalah angka yang dapat direpresentasikan secara sempurna dalam floating point.

Berbicara nilai-nilai yang dapat direpresentasikan dengan sempurna, Anda mendapatkan rentang 24 bit dalam konsep power-of-two (presisi tunggal). Jadi 1, 2, 4 dapat direpresentasikan dengan sempurna, seperti juga 0,5, 0,25, dan 0,125. Selama semua bit penting Anda dalam 24-bit, Anda adalah emas. Jadi 10.625 dapat diwakili secara tepat.

Ini bagus, tetapi akan cepat hancur di bawah tekanan. Dua skenario muncul dalam pikiran: 1) Ketika suatu perhitungan dilibatkan. Jangan percaya bahwa sqrt (3) * sqrt (3) == 3. Tidak akan seperti itu. Dan itu mungkin tidak akan berada dalam epsilon, seperti yang disarankan oleh beberapa jawaban lainnya. 2) Ketika ada non-power-of-2 (NPOT) yang terlibat. Jadi mungkin terdengar aneh, tetapi 0,1 adalah deret tak terbatas dalam biner dan karenanya perhitungan apa pun yang melibatkan angka seperti ini akan tidak tepat sejak awal.

(Oh dan pertanyaan awal menyebutkan perbandingan dengan nol. Jangan lupa bahwa -0.0 juga merupakan nilai floating-point yang valid.)

JHumphrey
sumber
11

['Jawaban yang benar' lebih menarik daripada memilih K. Memilih Kakhirnya sama ad-hoc seperti memilih VISIBLE_SHIFTtetapi memilih Kkurang jelas karena tidak seperti VISIBLE_SHIFTitu tidak didasarkan pada properti tampilan. Jadi pilih racun Anda - pilih Katau pilih VISIBLE_SHIFT. Jawaban ini menganjurkan pemilihan VISIBLE_SHIFTdan kemudian menunjukkan kesulitan dalam memilihK ]

Justru karena kesalahan bulat, Anda tidak boleh menggunakan perbandingan nilai 'tepat' untuk operasi logis. Dalam kasus spesifik Anda tentang posisi pada tampilan visual, tidak mungkin apakah posisi tersebut 0,0 atau 0,0000000003 - perbedaannya tidak terlihat oleh mata. Jadi logika Anda harus seperti:

#define VISIBLE_SHIFT    0.0001        // for example
if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

Namun, pada akhirnya, 'tidak terlihat oleh mata' akan tergantung pada properti tampilan Anda. Jika Anda dapat membatasi tampilan (Anda seharusnya bisa); lalu pilih VISIBLE_SHIFTmenjadi bagian dari batas atas itu.

Sekarang, 'jawaban yang benar' terletak di atas, Kjadi mari kita jelajahi memilih K. 'Jawaban yang benar' di atas mengatakan:

K adalah konstanta yang Anda pilih sedemikian rupa sehingga akumulasi kesalahan perhitungan Anda pasti dibatasi oleh unit K di tempat terakhir (dan jika Anda tidak yakin Anda mendapatkan perhitungan batas kesalahan yang benar, buat K beberapa kali lebih besar dari apa yang perhitungan Anda katakan itu seharusnya)

Jadi kita butuh K. Jika mendapatkan Klebih sulit, kurang intuitif daripada memilih saya VISIBLE_SHIFTmaka Anda akan memutuskan apa yang cocok untuk Anda. Untuk menemukan Kkita akan menulis sebuah program pengujian yang terlihat pada sekelompokK nilai sehingga kita dapat melihat bagaimana perilakunya. Seharusnya jelas bagaimana memilih K, jika 'jawaban yang tepat' dapat digunakan. Tidak?

Kita akan menggunakan, sebagai rincian 'jawaban yang benar':

if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

Mari kita coba semua nilai K:

#include <math.h>
#include <float.h>
#include <stdio.h>

void main (void)
{
  double x = 1e-13;
  double y = 0.0;

  double K = 1e22;
  int i = 0;

  for (; i < 32; i++, K = K/10.0)
    {
      printf ("K:%40.16lf -> ", K);

      if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
        printf ("YES\n");
      else
        printf ("NO\n");
    }
}
ebg@ebg$ gcc -o test test.c
ebg@ebg$ ./test
K:10000000000000000000000.0000000000000000 -> YES
K: 1000000000000000000000.0000000000000000 -> YES
K:  100000000000000000000.0000000000000000 -> YES
K:   10000000000000000000.0000000000000000 -> YES
K:    1000000000000000000.0000000000000000 -> YES
K:     100000000000000000.0000000000000000 -> YES
K:      10000000000000000.0000000000000000 -> YES
K:       1000000000000000.0000000000000000 -> NO
K:        100000000000000.0000000000000000 -> NO
K:         10000000000000.0000000000000000 -> NO
K:          1000000000000.0000000000000000 -> NO
K:           100000000000.0000000000000000 -> NO
K:            10000000000.0000000000000000 -> NO
K:             1000000000.0000000000000000 -> NO
K:              100000000.0000000000000000 -> NO
K:               10000000.0000000000000000 -> NO
K:                1000000.0000000000000000 -> NO
K:                 100000.0000000000000000 -> NO
K:                  10000.0000000000000000 -> NO
K:                   1000.0000000000000000 -> NO
K:                    100.0000000000000000 -> NO
K:                     10.0000000000000000 -> NO
K:                      1.0000000000000000 -> NO
K:                      0.1000000000000000 -> NO
K:                      0.0100000000000000 -> NO
K:                      0.0010000000000000 -> NO
K:                      0.0001000000000000 -> NO
K:                      0.0000100000000000 -> NO
K:                      0.0000010000000000 -> NO
K:                      0.0000001000000000 -> NO
K:                      0.0000000100000000 -> NO
K:                      0.0000000010000000 -> NO

Ah, jadi K harus 1e16 atau lebih besar jika saya ingin 1e-13 menjadi 'nol'.

Jadi, saya katakan Anda memiliki dua opsi:

  1. Lakukan perhitungan epsilon sederhana menggunakan penilaian teknik Anda untuk nilai 'epsilon', seperti yang saya sarankan. Jika Anda melakukan grafik dan 'nol' dimaksudkan sebagai 'perubahan yang terlihat' daripada memeriksa aset visual Anda (gambar, dll) dan menilai epsilon apa yang bisa.
  2. Jangan mencoba perhitungan floating point sampai Anda membaca referensi jawaban non-kargo-kultus (dan dapatkan gelar Ph.D dalam prosesnya) dan kemudian gunakan penilaian non-intuitif untuk memilih K.
GoZoner
sumber
10
Salah satu aspek dari resolusi-independensi adalah bahwa Anda tidak dapat memastikan dengan pasti apa "pergeseran yang terlihat" pada saat kompilasi. Apa yang tidak terlihat pada layar super-HD mungkin sangat jelas pada layar kecil. Satu setidaknya harus membuatnya menjadi fungsi ukuran layar. Atau beri nama yang lain.
Romain
1
Tetapi setidaknya memilih 'shift tampak' didasarkan pada properti tampilan (atau bingkai) yang mudah dipahami - tidak seperti <jawaban benar> Kyang sulit dan tidak intuitif untuk dipilih.
GoZoner
5

Pertanyaan yang benar: bagaimana cara membandingkan poin di Cocoa Touch?

Jawaban yang benar: CGPointEqualToPoint ().

Pertanyaan yang berbeda: Apakah dua nilai yang dihitung sama?

Jawabannya diposting di sini: Tidak.

Bagaimana cara mengecek apakah mereka sudah dekat? Jika Anda ingin memeriksa apakah mereka dekat, maka jangan gunakan CGPointEqualToPoint (). Tapi, jangan periksa untuk melihat apakah mereka sudah dekat. Lakukan sesuatu yang masuk akal di dunia nyata, seperti memeriksa untuk melihat apakah suatu titik berada di luar garis atau jika suatu titik berada di dalam bola.

Michael T.
sumber
4

Terakhir kali saya memeriksa standar C, tidak ada persyaratan untuk operasi floating point pada ganda (total 64 bit, 53 bit mantissa) agar akurat hingga lebih dari presisi itu. Namun, beberapa perangkat keras mungkin melakukan operasi dalam register dengan presisi yang lebih besar, dan persyaratan itu ditafsirkan berarti tidak ada persyaratan untuk menghapus bit urutan yang lebih rendah (di luar presisi dari angka yang dimuat ke dalam register). Jadi Anda bisa mendapatkan hasil perbandingan yang tidak terduga seperti ini tergantung pada apa yang tersisa di register dari siapa pun yang tidur di sana.

Yang mengatakan, dan meskipun upaya saya untuk menghapusnya setiap kali saya melihatnya, pakaian tempat saya bekerja memiliki banyak kode C yang dikompilasi menggunakan gcc dan berjalan di linux, dan kami belum melihat hasil yang tak terduga ini dalam waktu yang sangat lama . Saya tidak tahu apakah ini karena gcc sedang membersihkan bit orde rendah untuk kita, register 80-bit tidak digunakan untuk operasi ini pada komputer modern, standar telah diubah, atau apa. Saya ingin tahu apakah ada yang bisa mengutip pasal dan ayat.

Lucas Membrane
sumber
1

Anda dapat menggunakan kode tersebut untuk membandingkan float dengan nol:

if ((int)(theView.frame.origin.x * 100) == 0) {
    // do important operation
}

Ini akan dibandingkan dengan akurasi 0,1, itu cukup untuk CGFloat dalam kasus ini.

Igor
sumber
Masuk ke inttanpa mengasuransikan theView.frame.origin.xberada dalam / dekat kisaran intmengarah ke perilaku tidak terdefinisi (UB) - atau dalam hal ini, 1/100 kisaran int.
chux
Sama sekali tidak ada alasan untuk mengkonversi ke integer seperti ini. Seperti dikatakan chux, ada potensi untuk UB dari nilai-nilai di luar jangkauan; dan pada beberapa arsitektur ini akan jauh lebih lambat daripada hanya melakukan perhitungan di floating point. Terakhir, mengalikan dengan 100 seperti itu akan membandingkan dengan presisi 0,01, bukan 0,1.
Sneftel
0
-(BOOL)isFloatEqual:(CGFloat)firstValue secondValue:(CGFloat)secondValue{

BOOL isEqual = NO;

NSNumber *firstValueNumber = [NSNumber numberWithDouble:firstValue];
NSNumber *secondValueNumber = [NSNumber numberWithDouble:secondValue];

isEqual = [firstValueNumber isEqualToNumber:secondValueNumber];

return isEqual;

}

Abbas Mulani
sumber
0

Saya menggunakan fungsi perbandingan berikut untuk membandingkan sejumlah tempat desimal:

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}
denim
sumber
-6

Saya akan mengatakan hal yang benar adalah dengan menyatakan setiap angka sebagai objek, dan kemudian mendefinisikan tiga hal dalam objek itu: 1) operator kesetaraan. 2) metode setAcceptableDifference. 3) nilai itu sendiri. Operator persamaan mengembalikan nilai true jika perbedaan absolut dari dua nilai kurang dari nilai yang ditetapkan dapat diterima.

Anda dapat subklas objek agar sesuai dengan masalah. Misalnya, batang bundar logam antara 1 dan 2 inci mungkin dianggap berdiameter sama jika diameternya berbeda kurang dari 0,0001 inci. Jadi, Anda akan memanggil setAcceptableDifference dengan parameter 0,0001, dan kemudian menggunakan operator kesetaraan dengan percaya diri.

John White
sumber
1
Ini Bukan Jawaban Yang Baik. Pertama, seluruh "objek" tidak melakukan apa pun untuk menyelesaikan masalah Anda. Dan kedua, implementasi aktual Anda dari "kesetaraan" sebenarnya tidak benar.
Tom Swirly
3
Tom, mungkin Anda akan berpikir lagi tentang "benda benda". Dengan bilangan real, direpresentasikan dengan presisi tinggi, kesetaraan jarang terjadi. Tapi ide kesetaraan seseorang bisa disesuaikan jika cocok untuk Anda. Akan lebih baik jika ada operator 'kira-kira sama' yang dapat ditimpa, tetapi tidak ada.
John White