È possibile ottenere 0 sottraendo due numeri in virgola mobile disuguali?


131

È possibile ottenere una divisione per 0 (o infinito) nel seguente esempio?

public double calculation(double a, double b)
{
     if (a == b)
     {
         return 0;
     }
     else
     {
         return 2 / (a - b);
     }
}

In casi normali non lo sarà, ovviamente. Ma cosa succede se ae bsono molto vicini, può (a-b)risultare in essere 0dovuto alla precisione del calcolo?

Nota che questa domanda è per Java, ma penso che si applicherà alla maggior parte dei linguaggi di programmazione.


49
Dovrei provare tutte le combinazioni di doppio, ci vorrà un po ':)
Thirler

3
@Thirler mi sembra il momento di usare JUnit Testing per me!
Matt Clark,

7
@bluebrain, la mia ipotesi è che il tuo numero letterale 2.000 ecc contenga molti decimali per essere rappresentato da un float. Quindi gli ultimi non saranno rappresentati dal numero effettivamente utilizzato nel confronto.
Thirler,

4
@Thirler probabilmente. 'non puoi davvero garantire che il numero che assegni al float o al double sia esatto'
armata

4
Nota che in questo caso la restituzione di 0 può portare a un'ambiguità difficile da eseguire il debug, quindi assicurati di voler davvero restituire 0 invece di lanciare un'eccezione o restituire una NaN.
m0skit0,

Risposte:


132

In Java, a - bnon è mai uguale a 0se a != b. Questo perché Java impone operazioni in virgola mobile IEEE 754 che supportano numeri denormalizzati. Dalle specifiche :

In particolare, il linguaggio di programmazione Java richiede il supporto di numeri a virgola mobile denormalizzati IEEE 754 e underflow graduale, che semplificano la dimostrazione delle proprietà desiderabili di particolari algoritmi numerici. Le operazioni in virgola mobile non "svuotano a zero" se il risultato calcolato è un numero denormalizzato.

Se una FPU funziona con numeri denormalizzati , la sottrazione di numeri dispari non può mai produrre zero (diversamente dalla moltiplicazione), vedere anche questa domanda .

Per altre lingue, dipende. In C o C ++, ad esempio, il supporto IEEE 754 è facoltativo.

Detto questo, è possibile che l'espressione 2 / (a - b)trabocchi, ad esempio con a = 5e-308e b = 4e-308.


4
Tuttavia OP vuole sapere di 2 / (ab). Questo può essere garantito per essere finito?
Taemyr,

Grazie per la risposta, ho aggiunto un link a Wikipedia per la spiegazione dei numeri denormalizzati.
Thirler,

3
@Taemyr Vedi la mia modifica. La divisione può effettivamente traboccare.
nwellnhof,

@Taemyr (a,b) = (3,1)=> 2/(a-b) = 2/(3-1) = 2/2 = 1Se questo sia vero con IEEE in virgola mobile, non lo so
Cole Johnson,

1
@DrewDormann IEEE 754 è anche opzionale per C99. Vedi allegato F della norma.
nwellnhof,

50

Per ovviare al problema, che dire di quanto segue?

public double calculation(double a, double b) {
     double c = a - b;
     if (c == 0)
     {
         return 0;
     }
     else
     {
         return 2 / c;
     }
}

In questo modo non dipendi dal supporto IEEE in nessuna lingua.


6
Evita il problema e semplifica il test tutto in una volta. Me piace.
Giosuè,

11
-1 Se a=bnon dovresti tornare 0. Dividere 0in IEEE 754 ti porta all'infinito, non un'eccezione. Stai evitando il problema, quindi tornare 0è un bug in attesa di accadere. Prendere in considerazione 1/x + 1. Se x=0ciò comporterebbe 1, non il valore corretto: infinito.
Cole Johnson,

5
@ColeJohnson la risposta corretta non è neanche l'infinito (a meno che non si specifichi da quale parte provenga il limite, lato destro = + inf, lato sinistro = -inf, non specificato = non definito o NaN).
Nick T,

12
@ChrisHayes: Questa è una risposta valida alla domanda riconoscendo che la domanda potrebbe essere un problema XY: meta.stackexchange.com/questions/66377/what-is-the-xy-problem
slebetman

17
@ColeJohnson Il ritorno 0non è proprio il problema. Questo è ciò che l'OP fa nella domanda. Potresti mettere un'eccezione o qualunque cosa sia appropriata per la situazione in quella parte del blocco. Se non ti piace tornare 0, dovrebbe essere una critica alla domanda. Certamente, fare come l'OP non garantisce un downvote alla risposta. Questa domanda non ha nulla a che fare con ulteriori calcoli dopo il completamento della funzione specificata. Per quanto ne sai, i requisiti del programma richiedono la restituzione 0.
jpmc26,

