Differente risultato in virgola mobile con ottimizzazione abilitata – bug del compilatore?

Il codice seguente funziona su Visual Studio 2008 con e senza ottimizzazione. Ma funziona solo su g ++ senza ottimizzazione (O0).

#include  #include  #include  double round(double v, double digit) { double pow = std::pow(10.0, digit); double t = v * pow; //std::cout << "t:" << t << std::endl; double r = std::floor(t + 0.5); //std::cout << "r:" << r << std::endl; return r / pow; } int main(int argc, char *argv[]) { std::cout << round(4.45, 1) << std::endl; std::cout << round(4.55, 1) << std::endl; } 

L’output dovrebbe essere:

 4.5 4.6 

Ma g ++ con ottimizzazione ( O1O3 ) produrrà:

 4.5 4.5 

Se aggiungo la parola chiave volatile prima di t, funziona, quindi potrebbe esserci qualche tipo di bug di ottimizzazione?

Prova su g ++ 4.1.2 e 4.4.4.

Ecco il risultato su ideone: http://ideone.com/Rz937

E l’opzione che collaudo su g ++ è semplice:

 g++ -O2 round.cpp 

Il risultato più interessante, anche accendo /fp:fast opzione /fp:fast su Visual Studio 2008, il risultato è ancora corretto.

Ulteriore domanda:

Mi stavo chiedendo, dovrei sempre triggersre l’opzione -ffloat-store ?

Poiché la versione g ++ che ho testato è fornita con CentOS / Red Hat Linux 5 e CentOS / Redhat 6 .

Ho compilato molti dei miei programmi sotto queste piattaforms, e sono preoccupato che causerà bug inaspettati all’interno dei miei programmi. Sembra un po ‘difficile esaminare tutto il mio codice C ++ e le librerie usate se hanno problemi simili. Qualche suggerimento?

Qualcuno è interessato a perché even /fp:fast triggersto, Visual Studio 2008 funziona ancora? Sembra che Visual Studio 2008 sia più affidabile a questo problema di g ++?

I processori Intel x86 utilizzano internamente una precisione estesa di 80 bit, mentre il double è normalmente largo 64 bit. Diversi livelli di ottimizzazione influenzano la frequenza con cui i valori in virgola mobile della CPU vengono salvati in memoria e quindi arrotondati dalla precisione a 80 bit alla precisione a 64 bit.

Usa l’opzione gcc -ffloat-store per ottenere gli stessi risultati in virgola mobile con diversi livelli di ottimizzazione.

In alternativa, usa il tipo long double , che di solito è largo 80 bit su gcc per evitare arrotondamenti dalla precisione a 80-bit a 64-bit.

man gcc dice tutto:

  -ffloat-store Do not store floating point variables in registers, and inhibit other options that might change whether a floating point value is taken from a register or memory. This option prevents undesirable excess precision on machines such as the 68000 where the floating registers (of the 68881) keep more precision than a "double" is supposed to have. Similarly for the x86 architecture. For most programs, the excess precision does only good, but a few programs rely on the precise definition of IEEE floating point. Use -ffloat-store for such programs, after modifying them to store all pertinent intermediate computations into variables. 

L’output dovrebbe essere: 4.5 4.6 Ecco cosa sarebbe l’output se avessi una precisione infinita, o se stessimo lavorando con un dispositivo che utilizzava una rappresentazione in virgola mobile basata su decimale piuttosto che su un punto binario. Ma non lo sei. La maggior parte dei computer utilizza lo standard IEEE in virgola mobile binario.

Come ha già osservato Maxim Yegorushkin nella sua risposta, parte del problema è che internamente il tuo computer sta usando una rappresentazione in virgola mobile a 80 bit. Questo è solo una parte del problema, però. La base del problema è che qualsiasi numero del modulo n.nn5 non ha una rappresentazione fluttuante binaria esatta. Questi casi angolari sono sempre numeri inesatti.

Se vuoi davvero che il tuo arrotondamento sia in grado di arrotondare in modo affidabile questi casi d’angolo, hai bisogno di un algoritmo di arrotondamento che risolva il fatto che n.n5, n.nn5, o n.nnn5, ecc. (Ma non n.5) è sempre inesatta. Trova il caso d’angolo che determina se un valore di input arrotonda o diminuisca e restituisca il valore arrotondato o arrotondato in base a un confronto con questo caso d’angolo. E devi fare in modo che un compilatore ottimizzante non inserisca il caso dell’angolo trovato in un registro di precisione esteso.

Vedi Come Excel arrotonda con successo i numeri, anche se sono imprecisi? per un tale algoritmo.

Oppure puoi semplicemente convivere con il fatto che i casi d’angolo a volte girano erroneamente.

