Confronta il doppio con lo zero usando epsilon


214

Oggi stavo esaminando un po 'di codice C ++ (scritto da qualcun altro) e ho trovato questa sezione:

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

Sto cercando di capire se questo ha persino senso.

La documentazione per epsilon()dice:

La funzione restituisce la differenza tra 1 e il valore più piccolo maggiore di 1 che è rappresentabile [da un doppio].

Questo vale anche per 0, ovvero epsilon()il valore più piccolo è maggiore di 0? Oppure ci sono numeri tra 0e 0 + epsilonche possono essere rappresentati da un double?

In caso contrario, il confronto non è equivalente a someValue == 0.0?


3
Il epsilon intorno a 1 sarà molto più alto di quello intorno a 0, quindi probabilmente ci saranno valori tra 0 e 0 + epsilon_at_1. Immagino che l'autore di questa sezione volesse usare qualcosa di piccolo, ma non voleva usare una costante magica, quindi ha semplicemente usato questo valore essenzialmente arbitrario.
enobayram,

2
Il confronto dei numeri in virgola mobile è difficile e viene persino incoraggiato l'uso di epsilon o del valore di soglia. Consultare: cs.princeton.edu/introcs/91float e cygnus-software.com/papers/comparingfloats/comparingfloats.htm
Aditya Kumar Pandey

40
Il primo collegamento è 403.99999999
graham.reeds il

6
IMO, in questo caso l'utilizzo di numeric_limits<>::epsilonè fuorviante e irrilevante. Ciò che vogliamo è assumere 0 se il valore effettivo differisce di non più di alcuni ε da 0. E ε dovrebbe essere scelto in base alla specifica del problema, non a un valore dipendente dalla macchina. Sospetto che l'attuale epsilon sia inutile, poiché anche solo alcune operazioni del programma quadro potrebbero accumulare un errore maggiore di quello.
Andrey Vihrov,

1
+1. epsilon non è il più piccolo possibile ma può servire allo scopo indicato nella maggior parte delle attività di ingegneria pratica se sai di quale precisione hai bisogno e cosa stai facendo.
SChepurin,

Risposte:


192

Supponendo che il doppio IEEE a 64 bit, vi sia una mantissa a 52 bit e un esponente a 11 bit. Dividiamolo in bit:

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

Il numero rappresentabile più piccolo maggiore di 1:

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

Perciò:

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

Ci sono numeri tra 0 e epsilon? Un sacco ... Ad esempio il numero minimo rappresentabile positivo (normale) è:

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

In effetti ci sono (1022 - 52 + 1)×2^52 = 4372995238176751616numeri tra 0 e epsilon, che è il 47% di tutti i numeri rappresentabili positivi ...


27
Così strano che puoi dire "47% dei numeri positivi" :)
Configuratore

13
@configurator: No, non puoi dirlo (non esiste una misura finita "naturale"). Ma puoi dire "47% dei numeri rappresentabili positivi ".
Yakov Galka,

1
@ybungalobill Non riesco a capirlo. L'esponente ha 11 bit: 1 bit di segno e 10 bit di valore. Perché 2 ^ -1022 e non 2 ^ -1024 è il numero positivo più piccolo?
Pavlo Dyban il

3
@PavloDyban: semplicemente perché gli esponenti non hanno un bit di segno. Sono codificati come offset: se l'esponente codificato è 0 <= e < 2048allora la mantissa viene moltiplicata per 2 per la potenza di e - 1023. Ad esempio, l'esponente di 2^0è codificato come e=1023, 2^1come e=1024e 2^-1022come e=1. Il valore di e=0è riservato ai subnormali e allo zero reale.
Yakov Galka,

2
@PavloDyban: 2^-1022è anche il numero normale più piccolo . Il numero più piccolo è in realtà 0.0000 00000000 00000000 00000000 00000000 00000000 00000001 × 2^-1022 = 2^-1074. Questo è subnormale, il che significa che la parte della mantissa è più piccola di 1, quindi è codificata con l'esponente e=0.
Yakov Galka,

17