25

Non otterresti una divisione per zero indipendentemente dal valore di a - b, poiché la divisione in virgola mobile per 0 non genera un'eccezione. Restituisce l'infinito.

Ora, l'unico modo per a == brestituire true è if ae bcontiene esattamente gli stessi bit. Se differiscono solo per il bit meno significativo, la differenza tra loro non sarà 0.

MODIFICARE :

Come Bathsheba ha correttamente commentato, ci sono alcune eccezioni:

  1. "Non un numero confronta" falso con se stesso ma avrà modelli di bit identici.

  2. -0.0 è definito per confrontare true con +0.0 e i loro pattern di bit sono diversi.

Quindi, se entrambi ae lo bsono Double.NaN, raggiungerai la clausola else, ma poiché NaN - NaNanche ritorna NaN, non ti dividerai per zero.


11
Eran; non strettamente vero. "Non un numero confronta" falso con se stesso ma avrà modelli di bit identici. Inoltre -0.0 è definito per confrontare true con +0.0 e i loro pattern di bit sono diversi.
Bathsheba,

1
@Bathsheba Non ho considerato questi casi speciali. Grazie per il commento.
Eran,

2
@Eran, ottimo punto che la divisione per 0 restituirà l'infinito in un punto mobile. Aggiunto alla domanda.
Thirler,

2
@Prashant, ma la divisione non avrebbe luogo in questo caso, poiché a == b restituirebbe true.
Eran,

3
In realtà si potrebbe ottenere un'eccezione FP per la divisione per zero, è un'opzione definita dallo standard IEEE-754, anche se probabilmente non è quello che la maggior parte delle persone vorrebbe dire con "eccezione";)
Voo

17

Non vi è alcun caso in cui una divisione per zero può avvenire qui.

La SMT Risolutore Z3 supporta IEEE preciso aritmetica in virgola mobile. Chiediamo a Z3 di trovare numeri ae btali che a != b && (a - b) == 0:

(set-info :status unknown)
(set-logic QF_FP)
(declare-fun b () (FloatingPoint 8 24))
(declare-fun a () (FloatingPoint 8 24))
(declare-fun rm () RoundingMode)
(assert
(and (not (fp.eq a b)) (fp.eq (fp.sub rm a b) +zero) true))
(check-sat)

Il risultato è UNSAT. Non ci sono tali numeri.

La stringa SMTLIB precedente consente inoltre a Z3 di scegliere una modalità di arrotondamento arbitraria ( rm). Ciò significa che il risultato vale per tutte le possibili modalità di arrotondamento (di cui ci sono cinque). Il risultato include anche la possibilità che una qualsiasi delle variabili in gioco possa essere NaNo infinito.

a == bè implementato come fp.eqqualità in modo che +0fe -0fconfrontare uguale. Anche il confronto con zero viene implementato fp.eq. Poiché la domanda è volta ad evitare una divisione per zero, questo è il confronto appropriato.

Se il test di uguaglianza fosse stato implementato usando l'uguaglianza bit a bit +0fe -0fsarebbe stato un modo per fare a - bzero. Una versione precedente errata di questa risposta contiene i dettagli della modalità su quel caso per i curiosi.

Z3 Online non supporta ancora la teoria FPA. Questo risultato è stato ottenuto utilizzando l'ultimo ramo instabile. Può essere riprodotto usando i collegamenti .NET come segue:

var fpSort = context.MkFPSort32();
var aExpr = (FPExpr)context.MkConst("a", fpSort);
var bExpr = (FPExpr)context.MkConst("b", fpSort);
var rmExpr = (FPRMExpr)context.MkConst("rm", context.MkFPRoundingModeSort());
var fpZero = context.MkFP(0f, fpSort);
var subExpr = context.MkFPSub(rmExpr, aExpr, bExpr);
var constraintExpr = context.MkAnd(
        context.MkNot(context.MkFPEq(aExpr, bExpr)),
        context.MkFPEq(subExpr, fpZero),
        context.MkTrue()
    );

var smtlibString = context.BenchmarkToSMTString(null, "QF_FP", null, null, new BoolExpr[0], constraintExpr);

var solver = context.MkSimpleSolver();
solver.Assert(constraintExpr);

var status = solver.Check();
Console.WriteLine(status);

Utilizzando Z3 per rispondere alle domande IEEE float è bello perché è difficile ignorare i casi (come ad esempio NaN, -0f, +-inf) e si possono porre domande arbitrarie. Non è necessario interpretare e citare le specifiche. Puoi anche porre domande miste float e numeri interi come "questo int log2(float)algoritmo particolare è corretto?".


Potete per favore aggiungere un link a SMT Solver Z3 e un link a un interprete online? Mentre questa risposta sembra del tutto legittima, qualcuno può pensare che questi risultati siano sbagliati.
AL

