Quanto è pericoloso confrontare i valori in virgola mobile?


391

So che UIKitusa a CGFloatcausa del sistema di coordinate indipendente dalla risoluzione.

Ma ogni volta che desidera controllare se per esempio frame.origin.xsi 0mi fa sentire male:

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

Non è CGFloatvulnerabile a falsi positivi quando si confrontano con ==, <=, >=, <, >? È un punto mobile e hanno problemi di non precisione: 0.0000000000041ad esempio.

È Objective-Cla manipolazione di questo internamente quando si confrontano o può accadere che un origin.xche si legge come pari a zero non si confronta con 0vero?

Risposte:


466

Prima di tutto, i valori in virgola mobile non sono "casuali" nel loro comportamento. Il confronto esatto può e ha senso in molti usi del mondo reale. Ma se hai intenzione di utilizzare il virgola mobile devi essere consapevole di come funziona. Errare sul lato dell'ipotesi che il funzionamento in virgola mobile funzioni come numeri reali porterà a un codice che si rompe rapidamente. Errare sul lato dell'ipotesi che i risultati in virgola mobile abbiano un grande fuzz casuale associato con loro (come la maggior parte delle risposte qui suggerite) ti farà ottenere un codice che sembra funzionare all'inizio ma finisce per avere errori di grande magnitudine e casi d'angolo rotti.

Prima di tutto, se vuoi programmare con virgola mobile, dovresti leggere questo:

Ciò che ogni scienziato informatico dovrebbe sapere sull'aritmetica a virgola mobile

Sì, leggi tutto. Se è troppo oneroso, dovresti usare numeri interi / punti fissi per i tuoi calcoli fino a quando non hai tempo di leggerlo. :-)

Ora, detto ciò, i maggiori problemi con confronti esatti in virgola mobile si riducono a:

  1. Il fatto che un sacco di valori che si possono scrivere nel codice, o leggere con scanfo strtod, non esistono come valori in virgola mobile e ottenere in silenzio convertito alla approssimazione più vicina. Questa è la risposta di demon9733.

  2. Il fatto che molti risultati vengano arrotondati a causa della mancanza di precisione sufficiente per rappresentare il risultato effettivo. Un semplice esempio in cui puoi vedere questo è l'aggiunta x = 0x1fffffee y = 1come float. Qui xha 24 bit di precisione nella mantissa (ok) e yha solo 1 bit, ma quando li aggiungi, i loro bit non si trovano in punti sovrapposti e il risultato richiederebbe 25 bit di precisione. Al contrario, viene arrotondato ( 0x2000000nella modalità di arrotondamento predefinita).

  3. Il fatto che molti risultati vengano arrotondati a causa della necessità di infiniti posti per il valore corretto. Ciò include sia risultati razionali come 1/3 (che hai familiarità con i decimali dove occupa infinitamente molti posti) ma anche 1/10 (che occupa anche infinitamente molti posti in binario, poiché 5 non è una potenza di 2), così come risultati irrazionali come la radice quadrata di tutto ciò che non è un quadrato perfetto.

  4. Doppio arrotondamento. Su alcuni sistemi (in particolare x86), le espressioni in virgola mobile vengono valutate con una precisione maggiore rispetto ai loro tipi nominali. Ciò significa che quando si verifica uno dei suddetti tipi di arrotondamento, si ottengono due passaggi di arrotondamento, prima un arrotondamento del risultato al tipo di precisione superiore, quindi un arrotondamento al tipo finale. Ad esempio, considera cosa succede in decimale se arrotondi 1,49 a un numero intero (1), rispetto a ciò che accade se lo arrotoli prima in una posizione decimale (1,5), quindi arrotonda quel risultato a un numero intero (2). Questa è in realtà una delle aree più cattive da affrontare in virgola mobile, dal momento che il comportamento del compilatore (specialmente per compilatori buggy, non conformi come GCC) è imprevedibile.

  5. Funzioni trascendenti ( trig, exp, log, etc.) non sono specificate per avere risultati correttamente arrotondati; il risultato è appena specificato per essere corretto all'interno di un'unità nell'ultimo punto di precisione (di solito indicato come 1ulp ).

Quando si scrive un codice a virgola mobile, è necessario tenere presente ciò che si sta facendo con i numeri che potrebbero causare l'inesattezza dei risultati e fare i confronti di conseguenza. Spesso avrà senso confrontare con un "epsilon", ma tale epsilon dovrebbe essere basato sulla grandezza dei numeri che si stanno confrontando , non una costante assoluta. (Nei casi in cui un epsilon costante assoluto funzionerebbe, è fortemente indicativo che il punto fisso, non il punto mobile, sia lo strumento giusto per il lavoro!)

