2011-09-22 12 views
98

Il codice seguente funziona su Visual Studio 2008 con e senza ottimizzazione. Ma funziona solo su g ++ senza ottimizzazione (O0).Risultato in virgola mobile diverso con ottimizzazione abilitata - bug del compilatore?

#include <cstdlib> 
#include <iostream> 
#include <cmath> 

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 l'ottimizzazione (O1-O3) sarà in uscita:

4.5 
4.5 

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

Test su g ++ 4.1.2 e 4.4.4.

Ecco il risultato sul Ideone: http://ideone.com/Rz937

E l'opzione I test sul g ++ è semplice:

g++ -O2 round.cpp 

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

Ulteriore domanda:

Mi chiedevo, occorre rivolgersi sempre l'opzione -ffloat-store?

Poiché la versione g ++ che ho provato è fornito con CentOS/Red Hat Linux 5 e CentOS/RedHat 6.

Ho compilato molti dei miei programmi sotto queste piattaforme, 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?

C'è qualcuno interessato al motivo per cui anche /fp:fast è attivato, Visual Studio 2008 funziona ancora? Sembra che Visual   Studio   2008 sia più affidabile a questo problema di g ++?

+49

A tutti i nuovi utenti SO: QUESTO è il modo in cui si fa una domanda. +1 – tenfour

+1

FWIW, sto ottenendo l'output corretto con g ++ 4.5.0 usando MinGW. –

+0

Ottengo 4,5 4,6 per tutti i casi. Qual è la tua versione g ++? Ho g ++ (Debian 4.3.2-1.1) 4.3.2 –

risposta

79

I processori Intel x86 utilizzano internamente una precisione estesa di 80 bit, mentre lo standard 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.

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

In alternativa, utilizzare il tipo long double, che è normalmente largo 80 bit su gcc per evitare arrotondamenti dalla precisione da 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. 
+20

Penso che questa sia la risposta. La costante 4.55 viene convertita in 4.54999999999999 che è la rappresentazione binaria più vicina in 64 bit; moltiplicare per 10 e arrotondare di nuovo a 64 bit e si ottiene 45,5. Se si salta il passaggio di arrotondamento mantenendolo in un registro a 80 bit si finisce con 45.4999999999999. –

+0

Grazie, nemmeno io conosco questa opzione. Ma mi stavo chiedendo, devo sempre attivare l'opzione -ffloat-store? Poiché la versione g ++ che ho testato è distribuita con CentOS/Redhat 5 e CentOS/Redhat 6. Ho compilato molti miei programmi sotto queste piattaforme, sono preoccupato che causerà bug inattesi all'interno dei miei programmi. – Bear

+0

@Mark: Ma come spiegare se si disapprova l'istruzione di debug: std :: cout << "t:" << t << std :: endl; l'uscita sarà corretta ??? – Bear

6

Diversi compilatori hanno diverse impostazioni di ottimizzazione. Alcune di queste impostazioni di ottimizzazione più veloci non mantengono regole in virgola mobile rigorose in base a IEEE 754. Visual Studio ha un'impostazione specifica, /fp:strict, /fp:precise, /fp:fast, dove /fp:fast viola lo standard su ciò che può essere fatto. Potresti scoprire che questo flag è ciò che controlla l'ottimizzazione in tali impostazioni. È inoltre possibile 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.

+4

C'è un interruttore '-ffast-math' per GCC che, e non è attivato da nessuno dei livelli di ottimizzazione' -O' dal preventivo: "può generare output errato per i programmi che dipendono da un'implementazione esatta di IEEE o regole/specifiche ISO per le funzioni matematiche . " – Mat

+0

@Mat: ho provato '-ffast-math' e alcune altre cose sul mio' g ++ 4.4.3' e non riesco ancora a riprodurre il problema. – NPE

+0

Nice: con '-ffast-math' Io ottengo' 4.5' in entrambi i casi per livelli di ottimizzazione maggiori di '0'. –

3

Per coloro che non possono riprodurre il bug: non togliere il commento alla commentate stmts debug, influenzano il risultato.

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

Ulteriore domanda:

mi chiedevo, dovrebbe Accendo sempre l'opzione -ffloat-store?