Diversi compilatori hanno diverse impostazioni di ottimizzazione. Alcune di queste impostazioni di ottimizzazione più veloci non mantengono regole in virgola mobile rigorose secondo IEEE 754 . Visual Studio ha un’impostazione specifica, /fp:strict , /fp:precise , /fp:fast , dove /fp:fast viola lo standard su cosa può essere fatto. Potresti scoprire che questo flag è ciò che controlla l’ottimizzazione in tali impostazioni. È inoltre ansible trovare un’impostazione simile in GCC che modifica il comportamento.

Se questo è il caso, l’unica differenza tra i compilatori è che GCC cercherà il comportamento in virgola mobile più veloce per impostazione predefinita su ottimizzazioni più elevate, mentre Visual Studio non modifica il comportamento in virgola mobile con livelli di ottimizzazione più elevati. Quindi potrebbe non essere necessariamente un bug reale, ma un comportamento intenzionale di un’opzione che non sapevi che stavi accendendo.

A coloro che non riescono a riprodurre il bug: non decommentare gli stm di debug commentati, essi influenzano il risultato.

Ciò implica che il problema è correlato alle istruzioni di debug. E sembra che ci sia un errore di arrotondamento causato dal caricamento dei valori nei registri durante le istruzioni di output, motivo per cui altri hanno scoperto che è ansible risolvere questo problema con -ffloat-store

Ulteriore domanda:

Mi stavo chiedendo, dovrei sempre triggersre l’opzione -ffloat-store ?

Per essere irriverenti, ci deve essere un motivo per cui alcuni programmatori non triggersno -ffloat-store , altrimenti l’opzione non esisterebbe (allo stesso modo, ci deve essere un motivo per cui alcuni programmatori triggersno -ffloat-store ). Non consiglierei di accenderlo sempre o di spegnerlo sempre. Accenderlo impedisce alcune ottimizzazioni, ma distriggersrlo consente il tipo di comportamento che stai ottenendo.

Ma, in generale, c’è qualche discrepanza tra i numeri in virgola mobile binario (come il computer usa) ei numeri decimali in virgola mobile (che le persone hanno familiarità con), e che la mancata corrispondenza può causare un comportamento simile a quello che ottieni (per essere chiari, il comportamento stai ricevendo non è causato da questa mancata corrispondenza, ma un comportamento simile può essere). Il fatto è che, dato che hai già un po ‘di vaghezza quando si ha a che fare con il floating point, non posso dire che -ffloat-store renda migliore o peggiore.

Invece, potresti voler cercare altre soluzioni al problema che stai cercando di risolvere (sfortunatamente, Koenig non indica il documento reale, e non riesco a trovare un ovvio posto “canonico” per questo, quindi Dovrai mandarti su Google ).


Se non stai arrotondando per scopi di output, probabilmente guarderei std::modf() (in cmath ) e std::numeric_limits::epsilon() (nei limits ). Pensando alla funzione round() originale, credo che sarebbe più pulito sostituire la chiamata a std::floor(d + .5) con una chiamata a questa funzione:

 // this still has the same problems as the original rounding function int round_up(double d) { // return value will be coerced to int, and truncated as expected // you can then assign the int to a double, if desired return d + 0.5; } 

Penso che suggerisca il seguente miglioramento:

 // this won't work for negative d ... // this may still round some numbers up when they should be rounded down int round_up(double d) { double floor; d = std::modf(d, &floor); return floor + (d + .5 + std::numeric_limits::epsilon()); } 

Una semplice nota: std::numeric_limits::epsilon() è definito come “il numero più piccolo aggiunto a 1 che crea un numero diverso da 1.” Di solito è necessario utilizzare un epsilon relativo (ad esempio, ridimensionare epsilon in qualche modo per tenere conto del fatto che si sta lavorando con numeri diversi da “1”). La sum di d , .5 e std::numeric_limits::epsilon() dovrebbe essere vicina a 1, quindi raggruppare std::numeric_limits::epsilon() significa che std::numeric_limits::epsilon() avrà la dimensione giusta per cosa stiamo facendo Se qualcosa, std::numeric_limits::epsilon() sarà troppo grande (quando la sum di tutti e tre è inferiore a uno) e potrebbe farci arrotondare alcuni numeri quando non dovremmo.


Al giorno d’oggi, dovresti considerare std::nearbyint() .

La risposta accettata è corretta se si sta compilando su un target x86 che non include SSE2. Tutti i moderni processori x86 supportano SSE2, quindi se puoi sfruttarlo, dovresti:

 -mfpmath=sse -msse2 -ffp-contract=off 

Rompiamo questo.

-mfpmath=sse -msse2 . Questo esegue l’arrotondamento utilizzando i registri SSE2, che è molto più veloce rispetto alla memorizzazione di tutti i risultati intermedi nella memoria. Notare che questo è già l’impostazione predefinita su GCC per x86-64. Dal wiki di GCC :