Modifica: in particolare, un controllo epsilon relativo alla grandezza dovrebbe assomigliare a:

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

Da dove FLT_EPSILONproviene la costante float.h(sostituiscila con DBL_EPSILONfor doubles o LDBL_EPSILONfor long doubles) ed Kè una costante che scegli in modo tale che l'errore accumulato dei tuoi calcoli sia definitivamente limitato dalle Kunità nell'ultimo posto (e se non sei sicuro di aver ricevuto l'errore calcolo associato a destra, rendilo Kun po 'più grande di quello che i tuoi calcoli dicono che dovrebbe essere).

Infine, tieni presente che se lo usi, potrebbe essere necessario prestare particolare attenzione vicino allo zero, poiché FLT_EPSILONnon ha senso per i denormali. Una soluzione rapida sarebbe quella di farlo:

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

e allo stesso modo sostituire DBL_MINse si usano i doppi.


25
fabs(x+y)è problematico se xe y(può) avere un segno diverso. Tuttavia, una buona risposta contro l'ondata di confronti tra culto e merci.
Daniel Fischer,

27
Se xe yhanno un segno diverso, non è un problema. Il lato destro sarà "troppo piccolo", ma dato che xe ycon segno diverso, non dovrebbero comunque essere uguali. (A meno che non siano così piccoli da essere denormali, ma poi il secondo caso lo prende)
R .. GitHub FERMA AIUTANDO ICE

4
Sono curioso della tua affermazione: "specialmente per compilatori buggy, non conformi come GCC". GCC è davvero difettoso e anche non conforme?
Nicolás Ozimica,

3
Dal momento che la domanda è taggata iOS, vale la pena notare che i compilatori di Apple (sia clang che build gcc di Apple) hanno sempre usato FLT_EVAL_METHOD = 0 e cercano di essere completamente severi sul non portare una precisione in eccesso. In caso di violazioni, si prega di presentare segnalazioni di bug.
Stephen Canon,

17
"Prima di tutto, i valori in virgola mobile non sono" casuali "nel loro comportamento. Il confronto esatto può e ha senso in molti usi del mondo reale." - Solo due frasi e hai già guadagnato un +1! Questa è una delle ipotesi più inquietanti che le persone fanno quando lavorano con punti mobili.
Christian Rau,

36

Dato che 0 è esattamente rappresentabile come un numero in virgola mobile IEEE754 (o usando qualsiasi altra implementazione di numeri fp con cui abbia mai lavorato) il confronto con 0 è probabilmente sicuro. Potresti essere morso, tuttavia, se il tuo programma calcola un valore (come theView.frame.origin.x) che hai motivo di credere dovrebbe essere 0 ma che il tuo calcolo non può garantire di essere 0.

Per chiarire un po ', un calcolo come:

areal = 0.0

(a meno che la tua lingua o il tuo sistema non sia rotto) creerà un valore tale che (areal == 0,0) restituisca vero ma un altro calcolo come

areal = 1.386 - 2.1*(0.66)

non può.

Se puoi assicurarti che i tuoi calcoli producono valori che sono 0 (e non solo che producono valori che dovrebbero essere 0) allora puoi andare avanti e confrontare i valori fp con 0. Se non riesci ad assicurarti al grado richiesto , attenersi meglio al solito approccio di "uguaglianza tollerata".

Nel peggiore dei casi il confronto negligente dei valori di FP può essere estremamente pericoloso: pensa all'avionica, alla guida delle armi, alle operazioni delle centrali elettriche, alla navigazione dei veicoli, quasi a qualsiasi applicazione in cui il calcolo incontra il mondo reale.

Per Angry Birds, non così pericoloso.


11
In realtà, 1.30 - 2*(0.65)è un esempio perfetto di un'espressione che ovviamente valuta 0,0 se il compilatore implementa IEEE 754, perché i doppi rappresentati come 0.65e 1.30hanno gli stessi significati e la moltiplicazione per due è ovviamente esatta.
Pascal Cuoq,

7
Ho ancora ricevuto un rappresentante da questo, quindi ho cambiato il secondo frammento di esempio.
Contrassegno ad alte prestazioni

22

Voglio dare una risposta un po 'diversa rispetto alle altre. Sono ottimi per rispondere alla tua domanda come indicato, ma probabilmente non per quello che devi sapere o qual è il tuo vero problema.

Il virgola mobile nella grafica va bene! Ma non c'è quasi bisogno di confrontare direttamente i galleggianti. Perché dovresti farlo? La grafica utilizza i float per definire gli intervalli. E confrontare se un float è all'interno di un intervallo definito anche dai float è sempre ben definito e deve semplicemente essere coerente, non accurato o preciso! Finché è possibile assegnare un pixel (che è anche un intervallo!), È tutto ciò che serve per la grafica.