Il test non è certamente lo stesso di someValue == 0. L'idea generale di numeri in virgola mobile è che memorizzano un esponente e un significato. Rappresentano quindi un valore con un certo numero di cifre binarie significative di precisione (53 nel caso di un doppio IEEE). I valori rappresentabili sono molto più densamente impacchettati vicino a 0 di quanto non siano vicini a 1.

Per utilizzare un sistema decimale più familiare, supponiamo di memorizzare un valore decimale "a 4 cifre significative" con esponente. Quindi il prossimo valore rappresentabile maggiore di 1è 1.001 * 10^0ed epsilonè 1.000 * 10^-3. Ma 1.000 * 10^-4è anche rappresentabile, supponendo che l'esponente possa memorizzare -4. Puoi credermi sulla parola che un doppio IEEE può immagazzinare esponenti meno dell'esponente di epsilon.

Da questo codice non si può dire da soli se ha senso o meno usare epsilonspecificamente come limite, è necessario esaminare il contesto. Può darsi che epsilonsia una stima ragionevole dell'errore nel calcolo che ha prodotto someValue, e può darsi che non lo sia.


2
Un buon punto, ma anche in questo caso, una pratica migliore sarebbe quella di mantenere l'errore associato in una variabile con un nome ragionevole e usarlo nel confronto. Allo stato attuale, non è diverso da una costante magica.
enobayram,

Forse avrei dovuto essere più chiaro nella mia domanda: non mi chiedevo se epsilon fosse una "soglia" abbastanza grande da coprire l'errore computazionale, ma se questo confronto è uguale someValue == 0.0o no.
Sebastian Krysmanski il

13

Esistono numeri tra 0 e epsilon perché epsilon è la differenza tra 1 e il successivo numero più alto che può essere rappresentato sopra 1 e non la differenza tra 0 e il successivo numero più alto che può essere rappresentato sopra 0 (se lo fosse, quello codice farebbe ben poco): -

#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;
}

Usando un debugger, ferma il programma alla fine di main e guarda i risultati e vedrai che epsilon / 2 è distinto da epsilon, zero e uno.

Quindi questa funzione prende valori tra +/- epsilon e li rende zero.


5

Un'approssimazione di epsilon (la minima differenza possibile) attorno a un numero (1.0, 0.0, ...) può essere stampata con il seguente programma. Stampa il seguente output:
epsilon for 0.0 is 4.940656e-324
epsilon for 1.0 is 2.220446e-16
Un piccolo pensiero chiarisce che epsilon diventa più piccolo quanto più piccolo è il numero che usiamo per guardare il suo valore epsilon, perché l'esponente può adattarsi alle dimensioni di quel numero.

#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
Quali implementazioni hai controllato? Questo non è assolutamente il caso di GCC 4.7.
Anton Golov,

3

Supponiamo di lavorare con numeri a virgola mobile giocattolo che si inseriscono in un registro a 16 bit. C'è un bit di segno, un esponente di 5 bit e una mantissa di 10 bit.

Il valore di questo numero in virgola mobile è la mantissa, interpretata come un valore decimale binario, moltiplicato per due alla potenza dell'esponente.

Circa 1 l'esponente è uguale a zero. Quindi la cifra più piccola della mantissa è una parte nel 1024.

Circa 1/2 dell'esponente è meno uno, quindi la parte più piccola della mantissa è grande la metà. Con un esponente a cinque bit può arrivare a 16 negativi, a quel punto la parte più piccola della mantissa vale una parte in 32m. E a 16 esponente negativo, il valore è di circa una parte in 32k, molto più vicino allo zero di epsilon attorno a uno che abbiamo calcolato sopra!

Ora questo è un modello a virgola mobile giocattolo che non riflette tutte le stranezze di un sistema a virgola mobile reale, ma la capacità di riflettere valori inferiori a epsilon è ragionevolmente simile ai valori a virgola mobile reali.


3

La differenza tra Xe il valore successivo di Xvaria in base a X.
epsilon()è solo la differenza tra 1e il valore successivo di 1.
La differenza tra 0e il valore successivo di 0non è epsilon().

Invece puoi usare std::nextafterper confrontare un doppio valore con 0il seguente:

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

Penso che dipenda dal precisione del tuo computer. Dai un'occhiata a questa tabella : puoi vedere che se il tuo epsilon è rappresentato dal doppio, ma la tua precisione è maggiore, il confronto non è equivalente a