Su processori x86 più moderni che supportano SSE2, specificando le opzioni del compilatore -mfpmath=sse -msse2 garantisce che tutte le operazioni float e double vengano eseguite nei registri SSE e arrotondate correttamente. Queste opzioni non influenzano l’ABI e dovrebbero pertanto essere utilizzate ogni qualvolta ansible per risultati numerici prevedibili.

-ffp-contract=off . Tuttavia, controllare l’arrotondamento non è sufficiente per una corrispondenza esatta. Le istruzioni FMA (fuse multiple-add) possono modificare il comportamento dell’arrotondamento rispetto alle sue controparti non fuse, quindi è necessario disabilitarlo. Questo è l’impostazione predefinita su Clang, non su GCC. Come spiegato da questa risposta :

Un FMA ha un solo arrotondamento (mantiene effettivamente una precisione infinita per il risultato di moltiplicazione temporaneo interno), mentre un ADD + MUL ne ha due.

Disabilitando FMA, otteniamo risultati che corrispondono esattamente a debug e release, al costo di alcune prestazioni (e accuratezza). Possiamo ancora trarre vantaggio dagli altri vantaggi prestazionali di SSE e AVX.

Personalmente, ho riscontrato lo stesso problema andando dall’altra parte: da gcc a VS. Nella maggior parte dei casi, penso che sia meglio evitare l’ottimizzazione. L’unica volta che vale la pena è quando hai a che fare con metodi numerici che coinvolgono grandi matrici di dati in virgola mobile. Anche dopo averlo smontato, sono spesso deluso dalle scelte dei compilatori. Molto spesso è solo più semplice usare i componenti intrinseci del compilatore o semplicemente scrivere l’assembly da soli.

Ho approfondito questo problema e posso portare più precisioni. Innanzitutto, le rappresentazioni esatte di 4.45 e 4.55 secondo gcc su x84_64 sono le seguenti (con libquadmath per stampare l’ultima precisione):

 float 32: 4.44999980926513671875 double 64: 4.45000000000000017763568394002504646778106689453125 doublex 80: 4.449999999999999999826527652402319290558807551860809326171875 quad 128: 4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125 float 32: 4.55000019073486328125 double 64: 4.54999999999999982236431605997495353221893310546875 doublex 80: 4.550000000000000000173472347597680709441192448139190673828125 quad 128: 4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875 

Come detto sopra, il problema è dovuto alla dimensione di 80 bit dei registri della FPU.

Ma perché il problema non si verifica mai su Windows? su IA-32, la FPU x87 è stata configurata per utilizzare una precisione interna per la mantissa di 53 bit (equivalente a una dimensione totale di 64 bit: double ). Per Linux e Mac OS, è stata utilizzata la precisione predefinita di 64 bit (equivalente a una dimensione totale di 80 bit: long double ). Quindi il problema dovrebbe essere ansible, o no, su queste piattaforms diverse cambiando la parola di controllo della FPU (supponendo che la sequenza di istruzioni possa innescare il bug). Il problema è stato segnalato a gcc come bug 323 (leggi almeno il commento 92!).

Per mostrare la precisione della mantissa su Windows, puoi compilarlo a 32 bit con VC ++:

 #include "stdafx.h" #include  #include  int main(void) { char t[] = { 64, 53, 24, -1 }; unsigned int cw = _control87(0, 0); printf("mantissa is %d bits\n", t[(cw >> 16) & 3]); } 

e su Linux / Cygwin:

 #include  int main(int argc, char **argv) { char t[] = { 24, -1, 53, 64 }; unsigned int cw = 0; __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw)); printf("mantissa is %d bits\n", t[(cw >> 8) & 3]); } 

Si noti che con gcc è ansible impostare la precisione della FPU con -mpc32/64/80 , anche se viene ignorata in Cygwin. Ma tieni presente che modificherà la dimensione della mantissa, ma non quella dell’esponente, lasciando la porta aperta ad altri tipi di comportamenti diversi.

Sull’architettura x86_64, SSE viene usato come detto da tmandry , quindi il problema non si verificherà a meno che non si -mfpmath=387 la vecchia FPU x87 per il calcolo FP con -mfpmath=387 , oa meno che non si compili in modalità 32 bit con -m32 (sarà necessario il multilib pacchetto). Potrei riprodurre il problema su Linux con diverse combinazioni di flag e versioni di gcc:

 g++-5 -m32 floating.cpp -O1 g++-8 -mfpmath=387 floating.cpp -O1 

Ho provato alcune combinazioni su Windows o Cygwin con VC ++ / gcc / tcc ma il bug non si è mai verificato. Suppongo che la sequenza di istruzioni generate non sia la stessa.

Infine, si noti che un modo esotico per prevenire questo problema con 4.45 o 4.55 sarebbe usare _Decimal32/64/128 , ma il supporto è davvero scarso … Ho passato molto tempo solo per poter fare un printf con libdfp !