Se copio un float su un'altra variabile, saranno uguali?


167

So che usare ==per verificare l'uguaglianza delle variabili in virgola mobile non è un buon modo. Ma voglio solo saperlo con le seguenti dichiarazioni:

float x = ...

float y = x;

assert(y == x)

Dal momento che yviene copiato x, l'affermazione sarà vera?


78
Consentitemi di fornire una taglia di 50 a qualcuno che dimostra effettivamente la disuguaglianza dimostrando con codice reale. Voglio vedere la cosa 80 vs 64 bit in azione. Inoltre altri 50 per una spiegazione del codice assembler generato che mostra che una variabile si trova in un registro e l'altra no (o qualunque sia la ragione della disuguaglianza, mi piacerebbe spiegarla a un livello basso).
Thomas Weller,

1
@ThomasWeller il bug GCC su questo: gcc.gnu.org/bugzilla/show_bug.cgi?id=323 ; tuttavia, ho appena provato a riproporlo su un sistema x86-64 e non funziona, anche con -ffast-math. Ho il sospetto che tu abbia bisogno di un vecchio GCC su un sistema a 32 bit.
pjc50,

5
@ pjc50: in realtà è necessario un sistema a 80 bit per riprodurre il bug 323; è la FPU 80x87 che ha causato il problema. x86-64 utilizza l'FPU SSE. I bit extra causano il problema, perché sono arrotondati quando si riversa un valore su un float a 32 bit.
MSalter

4
Se la teoria di MSalters è corretta (e sospetto che lo sia), allora puoi riprogrammare compilando per 32-bit ( -m32) o istruendo GCC ad usare l'FPU x87 ( -mfpmath=387).
Cody Grey

