Risultato in virgola mobile diverso con ottimizzazione abilitata - bug del compilatore?


109

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

#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 ottimizzazione ( O1- O3) produrrà:

4.5
4.5

Se aggiungo la volatileparola chiave prima di t, funziona, quindi potrebbe esserci una sorta di bug di ottimizzazione?

Test su g ++ 4.1.2 e 4.4.4.

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

E l'opzione che provo su g ++ è semplice:

g++ -O2 round.cpp

Il risultato più interessante, anche se accendo l' /fp:fastopzione su Visual Studio 2008, il risultato è comunque corretto.

Ulteriore domanda:

Mi chiedevo, devo sempre attivare l' -ffloat-storeopzione?

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

Ho compilato molti dei miei programmi su queste piattaforme e sono preoccupato che possa causare bug imprevisti all'interno dei miei programmi. Sembra un po 'difficile indagare su tutto il mio codice C ++ e sulle librerie utilizzate se hanno tali problemi. Qualche suggerimento?

Qualcuno è interessato al motivo per cui anche se /fp:fastacceso, Visual Studio 2008 funziona ancora? Sembra che Visual Studio 2008 sia più affidabile su questo problema di g ++?


51
A tutti i nuovi utenti SO: QUESTO è il modo in cui poni una domanda. +1
tenfour

1
FWIW, ottengo l'output corretto con g ++ 4.5.0 utilizzando MinGW.
Steve Blackwell,

2
ideone usa 4.3.4 ideone.com/b8VXg
Daniel A. White

5
Dovresti tenere a mente che è improbabile che la tua routine funzioni in modo affidabile con tutti i tipi di output. In contrasto con l'arrotondamento di un doppio a un numero intero, questo è vulnerabile al fatto che non tutti i numeri reali possono essere rappresentati, quindi dovresti aspettarti di ottenere più bug come questo.
Jakub Wieczorek

2
Per coloro che non possono riprodurre il bug: non rimuovere il commento dagli stmt di debug commentati, influenzano il risultato.
n. 'pronomi' m.

Risposte:


91

I processori Intel x86 utilizzano internamente una precisione estesa a 80 bit, mentre doublenormalmente è larga 64 bit. Diversi livelli di ottimizzazione influiscono sulla frequenza con cui i valori in virgola mobile della CPU vengono salvati in memoria e quindi arrotondati da una precisione a 80 bit a una precisione a 64 bit.

Utilizza l' -ffloat-storeopzione gcc per ottenere gli stessi risultati in virgola mobile con diversi livelli di ottimizzazione.

In alternativa, usa il long doubletipo, che normalmente è largo 80 bit su gcc per evitare l'arrotondamento da una 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.

Nelle build x86_64 i compilatori utilizzano i registri SSE per floate doubleper impostazione predefinita, in modo che non venga utilizzata alcuna precisione estesa e questo problema non si verifichi.

gccl'opzione del compilatore lo-mfpmath controlla.


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 salti il ​​passaggio di arrotondamento mantenendolo in un registro a 80 bit, ti ritroverai con 45.4999999999999.
Mark Ransom

Grazie, non conosco nemmeno questa opzione. Ma mi chiedevo, devo sempre attivare l'opzione -ffloat-store? Poiché la versione g ++ che ho testato viene fornita con CentOS / Redhat 5 e CentOS / Redhat 6. Ho compilato molti dei miei programmi su queste piattaforme, temo che ciò possa causare bug imprevisti all'interno dei miei programmi.
Bear

5
@Bear, l'istruzione di debug probabilmente fa sì che il valore venga scaricato da un registro in memoria.
Mark Ransom

2
@Bear, normalmente la tua applicazione dovrebbe beneficiare di una precisione estesa, a meno che non operi su valori estremamente piccoli o enormi quando ci si aspetta che un float a 64 bit sia in eccesso o in overflow e produca inf. Non esiste una buona regola pratica, i test unitari possono darti una risposta definitiva.
Maxim Egorushkin,

