2009-10-23 13 views
23

A un certo punto in un algoritmo ho bisogno di confrontare il valore float di una proprietà di una classe con un float. Così faccio questo:Strano problema di confronto dei galleggianti nell'obiettivo-C

if (self.scroller.currentValue <= 0.1) { 
} 

dove currentValue è una proprietà float.

Tuttavia, quando ho l'uguaglianza e self.scroller.currentValue = 0.1 l'istruzione if non è soddisfatta e il codice non è stato eseguito! Ho scoperto che posso risolvere il problema gettando 0.1 a float. Mi piace:

if (self.scroller.currentValue <= (float)0.1) { 
} 

Questo funziona correttamente.

Qualcuno può spiegare al mio perché questo sta accadendo? 0.1 è definito come doppio di default o qualcosa del genere?

Grazie.

+4

Vedere anche, "Quello che ogni computer Scientist dovrebbe sapere aritmetica alla virgola mobile:" http://docs.sun.com/source/ 806-3568/ncg_goldberg.html –

+0

Per quelli di voi (in particolare @alastair) che hanno lavorato per migliorare la mia risposta, non sono convinto che potrebbe essere migliorato. Sono d'accordo che non era corretto e probabilmente pericoloso. L'ho cancellato. Per favore, vedi la risposta di James Snook per un'esplorazione molto più profonda di questo problema non banale. –

+0

@RobNapier Devo dire che ho pensato che valesse la pena mantenere la risposta, con le correzioni aggiunte, ma capisco il tuo punto di vista. Mi scuso se la mia modifica sembrava un po 'aggressiva - volevo solo chiarire quale fosse il problema. – alastair

risposta

30

credo, non avendo trovato lo standard che dice così, che quando si confrontano un float ad un double il float è gettato a un double prima confronto. I numeri in virgola mobile senza un modificatore sono considerati double in C.

Tuttavia, in C non esiste una rappresentazione esatta di 0,1 nei galleggianti e nei doppi. Ora, usare un float ti dà un piccolo errore. L'uso di un doppio ti dà un errore ancora più piccolo. Il problema ora è che, convertendo lo float in un double, si verifica l'errore più grande dello float. Ovviamente non sono andati a confrontare ora.

Invece di utilizzare (float)0.1 è possibile utilizzare 0.1f che è un po 'più bello da leggere.

1

Generalmente, in qualsiasi lingua, non si può realmente contare sull'uguaglianza di tipi simili a quelli del tipo float. Nel tuo caso poiché sembra che tu abbia più controllo, sembra che 0.1 non sia float di default. Probabilmente lo scoprirete con sizeof (0.1) (vs. sizeof (self.scroller.currentValue).

+0

questa è una buona idea! Non ho pensato a sizeof() – Dimitris

+0

sizeof rivela che 0.1 è un doppio. È ancora molto strano che non si ottenga l'uguaglianza quando entrambi hanno il valore 0.10000 no? – Dimitris

+0

@Dimitris, non è strano. Vedi la risposta di @ MarkPowell. –

4

In C, un letterale a virgola mobile come 0,1 è un doppio, non un galleggiante. Poiché i tipi di dati confrontati sono diversi, il confronto viene eseguito nel tipo più preciso (doppio). In tutte le implementazioni che conosco, float ha una rappresentazione più breve del doppio (di solito espressa in qualcosa come 6 vs 14 posizioni decimali). Inoltre, l'aritmetica è in binario e 1/10 non ha una rappresentazione esatta in binario.

Pertanto, si sta prendendo un valore 0,1, che perde precisione, estendendolo a doppio, e si aspetta che si confronti un doppio 0,1, che perde meno precisione.

Supponiamo di farlo in decimali, con il float di tre cifre e il doppio di sei, e ci siamo confrontati con 1/3.

Abbiamo il valore float memorizzato pari a 0,333. Lo confrontiamo con un doppio con valore 0,333333. Convertiamo il float 0,333 in doppio 0,333000 e lo troviamo diverso.

+0

Seguendo questo pensiero in decimale (che ha numeri diversi diversi da quelli binari, ma il concetto è lo stesso), troverete che (1/3) * 3! = 1, non importa quante cifre (finite) scegliete. Questo è il motivo per cui fare tutti i calcoli in virgola mobile o doppia non risolve il problema. –

+0

Giusto. Ovviamente, puoi ottenere arbitrariamente vicino a 1 utilizzando un numero di cifre sufficiente, quindi i test per diventare veramente vicini funzionano molto meglio dei test per l'uguaglianza. Il vero problema qui è che i test di uguaglianza in virgola mobile non funzionano in generale. –

+0

concordato. Quindi l'avviso disponibile (che raccomando di accendere) per evitare di utilizzare accidentalmente l'uguaglianza in virgola mobile. –