someValue == 0.0

Buona domanda comunque!


2

Non puoi applicare questo a 0, a causa della mantissa e delle parti dell'esponente. Grazie all'esponente puoi memorizzare numeri molto piccoli, che sono più piccoli di epsilon, ma quando provi a fare qualcosa del genere (1.0 - "numero molto piccolo") otterrai 1.0. Epsilon è un indicatore non di valore, ma di precisione del valore, che è in mantissa. Mostra quante cifre decimali conseguenti corrette di numero possiamo memorizzare.


2

Con IEEE in virgola mobile, tra il più piccolo valore positivo diverso da zero e il più piccolo valore negativo diverso da zero, esistono due valori: zero positivo e zero negativo. Verificare se un valore è compreso tra i valori minori diversi da zero equivale a verificare l'uguaglianza con zero; l'assegnazione, tuttavia, potrebbe avere un effetto, poiché cambierebbe uno zero negativo in uno zero positivo.

Sarebbe ipotizzabile che un formato a virgola mobile possa avere tre valori tra i più piccoli valori positivi e negativi finiti: infinitesimo positivo, zero senza segno e infinitesimale negativo. Non ho familiarità con i formati a virgola mobile che in effetti funzionano in questo modo, ma un comportamento del genere sarebbe perfettamente ragionevole e probabilmente migliore di quello dell'IEEE (forse non abbastanza per valere la pena aggiungere hardware aggiuntivo per supportarlo, ma matematicamente 1 / (1 / INF), 1 / (- 1 / INF) e 1 / (1-1) dovrebbero rappresentare tre casi distinti che illustrano tre diversi zero). Non so se uno standard C dovrebbe imporre che gli infinitesimi firmati, se presenti, debbano comparare uguale a zero. In caso contrario, un codice come quello sopra potrebbe utilmente garantire che ad es


"1 / (1-1)" (dal tuo esempio) non è infinito anziché zero?
Sebastian Krysmanski il

Le quantità (1-1), (1 / INF) e (-1 / INF) rappresentano tutte zero, ma la divisione di un numero positivo per ciascuno di essi dovrebbe in teoria produrre tre risultati diversi (la matematica IEEE considera i primi due identici ).
supercat

1

Supponiamo quindi che il sistema non sia in grado di distinguere 1.000000000000000000000 e 1.000000000000000000001. cioè 1.0 e 1.0 + 1e-20. Pensi che ci siano ancora alcuni valori che possono essere rappresentati tra -1e-20 e + 1e-20?


Tranne zero, non credo che ci siano valori tra -1e-20 e + 1e-20. Ma solo perché penso che questo non lo renda vero.
Sebastian Krysmanski il

@SebastianKrysmanski: non è vero, ci sono molti valori in virgola mobile tra 0 e epsilon. Perché è in virgola mobile, non in virgola fissa.
Steve Jessop,

Il valore rappresentabile più piccolo che è distinto da zero è limitato dal numero di bit allocati per rappresentare esponente. Quindi se il doppio ha esponente a 11 bit, il numero più piccolo sarebbe 1e-1023.
Cababunga,

0

Inoltre, una buona ragione per avere una tale funzione è quella di rimuovere i "denormali" (quei numeri molto piccoli che non possono più usare il "1" iniziale implicito e hanno una rappresentazione FP speciale). Perché vorresti farlo? Perché alcune macchine (in particolare alcune più vecchie Pentium 4) diventano molto, molto lente durante l'elaborazione dei denormali. Altri diventano leggermente più lenti. Se la tua applicazione non ha davvero bisogno di questi numeri molto piccoli, scaricarli a zero è una buona soluzione. Buoni posti da considerare sono gli ultimi passi di qualsiasi filtro IIR o funzione di decadimento.

Vedi anche: Perché la modifica da 0,1f a 0 rallenta le prestazioni di 10x?

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


1
Ciò rimuove molti più numeri rispetto ai soli numeri denormalizzati. Cambia la costante di Planck o la massa di un elettrone a zero, il che ti darà risultati molto, molto sbagliati se usassi questi numeri.
gnasher729,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.