2
@bear Come regola generale, se hai bisogno di risultati perfettamente prevedibili e / o esattamente ciò che un essere umano otterrebbe facendo le somme sulla carta, dovresti evitare il virgola mobile. -ffloat-store rimuove una fonte di imprevedibilità ma non è una bacchetta magica.
plugwash

10

L'output dovrebbe essere: 4.5 4.6 Questo è quello che sarebbe l'output se avessi una precisione infinita o se lavorassi con un dispositivo che utilizza una rappresentazione in virgola mobile basata su decimali invece che binaria. Ma non lo sei. La maggior parte dei computer utilizza lo standard binario a virgola mobile IEEE.

Come Maxim Yegorushkin ha già notato nella sua risposta, parte del problema è che internamente il tuo computer utilizza una rappresentazione in virgola mobile a 80 bit. Questa è solo una parte del problema, però. La base del problema è che qualsiasi numero nella forma n.nn5 non ha un'esatta rappresentazione binaria flottante. Quei casi d'angolo 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 affronti 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 viene arrotondato per eccesso o per difetto e restituisci il valore arrotondato per eccesso o per difetto in base a un confronto con questo caso d'angolo. Ed è necessario fare attenzione che un compilatore ottimizzato non inserisca il caso d'angolo trovato in un registro di precisione esteso.

Vedi In che modo Excel arrotonda correttamente i numeri mobili anche se sono imprecisi? per un tale algoritmo.

Oppure puoi semplicemente convivere con il fatto che i casi d'angolo a volte si arrotondano in modo errato.


6

Diversi compilatori hanno impostazioni di ottimizzazione diverse. Alcune di queste impostazioni di ottimizzazione più veloci non mantengono rigide regole in virgola mobile secondo IEEE 754 . Visual Studio ha un'impostazione specifica, /fp:strict, /fp:precise, /fp:fast, in cui /fp:fastviola la serie su cosa si può fare. 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 cosa diversa 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 il comportamento previsto di un'opzione che non sapevi di attivare.


4
C'è un -ffast-mathinterruttore per GCC che, e non è attivato da nessuno dei -Olivelli di ottimizzazione dalla citazione: "può risultare in un output non corretto per i programmi che dipendono da un'esatta implementazione delle regole / specifiche IEEE o ISO per le funzioni matematiche."
Sabato

@ Mat: ho provato -ffast-mathe poche altre cose sul mio g++ 4.4.3e non sono ancora in grado di riprodurre il problema.
NPE

Bello: con -ffast-mathottengo 4.5in entrambi i casi livelli di ottimizzazione maggiori di 0.
Kerrek SB

(Correzione: ottengo 4.5con -O1e -O2, ma non con -O0e -O3in GCC 4.4.3, ma con -O1,2,3in GCC 4.6.1.)
Kerrek SB

4

Per coloro che non possono riprodurre il bug: non rimuovere il commento dagli stmt di debug commentati, 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 puoi risolverlo con-ffloat-store

Ulteriore domanda:

Mi chiedevo, devo sempre attivare l' -ffloat-storeopzione?

Per essere impertinenti, 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 per cui alcuni programmatori si accendono -ffloat-store). Non consiglierei di accenderlo sempre o spegnerlo sempre. Attivarlo impedisce alcune ottimizzazioni, ma disattivarlo consente il tipo di comportamento che stai ottenendo.

Ma, generalmente, c'è qualche discrepanza tra i numeri in virgola mobile binari (come usa il computer) e i numeri in virgola mobile decimale (con cui le persone hanno familiarità), e quella mancata corrispondenza può causare un comportamento simile a quello che stai ottenendo (per essere chiari, il comportamento che stai ottenendo non è causato da questa mancata corrispondenza, ma un comportamento simile può essere). Il fatto è che, dal momento che hai già un po 'di vaghezza quando hai a che fare con il punto mobile, non posso dire che lo -ffloat-storerenda migliore o peggiore.

Invece, potresti voler cercare altre soluzioni al problema che stai cercando di risolvere (sfortunatamente, Koenig non indica il documento vero e proprio e non riesco davvero a trovare un posto "canonico" ovvio per esso, quindi ho dovrò inviarti a Google ).