Per essere irriverente, ci deve essere una ragione per cui alcuni programmatori non si accendono -ffloat-store, altrimenti l'opzione non esisterebbe (allo stesso modo, ci deve essere una ragione che alcuni programmatori fanno accendere -ffloat-store) . Non consiglierei di accenderlo sempre o di spegnerlo sempre. Accenderlo impedisce alcune ottimizzazioni, ma disattivarlo consente il tipo di comportamento che stai ottenendo.

Ma, in generale, c'è some mismatch tra numeri in virgola mobile binario (come il computer utilizza) e 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 che stai ottenendo è non causato da questa mancata corrispondenza, ma simile comportamento può essere). Il fatto è che, dato che hai già un po 'di vaghezza quando si ha a che fare con virgola mobile, non posso dire che lo -ffloat-store lo rende migliore o peggiore.

Invece, potresti voler esaminare other solutions al problema che stai cercando di risolvere (sfortunatamente, Koenig non indica il documento reale, e non riesco a trovare un ovvio posto "canonico" per esso , quindi dovrò inviarti a Google).


Se non sei arrotondamento per scopi di uscita, probabilmente guardare std::modf() (in cmath) e std::numeric_limits<double>::epsilon() (in limits).Ripensando la funzione originale round(), credo che sarebbe stato più pulito di 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 suggerisce 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<double>::epsilon()); 
} 

Un semplice nota: std::numeric_limits<T>::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, la scala epsilon in qualche modo per tenere conto del fatto che si sta lavorando con numeri diversi da "1"). La somma di d, .5 e std::numeric_limits<double>::epsilon() dovrebbe essere vicino a 1, quindi il raggruppamento di quell'aggiunta significa che std::numeric_limits<double>::epsilon() avrà le dimensioni giuste per quello che stiamo facendo. Se mai, std::numeric_limits<double>::epsilon() sarà troppo grande (quando la somma di tutti e tre è inferiore a uno) e potrebbe farci arrotondare alcuni numeri quando non dovremmo.


Al giorno d'oggi, si dovrebbe considerare std::nearbyint().

+0

Un "epsilon relativo" è chiamato 1 ulp (1 unità nell'ultimo posto). 'x - nextafter (x, INFINITY)' è relativo a 1 ulp per x (ma non usarlo, sono sicuro che ci sono casi d'angolo e l'ho appena inventato). L'esempio di cppreference per 'epsilon()' [ha un esempio di ridimensionamento per ottenere un errore relativo basato su ULP] (http://en.cppreference.com/w/cpp/types/numeric_limits/epsilon). –

+2

BTW, la risposta del 2016 a '-ffloat-store' è: non usare x87 in primo luogo. Usa la matematica SSE2 (file binari a 64 bit o '-mfpmath = sse -msse2' per creare vecchi binari a 32 bit croccanti), perché SSE/SSE2 ha provini senza precisione extra. 'double' e' float' vars nei registri XMM sono veramente in formato IEEE 64-bit o 32-bit. (A differenza di x87, in cui i registri sono sempre a 80 bit e si memorizzano in cicli di memoria a 32 o 64 bit.) –

10

uscita dovrebbe essere: 4.5 4.6 Questo è ciò che l'uscita sarebbe se tu avessi precisione infinita, o se si sta lavorando con un dispositivo che utilizza una rappresentazione decimale a base piuttosto che binario a base di virgola mobile. Ma non lo sei. La maggior parte dei computer utilizza lo standard IEEE in virgola mobile binario.

Come Maxim Yegorushkin già osservato nella sua risposta, parte del problema è che internamente il computer utilizza una rappresentazione in virgola mobile 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 si desidera che il proprio arrotondamento sia in grado di arrotondare in modo affidabile questi casi di angolo, è necessario un algoritmo di arrotondamento che risolva il fatto che n.n5, n.nn5 o n.nnn5, ecc. (Ma non n. 5) è sempre inesatto. 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.

Vedere How does Excel successfully Rounds Floating numbers even though they are imprecise? per un tale algoritmo.

Oppure si può semplicemente convivere con il fatto che i casi d'angolo a volte girano erroneamente.

0

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.

Problemi correlati