Bandingkan dobel dengan nol menggunakan epsilon


214

Hari ini, saya melihat-lihat beberapa kode C ++ (ditulis oleh orang lain) dan menemukan bagian ini:

double someValue = ...
if (someValue <  std::numeric_limits<double>::epsilon() && 
    someValue > -std::numeric_limits<double>::epsilon()) {
  someValue = 0.0;
}

Saya mencoba mencari tahu apakah ini masuk akal.

Dokumentasi untuk epsilon()mengatakan:

Fungsi mengembalikan selisih antara 1 dan nilai terkecil lebih besar dari 1 yang dapat direpresentasikan [oleh gandakan].

Apakah ini berlaku untuk 0 juga, yaitu epsilon()apakah nilai terkecil lebih besar dari 0? Atau adakah angka antara 0dan 0 + epsilonyang dapat diwakili oleh a double?

Jika tidak, bukankah perbandingannya setara dengan someValue == 0.0?


3
Epsilon sekitar 1 kemungkinan besar akan jauh lebih tinggi dari itu sekitar 0, jadi mungkin akan ada nilai antara 0 dan 0 + epsilon_at_1. Saya kira penulis bagian ini ingin menggunakan sesuatu yang kecil, tetapi ia tidak ingin menggunakan konstanta sihir, jadi ia hanya menggunakan nilai dasarnya yang sewenang-wenang ini.
enobayram

2
Membandingkan angka Floating point sangat sulit, dan penggunaan nilai epsilon atau ambang batas bahkan dianjurkan. Silakan merujuk: cs.princeton.edu/introcs/91float dan cygnus-software.com/papers/comparingfloats/comparingfloats.htm
Aditya Kumar Pandey

40
Link pertama adalah 403.99999999
graham.reeds

6
IMO, dalam hal ini penggunaannya numeric_limits<>::epsilonmenyesatkan dan tidak relevan. Yang kita inginkan adalah mengasumsikan 0 jika nilai aktual berbeda tidak lebih dari beberapa ε dari 0. Dan ε harus dipilih berdasarkan spesifikasi masalah, bukan pada nilai yang tergantung pada mesin. Saya menduga bahwa epsilon saat ini tidak berguna, karena bahkan hanya beberapa operasi FP dapat mengakumulasi kesalahan yang lebih besar dari itu.
Andrey Vihrov

1
+1. epsilon bukan yang sekecil mungkin tetapi dapat melayani tujuan yang diberikan dalam sebagian besar tugas-tugas teknik praktis jika Anda tahu presisi apa yang Anda butuhkan dan apa yang Anda lakukan.
SChepurin

Jawaban:


192

Dengan asumsi 64-bit IEEE double, ada mantissa 52-bit dan eksponen 11-bit. Mari kita hancurkan menjadi bit:

1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^0 = 1

Angka keterwakilan terkecil yang lebih besar dari 1:

1.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^0 = 1 + 2^-52

Karena itu:

epsilon = (1 + 2^-52) - 1 = 2^-52

Apakah ada angka antara 0 dan epsilon? Banyak ... Misalnya angka minimum positif representable (normal) adalah:

1.0000 00000000 00000000 00000000 00000000 00000000 00000000 × 2^-1022 = 2^-1022

Bahkan ada (1022 - 52 + 1)×2^52 = 4372995238176751616angka antara 0 dan epsilon, yang merupakan 47% dari semua angka positif yang dapat diwakili ...


27
Sangat aneh sehingga Anda dapat mengatakan "47% dari angka positif" :)
konfigurator

13
@configurator: Nah, Anda tidak bisa mengatakan itu (tidak ada ukuran terbatas 'alami'). Tetapi Anda dapat mengatakan "47% dari angka representatif positif ".
Yakov Galka