12

La funzione fornita può effettivamente restituire l'infinito:

public class Test {
    public static double calculation(double a, double b)
    {
         if (a == b)
         {
             return 0;
         }
         else
         {
             return 2 / (a - b);
         }
    }    

    /**
     * @param args
     */
    public static void main(String[] args) {
        double d1 = Double.MIN_VALUE;
        double d2 = 2.0 * Double.MIN_VALUE;
        System.out.println("Result: " + calculation(d1, d2)); 
    }
}

L'output è Result: -Infinity.

Quando il risultato della divisione è troppo grande per essere memorizzato in un doppio, viene restituito l'infinito anche se il denominatore è diverso da zero.


6

In un'implementazione in virgola mobile conforme a IEEE-754, ogni tipo in virgola mobile può contenere numeri in due formati. Uno ("normalizzato") viene utilizzato per la maggior parte dei valori in virgola mobile, ma il secondo numero più piccolo che può rappresentare è solo un po 'più grande del più piccolo, e quindi la differenza tra loro non è rappresentabile nello stesso formato. L'altro formato ("denormalizzato") viene utilizzato solo per numeri molto piccoli che non sono rappresentabili nel primo formato.

I circuiti per gestire in modo efficiente il formato a virgola mobile denormalizzato sono costosi e non tutti i processori lo includono. Alcuni processori offrono la possibilità di scegliere se le operazioni su numeri molto piccoli siano molto più lente delle operazioni su altri valori, oppure se il processore considera semplicemente numeri troppo piccoli per il formato normalizzato.

Le specifiche Java implicano che le implementazioni dovrebbero supportare il formato denormalizzato, anche su macchine in cui ciò farebbe funzionare il codice più lentamente. D'altra parte, è possibile che alcune implementazioni possano offrire opzioni per consentire al codice di essere eseguito più velocemente in cambio di una gestione dei valori leggermente approssimativa che per la maggior parte degli scopi sarebbe troppo piccola per essere rilevante (nei casi in cui i valori sono troppo piccoli per essere importanti, può essere fastidioso avere calcoli con loro che richiedono dieci volte più dei calcoli che contano, quindi in molte situazioni pratiche lo sciacquone a zero è più utile dell'aritmetica lenta ma accurata).


6

Anticamente prima di IEEE 754, era del tutto possibile che a! = B non implicasse ab! = 0 e viceversa. Questo è stato uno dei motivi per creare IEEE 754 in primo luogo.

Con IEEE 754 è quasi garantito. I compilatori C o C ++ possono eseguire un'operazione con una precisione maggiore del necessario. Quindi se aeb non sono variabili ma espressioni, allora (a + b)! = C non implica (a + b) - c! = 0, perché a + b potrebbe essere calcolato una volta con maggiore precisione e una volta senza maggiore precisione.

Molte FPU possono essere commutate in una modalità in cui non restituiscono numeri denormalizzati ma li sostituiscono con 0. In quella modalità, se aeb sono piccoli numeri normalizzati in cui la differenza è minore del numero normalizzato più piccolo ma maggiore di 0, a ! = b inoltre non garantisce a == b.

"Non confrontare mai i numeri in virgola mobile" è la programmazione di culto del carico. Tra le persone che hanno il mantra "hai bisogno di un epsilon", la maggior parte non ha idea di come scegliere correttamente epsilon.


2

Mi viene in mente un caso in cui potresti essere in grado di farlo accadere. Ecco un esempio analogo in base 10 - in realtà, questo accadrebbe ovviamente in base 2.

I numeri in virgola mobile sono memorizzati più o meno in notazione scientifica - cioè, invece di vedere 35.2, il numero che viene memorizzato sarebbe più simile a 3.52e2.

Immagina per comodità che abbiamo un'unità a virgola mobile che opera nella base 10 e ha 3 cifre di precisione. Cosa succede quando si sottrae 9.99 da 10.0?

1.00e2-9.99e1

Sposta per dare a ciascun valore lo stesso esponente

1.00e2-0.999e2

Arrotondare a 3 cifre

1.00e2-1.00e2

Uh Oh!

Il fatto che ciò accada alla fine dipende dal design della FPU. Poiché la gamma di esponenti per un doppio è molto ampia, l'hardware deve arrotondare internamente ad un certo punto, ma nel caso sopra, solo 1 cifra aggiuntiva internamente eviterà qualsiasi problema.


1
I registri che trattengono gli operandi allineati per la sottrazione devono contenere due bit extra, chiamati "bit di guardia", per far fronte a questa situazione. Nello scenario in cui la sottrazione provocherebbe un prestito dal bit più significativo, la grandezza dell'operando più piccolo deve superare la metà di quella dell'operando più grande (il che implica che può avere solo un ulteriore bit di precisione) oppure il risultato deve essere almeno metà dell'entità dell'operando più piccolo (sottintendendo che avrà bisogno solo di un altro bit, più informazioni sufficienti per garantire il corretto arrotondamento).
supercat,