Quindi, se vuoi testare se il tuo punto è al di fuori di un intervallo [0..width [questo va bene. Assicurati solo di definire l'inclusione in modo coerente. Ad esempio, definire sempre inside è (x> = 0 && x <larghezza). Lo stesso vale per i test di intersezione o hit.

Tuttavia, se stai abusando di una coordinata grafica come una sorta di flag, ad esempio per vedere se una finestra è ancorata o meno, non dovresti farlo. Utilizzare invece un flag booleano separato dal livello di presentazione grafica.


13

Il confronto con zero può essere un'operazione sicura, purché lo zero non sia un valore calcolato (come indicato in una risposta sopra). La ragione di ciò è che zero è un numero perfettamente rappresentabile in virgola mobile.

Parlando di valori perfettamente rappresentabili, ottieni 24 bit di intervallo in una nozione di potenza di due (precisione singola). Quindi 1, 2, 4 sono perfettamente rappresentabili, come lo sono .5, .25 e .125. Finché tutti i tuoi bit importanti sono in 24 bit, sei d'oro. Quindi 10.625 possono essere riassunti con precisione.

Questo è fantastico, ma cadrà rapidamente sotto pressione. Mi vengono in mente due scenari: 1) Quando è coinvolto un calcolo. Non fidarti di sqrt (3) * sqrt (3) == 3. Semplicemente non sarà così. E probabilmente non sarà all'interno di un epsilon, come suggeriscono alcune delle altre risposte. 2) Quando è coinvolta una non potenza di 2 (NPOT). Quindi può sembrare strano, ma 0.1 è una serie infinita in binario e quindi qualsiasi calcolo che coinvolge un numero come questo sarà impreciso dall'inizio.

(Oh, e la domanda originale menzionava i confronti a zero. Non dimenticare che -0,0 è anche un valore in virgola mobile perfettamente valido.)


11

[La "risposta giusta" va oltre la selezione K. La selezione Kfinisce per essere ad-hoc come la selezione, VISIBLE_SHIFTma la selezione Kè meno ovvia perché a differenza di VISIBLE_SHIFTessa non è fondata su alcuna proprietà di visualizzazione. Scegli quindi il tuo veleno: seleziona Ko seleziona VISIBLE_SHIFT. Questa risposta sostiene la selezione VISIBLE_SHIFTe quindi dimostra la difficoltà nella selezione K]

Proprio a causa di errori rotondi, non è necessario utilizzare il confronto di valori "esatti" per le operazioni logiche. Nel tuo caso specifico di una posizione su un display visivo, non può importare se la posizione è 0,0 o 0,0000000003 - la differenza è invisibile all'occhio. Quindi la tua logica dovrebbe essere qualcosa del tipo:

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

Tuttavia, alla fine, "invisibile alla vista" dipenderà dalle proprietà del display. Se puoi rilegare in alto il display (dovresti essere in grado di farlo); quindi scegli VISIBLE_SHIFTdi essere una frazione di quel limite superiore.

Ora, la "risposta giusta" si basa Kquindi esploriamo la raccolta K. La "risposta giusta" sopra dice:

K è una costante che scegli in modo tale che l'errore accumulato dei tuoi calcoli sia delimitato definitivamente dalle unità K nell'ultimo posto (e se non sei sicuro di aver corretto il calcolo del limite di errore, rendi K alcune volte più grande di quello che calcoli dire che dovrebbe essere)

Quindi abbiamo bisogno K. Se ottenere Kè più difficile, meno intuitivo di selezionare il mio, VISIBLE_SHIFTallora deciderai cosa funziona per te. Per scoprire Kche stiamo per scrivere un programma di test che esamina un sacco di Kvalori in modo da poter vedere come si comporta. Dovrebbe essere ovvio come scegliere K, se la "risposta giusta" è utilizzabile. No?

Useremo, come i dettagli della "risposta giusta":

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

Proviamo solo tutti i valori di 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, quindi K dovrebbe essere 1e16 o più grande se voglio che 1e-13 sia 'zero'.

Quindi, direi che hai due opzioni:

  1. Fai un semplice calcolo epsilon usando il tuo giudizio ingegneristico per il valore di "epsilon", come ho suggerito. Se stai facendo grafica e "zero" significa essere un "cambiamento visibile", allora esamina le tue risorse visive (immagini, ecc.) E giudica cosa può essere epsilon.
  2. Non tentare calcoli in virgola mobile fino a quando non hai letto il riferimento della risposta non-cargo-cult (e ottenuto il tuo dottorato di ricerca nel processo) e quindi usa il tuo giudizio non intuitivo per selezionare K.

10
Un aspetto dell'indipendenza dalla risoluzione è che non si può dire con certezza quale sia uno "spostamento visibile" in fase di compilazione. Ciò che è invisibile su uno schermo super HD potrebbe benissimo essere ovvio su uno schermo minuscolo. Si dovrebbe almeno renderlo una funzione delle dimensioni dello schermo. O nominalo qualcos'altro.
Romain,

1
Ma almeno la selezione di "spostamento visibile" si basa su proprietà di visualizzazione (o frame) facilmente comprensibili, diversamente dalle <risposte corrette> Kche è difficile e non intuitivo da selezionare.
GoZoner,

5

La domanda corretta: come si confrontano i punti in Cocoa Touch?

La risposta corretta: CGPointEqualToPoint ().

Una domanda diversa: due valori calcolati sono uguali?

La risposta pubblicata qui: non lo sono.

Come verificare se sono vicini? Se vuoi controllare se sono vicini, non usare CGPointEqualToPoint (). Ma non controllare per vedere se sono vicini. Fai qualcosa che abbia senso nel mondo reale, come controllare se un punto è oltre una linea o se un punto è all'interno di una sfera.


4

L'ultima volta che ho verificato lo standard C, non era necessario che le operazioni in virgola mobile su doppio (64 bit in totale, 53 bit in mantissa) fossero più accurate di quella precisione. Tuttavia, alcuni componenti hardware potrebbero eseguire le operazioni nei registri con maggiore precisione e il requisito è stato interpretato in modo da non richiedere alcun requisito per cancellare i bit di ordine inferiore (oltre la precisione dei numeri caricati nei registri). Quindi potresti ottenere risultati inaspettati di confronti come questo a seconda di ciò che è rimasto nei registri da chiunque abbia dormito lì per ultimo.

Detto questo, e nonostante i miei sforzi per espanderlo ogni volta che lo vedo, l'outfit in cui lavoro ha un sacco di codice C che viene compilato usando gcc ed eseguito su Linux, e non abbiamo notato nessuno di questi risultati inaspettati da molto tempo . Non ho idea se questo sia perché gcc sta cancellando i bit di basso ordine per noi, i registri a 80 bit non vengono utilizzati per queste operazioni sui computer moderni, lo standard è stato modificato o cosa. Mi piacerebbe sapere se qualcuno può citare il capitolo e il verso.


1

È possibile utilizzare tale codice per confrontare float con zero:

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

Ciò si confronterà con una precisione di 0,1, abbastanza per CGFloat in questo caso.


Lanciare intsenza assicurare theView.frame.origin.xè dentro / vicino a quell'intervallo di intporta a un comportamento indefinito (UB) - o in questo caso, 1/100 dell'intervallo di int.
chux - Ripristina Monica il

Non c'è assolutamente alcun motivo per convertire in numeri interi come questo. Come diceva chux, esiste il potenziale per UB da valori fuori range; e su alcune architetture questo sarà significativamente più lento del semplice calcolo in virgola mobile. Infine, moltiplicando per 100 in questo modo verrà confrontato con una precisione di 0,01, non di 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;

}