1
@ybungalobill saya tidak bisa mengetahuinya. Eksponen memiliki 11 bit: 1 bit tanda dan 10 bit nilai. Mengapa 2 ^ -1022 dan bukan 2 ^ -1024 adalah angka positif terkecil?
Pavlo Dyban

3
@PavloDyban: hanya karena eksponen tidak memiliki bit tanda. Mereka dikodekan sebagai offset: jika eksponen yang dikodekan 0 <= e < 2048maka mantissa dikalikan 2 dengan kekuatan e - 1023. Misalnya eksponen 2^0dikodekan sebagai e=1023, 2^1as e=1024dan 2^-1022as e=1. Nilai e=0dicadangkan untuk subnormal dan nol nyata.
Yakov Galka

2
@PavloDyban: juga 2^-1022merupakan angka normal terkecil . Jumlah terkecil sebenarnya 0.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^-1022 = 2^-1074. Ini subnormal, artinya bagian mantissa lebih kecil dari 1, sehingga dikodekan dengan eksponen e=0.
Yakov Galka

17

Tesnya tentu tidak sama dengan someValue == 0. Seluruh ide angka floating-point adalah bahwa mereka menyimpan eksponen dan signifikan. Oleh karena itu mereka mewakili nilai dengan angka presisi signifikan biner tertentu (53 dalam kasus ganda IEEE). Nilai yang diwakili jauh lebih padat di dekat 0 daripada di dekat 1.

Untuk menggunakan sistem desimal yang lebih dikenal, misalkan Anda menyimpan nilai desimal "ke 4 angka penting" dengan eksponen. Maka selanjutnya representable nilai lebih besar dari 1yang 1.001 * 10^0, dan epsilonadalah 1.000 * 10^-3. Tetapi 1.000 * 10^-4juga representable, dengan asumsi bahwa eksponen dapat menyimpan -4. Anda dapat mengambil kata saya untuk itu bahwa IEEE ganda dapat menyimpan eksponen kurang dari eksponen epsilon.

Anda tidak dapat mengetahui dari kode ini sendiri apakah masuk akal atau tidak menggunakan epsilonsecara spesifik sebagai terikat, Anda perlu melihat konteksnya. Bisa jadi itu epsilonadalah perkiraan yang masuk akal dari kesalahan dalam perhitungan yang dihasilkan someValue, dan mungkin itu bukan.


2
Poin yang bagus, tetapi bahkan jika itu masalahnya, praktik yang lebih baik adalah menjaga kesalahan terikat pada variabel yang cukup masuk akal dan menggunakannya dalam perbandingan. Seperti berdiri, itu tidak berbeda dari konstanta sihir.
enobayram

Mungkin saya seharusnya lebih jelas dalam pertanyaan saya: Saya tidak mempertanyakan apakah epsilon adalah "ambang" yang cukup besar untuk menutupi kesalahan komputasi tetapi apakah perbandingan ini sama someValue == 0.0atau tidak.
Sebastian Krysmanski

13

Ada angka yang ada antara 0 dan epsilon karena epsilon adalah perbedaan antara 1 dan angka tertinggi berikutnya yang dapat diwakili di atas 1 dan bukan perbedaan antara 0 dan angka tertinggi berikutnya yang dapat diwakili di atas 0 (jika itu, itu kode akan sangat sedikit): -

#include <limits>

int main ()
{
  struct Doubles
  {
      double one;
      double epsilon;
      double half_epsilon;
  } values;

  values.one = 1.0;
  values.epsilon = std::numeric_limits<double>::epsilon();
  values.half_epsilon = values.epsilon / 2.0;
}

Dengan menggunakan debugger, hentikan program di akhir main dan lihat hasilnya dan Anda akan melihat bahwa epsilon / 2 berbeda dari epsilon, nol dan satu.

Jadi fungsi ini mengambil nilai antara +/- epsilon dan menjadikannya nol.


5