4
Cambia "48 bit" in "80 bit", quindi puoi rimuovere l'aggettivo "mitico" lì, @Hot. Questo è esattamente ciò che è stato discusso immediatamente prima del tuo commento. L'x87 (FPU per l'architettura x86) utilizza registri a 80 bit, un formato di "precisione estesa".
Cody Grey

Risposte:


125

Oltre al assert(NaN==NaN);caso indicato da kmdreko, puoi avere situazioni con x87-math, quando i float a 80 bit vengono temporaneamente archiviati in memoria e successivamente confrontati con valori che sono ancora memorizzati in un registro.

Possibile esempio minimo, che fallisce con gcc9.2 quando compilato con -O2 -m32:

#include <cassert>

int main(int argc, char**){
    float x = 1.f/(argc+2);
    volatile float y = x;
    assert(x==y);
}

Demo Godbolt: https://godbolt.org/z/X-Xt4R

La volatileprobabilmente può essere omesso, se si riesce a creare sufficienti registro pressione di aver yconservato e ricaricato dalla memoria (ma confondere il compilatore abbastanza, di non omettere il confronto tutto insieme).

Vedi riferimento FAQ GCC:


2
Sembra strano che i bit extra vengano presi in considerazione nel confronto tra una floatprecisione standard e una precisione extra.
Nat

13
@Nat Si è strano; questo è un bug .
Corse di leggerezza in orbita

13
@ThomasWeller No, è un premio ragionevole. Anche se vorrei che la risposta facesse notare che si tratta di un comportamento non conforme
gare di leggerezza in orbita

4
Posso estendere questa risposta, sottolineando cosa succede esattamente nel codice assembly e che ciò viola effettivamente lo standard, anche se non mi definirei un avvocato del linguaggio, quindi non posso garantire che non ci sia un oscuro clausola che consente esplicitamente tale comportamento. Suppongo che l'OP fosse più interessato a complicazioni pratiche sui compilatori reali, non su compilatori completamente privi di bug e pienamente conformi (il che di fatto non esiste, immagino).
CHTZ

4
Vale la pena ricordare che -ffloat-storesembra essere il modo per impedirlo.
OrangeDog

116

Non sarà vero se lo xè NaN, poiché i confronti su NaNsono sempre falsi (sì, anche NaN == NaN). Per tutti gli altri casi (valori normali, valori non normali, infiniti, zeri) questa affermazione sarà vera.

Il consiglio per evitare i ==float si applica ai calcoli a causa del fatto che i numeri in virgola mobile non sono in grado di esprimere molti risultati esattamente quando usati nelle espressioni aritmetiche. L'assegnazione non è un calcolo e non vi è alcun motivo per cui l'assegnazione produrrebbe un valore diverso dall'originale.


La valutazione di precisione estesa dovrebbe essere un problema se si segue lo standard. Da <cfloat>ereditato da C [5.2.4.2.2.8] ( sottolineatura mia ):

Ad eccezione di assegnazione e cast (che rimuovono tutta la gamma e la precisione extra) , i valori delle operazioni con operandi flottanti e valori soggetti alle solite conversioni aritmetiche e delle costanti fluttuanti vengono valutati in un formato la cui gamma e precisione possono essere maggiori di quelle richieste dal genere.

Tuttavia, come hanno sottolineato i commenti, alcuni casi con determinati compilatori, opzioni di build e obiettivi potrebbero renderlo paradossalmente falso.


10
Cosa succede se xviene calcolato in un registro nella prima riga, mantenendo più precisione del minimo per a float. La y = xpuò essere in memoria, mantenendo solo floatla precisione. Quindi il test per l'uguaglianza verrebbe eseguito con la memoria rispetto al registro, con diverse precisazioni, e quindi nessuna garanzia.
David Schwartz,

5
x+pow(b,2)==x+pow(a,3)potrebbe differire dal auto one=x+pow(b,2); auto two=y+pow(a,3); one==twofatto che uno potrebbe confrontare usando una maggiore precisione rispetto all'altro (se uno / due sono valori a 64 bit in ram, mentre i valori intermedi sono 80ish bit su fpu). Quindi l'incarico può fare qualcosa, a volte.
Yakk - Adam Nevraumont il

22
@evg Certo! La mia risposta segue semplicemente lo standard. Tutte le scommesse sono disattivate se dici al tuo compilatore di non confondere, specialmente quando abiliti la matematica veloce.
kmdreko,

11
@Voo Vedi la citazione nella mia risposta. Il valore di RHS è assegnato alla variabile su LHS. Non esiste alcuna giustificazione legale per il valore risultante dell'LHS diverso dal valore dell'RHS. Apprezzo il fatto che diversi compilatori abbiano dei bug in questo senso. Ma se qualcosa è memorizzato in un registro non dovrebbe avere nulla a che fare con esso.
Corse di leggerezza in orbita

6
@Voo: in ISO C ++, l'arrotondamento per digitare larghezza dovrebbe avvenire in qualsiasi compito. Nella maggior parte dei compilatori che hanno come target x87, succede davvero solo quando il compilatore decide di versare / ricaricare. È possibile forzarlo gcc -ffloat-storeper una rigorosa conformità. Ma questa domanda è circa x=y; x==y; senza fare nulla per variare una via di mezzo. Se yè già arrotondato per adattarsi a un float, la conversione in double o long double e back non modificherà il valore. ...
Peter Cordes,

34

Sì, yassumerà sicuramente il valore di x:

[expr.ass]/2: Nell'assegnazione semplice (=), l'oggetto a cui fa riferimento l'operando di sinistra viene modificato ([defns.access]) sostituendo il suo valore con il risultato dell'operando di destra.

Non vi è margine di manovra per l'assegnazione di altri valori.

(Altri hanno già sottolineato che un confronto di equivalenza ==valuterà comunque i falsevalori di NaN.)

Il solito problema con il virgola mobile ==è che è facile non avere abbastanza il valore che pensi di avere. Qui, sappiamo che i due valori, qualunque essi siano, sono gli stessi.


7
@ThomasWeller È un bug noto in un'implementazione di conseguenza non conforme. Buono a dirlo però!
Corse di leggerezza in orbita

Inizialmente, ho pensato che il linguaggio legiferare sulla distinzione tra "valore" e "risultato" sarebbe perverso, ma questa distinzione non è richiesta per essere senza differenza dalla lingua di C2.2, 7.1.6; C3.3, 7.1.6; C4.2, 7.1.6 o C5.3, 7.1.6 del progetto di norma che citi.
Eric Towers

@EricTowers Spiacente, puoi chiarire quei riferimenti? Non trovo quello che stai indicando
gare di leggerezza in orbita il

@ LightnessRacesBY-SA3.0: C . C2.2 , C3.3 , C4.2 e C5.3 .
Eric Towers,

@EricTowers Sì, ancora non ti seguo. Il tuo primo link va all'indice dell'Appendice C (non mi dice nulla). I tuoi prossimi quattro link vanno tutti a [expr]. Se devo ignorare i collegamenti e concentrarmi sulle citazioni, rimango con la confusione che ad esempio C.5.3 non sembra indirizzare l'uso del termine "valore" o del termine "risultato" (anche se lo fa usa "risultato" una volta nel suo normale contesto inglese). Forse potresti descrivere più chiaramente dove pensi che lo standard faccia una distinzione e fornire un'unica chiara citazione a ciò che accade. Grazie!
Corse di leggerezza in orbita il

3

Sì, in tutti i casi (trascurando NaN e problemi x87), questo sarà vero.

Se lo fai memcmpsu di loro sarai in grado di testare l'uguaglianza mentre sarai in grado di confrontare NaN e sNaN. Ciò richiederà anche che il compilatore prenda l'indirizzo della variabile che forzerà il valore in un 32 bit floatanziché in uno a 80 bit. Ciò eliminerà i problemi di x87. La seconda affermazione qui non intende mostrare che ==non confronterà NaN come veri:

#include <cmath>
#include <cassert>
#include <cstring>

int main(void)
{
    float x = std::nan("");
    float y = x;
    assert(!std::memcmp(&y, &x, sizeof(float)));
    assert(y == x);
    return 0;
}

Si noti che se i NaN hanno una rappresentazione interna diversa (cioè mantissa diversa), il memcmpconfronto non sarà vero.


1

In casi normali, verrebbe valutato come vero. (o l'istruzione assert non farà nulla)

Modifica :

Per "casi usuali" intendo escludere gli scenari summenzionati (come valori di NaN e unità a virgola mobile 80x87) come indicato da altri utenti.

Data l'ossolenza dei chip 8087 nel contesto odierno, il problema è piuttosto isolato e per la domanda applicabile nell'attuale stato dell'architettura a virgola mobile utilizzata, vale per tutti i casi ad eccezione dei NaN.

(riferimento circa 8087 - https://home.deec.uc.pt/~jlobo/tc/artofasm/ch14/ch143.htm )

Complimenti a @chtz per aver riprodotto un buon esempio e @kmdreko per aver menzionato NaN - non ne erano a conoscenza prima!


1
Ho pensato che fosse del tutto possibile xessere in un registro a virgola mobile mentre yè caricato dalla memoria. La memoria potrebbe avere meno precisione di un registro, causando il fallimento del confronto.
David Schwartz,

1
Questo potrebbe essere un caso per un falso, non ho pensato così lontano. (dal momento che l'OP non ha fornito casi particolari, non presumo alcun vincolo aggiuntivo)
Anirban166

1
Non capisco davvero cosa stai dicendo. Come capisco la domanda, l'OP si sta chiedendo se la copia di un float e la verifica dell'uguaglianza sono garantite per avere successo. La tua risposta sembra dire "sì". Sto chiedendo perché la risposta non è no.
David Schwartz,

6
La modifica rende questa risposta errata. Lo standard C ++ richiede che l'assegnazione converta il valore nel tipo di destinazione: la precisione in eccesso può essere utilizzata nelle valutazioni delle espressioni ma non può essere mantenuta attraverso l'assegnazione. Non ha importanza se il valore è contenuto in un registro o in una memoria; lo standard C ++ richiede che sia, man mano che il codice viene scritto, un floatvalore senza ulteriore precisione.
Eric Postpischil,

2
@AProgrammer Dato che un compilatore (n estremamente) buggy potrebbe teoricamente causare int a=1; int b=a; assert( a==b );un'asserzione, penso che abbia senso rispondere a questa domanda in relazione a un compilatore che funzioni correttamente (pur notando che alcune versioni di alcuni compilatori hanno / hanno - noto - per sbagliare). In termini pratici, se per qualche motivo un compilatore non rimuove la precisione aggiuntiva dal risultato di un'assegnazione memorizzata nel registro, dovrebbe farlo prima di utilizzare quel valore.
TripeHound,

-1

Sì, restituirà True sempre, tranne se è NaN . Se il valore della variabile è NaN , restituisce sempre False !

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.