4

0.1 è in realtà un valore molto difficile da memorizzare binario.In base 2, 1/10 è la frazione infinitamente ripetere

0.0001100110011001100110011001100110011001100110011... 

Come vari ha sottolineato, il confronto deve fornito con una costante la stessa precisione.

6

Il problema è, come suggerito nella domanda, che si sta confrontando un float con un doppio.

C'è un problema più generale con il confronto di float, questo accade perché quando si esegue un calcolo su un numero in virgola mobile il risultato del calcolo potrebbe non essere esattamente quello che ci si aspetta. È abbastanza comune che l'ultimo bit del float risultante sia errato (anche se l'imprecisione può essere maggiore dell'ultimo bit). Se si utilizza == per confrontare due float, tutti i bit devono essere uguali per i float. Se il tuo calcolo dà un risultato un po 'impreciso, allora non compareranno uguali quando te lo aspetti. Invece di confrontare i valori come questo, puoi confrontarli per vedere se sono quasi uguali. Per fare questo puoi prendere la differenza positiva tra i float e vedere se è inferiore a un dato valore (chiamato epsilon).

Per scegliere un buon epsilon è necessario capire un po 'sui numeri in virgola mobile. I numeri in virgola mobile funzionano in modo simile a rappresentare un numero per un dato numero di cifre significative. Se lavoriamo a 5 cifre significative e il tuo calcolo risulta errato nell'ultima cifra del risultato, allora 1.2345 avrà un errore di + -0.0001 mentre 1234500 avrà un errore di + -100. Se si basa sempre il margine di errore sul valore 1.2345, la routine di confronto sarà identica a == per tutti i valori superiori a 10 (quando si utilizza il decimale). Questo è peggio in binario, sono tutti valori maggiori di 2. Ciò significa che l'epsilon che scegliamo deve essere relativo alla dimensione dei float che stiamo confrontando.

FLT_EPSILON è il divario tra 1 e il successivo movimento più vicino. Ciò significa che può essere un buon epsilon scegliere se il tuo numero è compreso tra 1 e 2, ma se il tuo valore è maggiore di 2 usando questo epsilon è inutile perché il divario tra 2 e il successivo float più vicino è maggiore di epsilon. Quindi dobbiamo scegliere un epsilon in relazione alla dimensione dei nostri float (poiché l'errore nel calcolo è relativo alla dimensione dei nostri float).

Un buon (ish) virgola mobile confronto di routine simile a questa:.

bool compareNearlyEqual (float a, float b, unsigned epsilonMultiplier)  
{ 
    float epsilon; 
    /* May as well do the easy check first. */ 
    if (a == b) 
    return true; 

    if (a > b) { 
    epsilon = scalbnf(1.0f, ilogb(a)) * FLT_EPSILON * epsilonMultiplier; 
    } else { 
    epsilon = scalbnf(1.0, ilogb(b)) * FLT_EPSILON * epsilonMultiplier; 
    } 

    return fabs (a - b) <= epsilon; 
} 

Questa routine confronto confronta carri relativi alla dimensione del più grande galleggiante passato in scalbnf(1.0f, ilogb(a)) * FLT_EPSILON trova il divario tra a e la prossima il galleggiante più vicino. Questo viene quindi moltiplicato per il numero epsilonMultiplier, quindi la dimensione della differenza può essere regolata, a seconda di quanto sia inaccurato il risultato del calcolo.

È possibile effettuare una semplice compareLessThan di routine come questa:

bool compareLessThan (float a, float b, unsigned epsilonMultiplier) 
{ 
    if (compareNearlyEqual (a, b, epsilonMultiplier) 
    return false; 

    return a < b; 
} 

Si potrebbe anche scrivere un molto simile compareGreaterThan funzione.

Vale la pena notare che confrontare galleggianti come questo potrebbe non essere sempre quello che vuoi. Ad esempio, questo non troverà mai che un float è vicino a 0 a meno che non sia 0. Per risolvere questo problema, devi decidere quale valore pensavi fosse vicino allo zero e scrivere un test aggiuntivo per questo.

A volte le imprecisioni ottenute non dipenderanno dalla dimensione del risultato di un calcolo, ma dipenderanno dai valori inseriti in un calcolo.Ad esempio, sin(1.0f + (float)(200 * M_PI)) darà un risultato molto meno accurato di sin(1.0f) (i risultati dovrebbero essere identici). In questo caso la routine di confronto dovrebbe considerare il numero inserito nel calcolo per conoscere il margine di errore della risposta.

-1

Convertire in una stringa, quindi confrontare:

NSString* numberA = [NSString stringWithFormat:@"%.6f", a]; 
NSString* numberB = [NSString stringWithFormat:@"%.6f", b]; 

return [numberA isEqualToString: numberB]; 
Problemi correlati