Sebuah aproximation dari epsilon (perbedaan sekecil mungkin) di sekitar angka (1.0, 0.0, ...) dapat dicetak dengan program berikut. Ini mencetak output berikut:
epsilon for 0.0 is 4.940656e-324
epsilon for 1.0 is 2.220446e-16
Sedikit pemikiran membuatnya jelas, bahwa epsilon semakin kecil semakin kecil jumlahnya yang kita gunakan untuk melihat nilai epsilon-nya, karena eksponen dapat menyesuaikan dengan ukuran angka itu.

#include <stdio.h>
#include <assert.h>
double getEps (double m) {
  double approx=1.0;
  double lastApprox=0.0;
  while (m+approx!=m) {
    lastApprox=approx;
    approx/=2.0;
  }
  assert (lastApprox!=0);
  return lastApprox;
}
int main () {
  printf ("epsilon for 0.0 is %e\n", getEps (0.0));
  printf ("epsilon for 1.0 is %e\n", getEps (1.0));
  return 0;
}

2
Implementasi apa yang sudah Anda periksa? Ini jelas bukan kasus untuk GCC 4.7.
Anton Golov

3

Misalkan kita bekerja dengan angka floating point mainan yang sesuai dengan register 16 bit. Ada tanda bit, eksponen 5 bit, dan mantissa 10 bit.

Nilai angka floating point ini adalah mantissa, yang ditafsirkan sebagai nilai desimal biner, dua kali lipat dari pangkat eksponen.

Sekitar 1 eksponen sama dengan nol. Jadi digit terkecil dari mantissa adalah satu bagian dalam 1024.

Dekat 1/2 eksponen minus satu, sehingga bagian terkecil dari mantissa adalah setengah besar. Dengan eksponen lima bit dapat mencapai negatif 16, di mana titik terkecil mantissa bernilai satu bagian dalam 32m. Dan pada eksponen negatif 16, nilainya sekitar satu bagian dalam 32k, lebih dekat ke nol daripada epsilon di sekitar yang kita hitung di atas!

Sekarang ini adalah model floating point mainan yang tidak mencerminkan semua kebiasaan sistem floating point nyata, tetapi kemampuan untuk mencerminkan nilai yang lebih kecil dari epsilon cukup mirip dengan nilai floating point yang sebenarnya.


3

Perbedaan antara Xdan nilai selanjutnya Xbervariasi sesuai dengan X.
epsilon()hanya perbedaan antara 1dan nilai selanjutnya dari 1.
Perbedaan antara 0dan nilai selanjutnya 0adalah tidak epsilon().

Sebagai gantinya Anda dapat menggunakan std::nextafteruntuk membandingkan nilai ganda dengan 0sebagai berikut:

bool same(double a, double b)
{
  return std::nextafter(a, std::numeric_limits<double>::lowest()) <= b
    && std::nextafter(a, std::numeric_limits<double>::max()) >= b;
}

double someValue = ...
if (same (someValue, 0.0)) {
  someValue = 0.0;
}

2

Saya pikir itu tergantung pada ketepatan komputer Anda. Lihatlah tabel ini : Anda dapat melihat bahwa jika epsilon Anda diwakili oleh double, tetapi presisi Anda lebih tinggi, perbandingannya tidak setara dengan

someValue == 0.0

Pertanyaan yang bagus!


2

Anda tidak dapat menerapkan ini ke 0, karena mantissa dan bagian eksponen. Karena eksponen Anda dapat menyimpan angka yang sangat kecil, yang lebih kecil dari epsilon, tetapi ketika Anda mencoba melakukan sesuatu seperti (1.0 - "angka sangat kecil") Anda akan mendapatkan 1,0. Epsilon adalah indikator bukan nilai, tetapi presisi nilai, yang ada di mantissa. Ini menunjukkan berapa banyak angka desimal yang benar yang dapat kita simpan.


2