1
"Se ciò può accadere alla fine dipende dal design della FPU" No, non può accadere perché la definizione Java dice che non può. Il design FPU non ha nulla a che fare con esso.
Pascal Cuoq,

@PascalCuoq: correggimi se sbaglio, ma strictfpnon è abilitato, è possibile che i calcoli producano valori troppo piccoli doublema che si adattano a un valore a virgola mobile di precisione estesa.
supercat,

@supercat L'assenza di strictfpinfluenza solo i valori di "risultati intermedi", e sto citando da docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.4 . ae bsono doublevariabili, non risultati intermedi, quindi i loro valori sono valori a doppia precisione, quindi sono multipli di 2 ^ -1074. La sottrazione di questi due valori di doppia precisione è di conseguenza un multiplo di 2 ^ -1074, quindi l'intervallo esponente più ampio modifica la proprietà che la differenza è 0 iff a == b.
Pascal Cuoq,

@supercat Questo ha senso: per farlo ti servirà solo un po 'di più.
Keldor314,

1

Non dovresti mai confrontare float o doppi per l'uguaglianza; perché, non puoi davvero garantire che il numero che assegni al float o al double sia esatto.

Per confrontare i float per l'uguaglianza in modo sano, è necessario verificare se il valore è "abbastanza vicino" allo stesso valore:

if ((first >= second - error) || (first <= second + error)

6
"Dovrei mai" è un po 'forte, ma in genere questo è un buon consiglio.
Mark Pattison,

1
Mentre sei vero, abs(first - second) < error(o <= error) è più facile e più conciso.
glglgl,

3
Sebbene sia vero nella maggior parte dei casi ( non tutti ), in realtà non risponde alla domanda.
Milleniumbug,

4
Testare numeri in virgola mobile per l'uguaglianza è abbastanza spesso utile. Non c'è nulla di sano nel confronto con un epsilon che non è stato scelto con cura, e ancora meno sano nel confronto con un epsilon quando si sta verificando l'uguaglianza.
tmyklebu,

1
Se ordini un array su una chiave in virgola mobile, posso garantire che il tuo codice non funzionerà se provi a usare dei trucchi che confrontano i numeri in virgola mobile con un epsilon. Perché la garanzia che a == b = b == c implica a == c non c'è più. Per le tabelle hash, lo stesso identico problema. Quando l'uguaglianza non è transitiva, i tuoi algoritmi si rompono.
gnasher729,

1

La divisione per zero non è definita, poiché il limite dai numeri positivi tende all'infinito, il limite dai numeri negativi tende all'infinito negativo.

Non sono sicuro se si tratta di C ++ o Java poiché non esiste un tag di lingua.

double calculation(double a, double b)
{
     if (a == b)
     {
         return nan(""); // C++

         return Double.NaN; // Java
     }
     else
     {
         return 2 / (a - b);
     }
}

1

Il problema principale è che la rappresentazione al computer di un doppio (alias float o numero reale in linguaggio matematico) è errata quando si dispone di "troppo" decimale, ad esempio quando si tratta di un doppio che non può essere scritto come valore numerico ( pi o il risultato di 1/3).

Quindi a == b non può essere fatto con nessun doppio valore di aeb, come si fa a gestire a == b quando a = 0,333 eb = 1/3? A seconda del tuo sistema operativo vs FPU vs numero vs lingua rispetto al conteggio di 3 dopo 0, avrai vero o falso.

Ad ogni modo, se esegui un "calcolo del doppio valore" su un computer, devi fare i conti con la precisione, quindi invece di farlo a==b, devi farlo absolute_value(a-b)<epsilon, ed epsilon è relativo a ciò che stai modellando in quel momento nel tuo algoritmo. Non puoi avere un valore epsilon per tutti i tuoi doppi confronti.

In breve, quando si digita a == b, si ha un'espressione matematica che non può essere tradotta su un computer (per qualsiasi numero in virgola mobile).

PS: ronzio, tutto ciò che rispondo qui è ancora più o meno nelle altre risposte e commenti.


1

Sulla base della risposta di @malarres e del commento di @Taemyr, ecco il mio piccolo contributo:

public double calculation(double a, double b)
{
     double c = 2 / (a - b);

     // Should not have a big cost.
     if (isnan(c) || isinf(c))
     {
         return 0; // A 'whatever' value.
     }
     else
     {
         return c;
     }
}

Il mio punto è dire: il modo più semplice per sapere se il risultato della divisione è nan o inf è effettivamente quello di eseguire la divisione.

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.