Se non stai arrotondando per scopi di output, probabilmente guarderei std::modf()(in cmath) e std::numeric_limits<double>::epsilon()(in limits). Pensando alla round()funzione 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 ciò 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<double>::epsilon());
}

Una 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 (ovvero ridimensionare epsilon in qualche modo per tenere conto del fatto che si sta lavorando con numeri diversi da "1"). La somma di d, .5e std::numeric_limits<double>::epsilon()dovrebbe essere vicino 1, quindi il raggruppamento che i mezzi aggiunta che std::numeric_limits<double>::epsilon()sarà circa la dimensione giusta per quello che stiamo facendo. Se non altro, 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, dovresti considerare std::nearbyint().


Un "epsilon relativo" è chiamato 1 ulp (1 unità all'ultimo posto). x - nextafter(x, INFINITY)è correlato a 1 ulp per x (ma non usarlo; sono sicuro che ci sono casi d'angolo e l'ho appena inventato). L'esempio cppreference per epsilon() ha un esempio di ridimensionamento per ottenere un errore relativo basato su ULP .
Peter Cordes

2
A proposito, la risposta del 2016 -ffloat-storeè: non usare x87 in primo luogo. Usa la matematica SSE2 (binari a 64 bit o -mfpmath=sse -msse2per creare vecchi binari a 32 bit), perché SSE / SSE2 ha temporanei senza ulteriore precisione. doublee le floatvariabili nei registri XMM sono in realtà in formato IEEE a 64 o 32 bit. (A differenza di x87, dove i registri sono sempre a 80 bit e l'archiviazione in memoria esegue giri a 32 o 64 bit.)
Peter Cordes

3

La risposta accettata è corretta se si sta compilando su una destinazione x86 che non include SSE2. Tutti i moderni processori x86 supportano SSE2, quindi se puoi trarne vantaggio, dovresti:

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

Analizziamolo.

-mfpmath=sse -msse2. Ciò esegue l'arrotondamento utilizzando i registri SSE2, che è molto più veloce rispetto all'archiviazione di ogni risultato intermedio in memoria. Nota che questa è già l'impostazione predefinita su GCC per x86-64. Dal wiki di GCC :

Sui processori x86 più moderni che supportano SSE2, la specifica delle opzioni del compilatore -mfpmath=sse -msse2garantisce che tutte le operazioni float e double vengano eseguite nei registri SSE e arrotondate correttamente. Queste opzioni non influiscono sull'ABI e dovrebbero pertanto essere utilizzate, ove possibile, per risultati numerici prevedibili.

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

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

Disabilitando FMA, otteniamo risultati che corrispondono esattamente al debug e al rilascio, a scapito di alcune prestazioni (e accuratezza). Possiamo ancora sfruttare altri vantaggi in termini di prestazioni di SSE e AVX.


1

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 ha detto Maxim sopra, il problema è dovuto alla dimensione di 80 bit dei registri FPU.

Ma perché il problema non si verifica mai su Windows? su IA-32, la FPU x87 era 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 possibile, o meno, su queste diverse piattaforme cambiando la parola di controllo della FPU (supponendo che la sequenza di istruzioni innescherebbe 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 in 32 bit con VC ++:

#include "stdafx.h"
#include <stdio.h>  
#include <float.h>  

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 <stdio.h>

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]);
}

Nota che con gcc puoi impostare la precisione dell'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 comportamento diverso.

Sull'architettura x86_64, SSE viene utilizzato come detto da tmandry , quindi il problema non si verificherà a meno che non si forzi la vecchia FPU x87 per il calcolo FP con -mfpmath=387, oa meno che non si compili in modalità a 32 bit con -m32(sarà necessario il pacchetto multilib). 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 presentato. Suppongo che la sequenza di istruzioni generate non sia la stessa.

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


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 array di dati in virgola mobile. Anche dopo lo smontaggio sono spesso deluso dalle scelte dei compilatori. Molto spesso è più semplice usare gli intrinseci del compilatore o semplicemente scrivere l'assembly da soli.

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.