Dengan IEEE floating-point, antara nilai positif non-nol terkecil dan nilai negatif non-nol terkecil, terdapat dua nilai: nol positif dan nol negatif. Menguji apakah suatu nilai berada di antara nilai non-nol terkecil sama dengan menguji kesetaraan dengan nol; penugasan, bagaimanapun, dapat memiliki efek, karena itu akan mengubah nol negatif menjadi nol positif.

Dapat dibayangkan bahwa format floating-point mungkin memiliki tiga nilai antara nilai positif dan negatif hingga terbatas terkecil: positif sangat kecil, nol tidak ditandatangani, dan negatif sangat kecil. Saya tidak terbiasa dengan format floating-point yang sebenarnya bekerja seperti itu, tetapi perilaku seperti itu akan sangat masuk akal dan bisa dibilang lebih baik daripada IEEE (mungkin tidak cukup baik untuk menambahkan perangkat keras tambahan untuk mendukungnya, tetapi secara matematis 1 / (1 / INF), 1 / (- 1 / INF), dan 1 / (1-1) harus mewakili tiga kasus berbeda yang menggambarkan tiga nol berbeda). Saya tidak tahu apakah ada standar C yang akan mengamanatkan bahwa infinitesimal yang ditandatangani, jika ada, harus dibandingkan dengan nol. Jika tidak, kode seperti di atas dapat bermanfaat misalnya


Bukankah "1 / (1-1)" (dari contoh Anda) infinity daripada nol?
Sebastian Krysmanski

Kuantitas (1-1), (1 / INF), dan (-1 / INF) semuanya mewakili nol, tetapi membagi angka positif dengan masing-masing harus secara teori menghasilkan tiga hasil yang berbeda (matematika IEEE menganggap dua yang pertama sebagai identik ).
supercat

1

Jadi katakanlah sistem tidak dapat membedakan 1,000000000000000000000 dan 1,00000000000000000000001. yaitu 1.0 dan 1.0 + 1e-20. Apakah Anda pikir masih ada beberapa nilai yang dapat direpresentasikan antara -1e-20 dan + 1e-20?


Kecuali nol, saya tidak berpikir bahwa ada nilai antara -1e-20 dan + 1e-20. Tetapi hanya karena saya pikir ini tidak menjadikannya benar.
Sebastian Krysmanski

@SebastianKrysmanski: itu tidak benar, ada banyak nilai floating-point antara 0 dan epsilon. Karena itu titik mengambang , bukan titik tetap.
Steve Jessop

Nilai representable terkecil yang berbeda dari nol dibatasi oleh jumlah bit yang dialokasikan untuk mewakili eksponen. Jadi, jika dobel memiliki eksponen 11 bit, angka terkecil adalah 1e-1023.
cababunga

0

Juga, alasan yang baik untuk memiliki fungsi seperti itu adalah untuk menghapus "denormals" (angka-angka yang sangat kecil yang tidak dapat lagi menggunakan terkemuka yang tersirat "1" dan memiliki representasi FP khusus). Mengapa Anda ingin melakukan ini? Karena beberapa mesin (khususnya, beberapa Pentium 4 yang lebih tua) menjadi sangat, sangat lambat saat memproses denormals. Lainnya hanya sedikit lebih lambat. Jika aplikasi Anda tidak benar-benar membutuhkan angka yang sangat kecil ini, membilasnya ke nol adalah solusi yang baik. Tempat yang baik untuk mempertimbangkan ini adalah langkah terakhir dari setiap filter IIR atau fungsi peluruhan.

Lihat juga: Mengapa mengubah 0.1f ke 0 memperlambat kinerja sebesar 10x?

dan http://en.wikipedia.org/wiki/Denormal_number


1
Ini menghapus lebih banyak angka dari sekadar angka yang dinormalisasi. Ini mengubah konstanta Planck atau massa elektron menjadi nol yang akan memberi Anda hasil yang sangat, sangat salah jika Anda menggunakan angka-angka ini.
gnasher729
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.