0

Sto usando la seguente funzione di confronto per confrontare un numero di cifre decimali:

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
}

-6

Direi che la cosa giusta è dichiarare ogni numero come un oggetto, e quindi definire tre cose in quell'oggetto: 1) un operatore di uguaglianza. 2) un metodo setAcceptableDifference. 3) il valore stesso. L'operatore di uguaglianza restituisce vero se la differenza assoluta di due valori è inferiore al valore impostato come accettabile.

È possibile sottoclassare l'oggetto per adattarlo al problema. Ad esempio, barre tonde di metallo tra 1 e 2 pollici potrebbero essere considerate di uguale diametro se i loro diametri differivano di meno di 0,0001 pollici. Quindi chiameresti setAcceptableDifference con il parametro 0.0001 e poi utilizzeresti l'operatore di uguaglianza con sicurezza.


1
Questa non è una buona risposta. Innanzitutto, l'intera "cosa oggetto" non fa assolutamente nulla per risolvere il problema. E in secondo luogo, l'implementazione effettiva di "uguaglianza" non è in realtà quella corretta.
Tom Swirly,

3
Tom, forse ripenseresti alla "cosa oggetto". Con numeri reali, rappresentati con elevata precisione, raramente si verifica l'uguaglianza. Ma l' idea di uguaglianza può essere personalizzata se ti va bene. Sarebbe più bello se ci fosse un operatore scavalcabile "approssimativamente uguale", ma non lo è.
John White,
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.