Ho un bug di ottimizzazione di gcc o un problema con il codice C?
Domanda
Prova il seguente codice:
#include <stdio.h>
#include <stdlib.h>
main()
{
const char *yytext="0";
const float f=(float)atof(yytext);
size_t t = *((size_t*)&f);
printf("t should be 0 but is %d\n", t);
}
Compilalo con:
gcc -O3 test.c
L'output BUONO dovrebbe essere:
"t should be 0 but is 0"
Ma con il mio gcc 4.1.3, ho:
"t should be 0 but is -1209357172"
Soluzione
Utilizza il flag del compilatore -fno-strict-aliasing.
Con l'aliasing rigoroso abilitato, come per impostazione predefinita almeno per -O3, nella riga:
size_t t = *((size_t*)&f);
il compilatore presuppone che size_t* NON punti alla stessa area di memoria di float*.Per quanto ne so, questo è un comportamento conforme agli standard (l'aderenza alle rigide regole di aliasing nello standard ANSI inizia intorno a gcc-4, come ha sottolineato Thomas Kammeyer).
Se ricordo bene, puoi utilizzare un cast intermedio su char* per aggirare questo problema.(il compilatore presuppone che char* possa essere un alias di qualsiasi cosa)
In altre parole, prova questo (non posso testarlo personalmente in questo momento ma penso che funzionerà):
size_t t = *((size_t*)(char*)&f);
Altri suggerimenti
Nello standard C99, questo è coperto dalla seguente regola in 6.5-7:
Un oggetto deve avere il suo valore memorizzato accessibile solo da un'espressione di LValue che ha uno dei seguenti tipi: 73)
un tipo compatibile con il tipo effettivo dell'oggetto,
una versione qualificata di un tipo compatibile con il tipo effettivo dell'oggetto,
un tipo che è il tipo firmato o non firmato corrispondente al tipo effettivo dell'oggetto,
un tipo che è il tipo firmato o non firmato corrispondente a una versione qualificata del tipo efficace dell'oggetto,
un tipo aggregato o sindacale che include uno dei tipi sopra menzionati tra i suoi membri (inclusi, ricorsivamente, un membro di un sottoaggregato o un sindacato contenuto) o
un tipo di carattere.
L'ultimo elemento è il motivo per cui il casting first to a (char*) funziona.
Ciò non è più consentito secondo le regole C99 sull'aliasing dei puntatori.Puntatori di due tipi diversi non possono puntare alla stessa posizione in memoria.Le eccezioni a questa regola sono i puntatori void e char.
Quindi nel tuo codice in cui stai trasmettendo a un puntatore di size_t, il compilatore può scegliere di ignorarlo.Se vuoi ottenere il valore float come size_t, assegnalo e il float verrà lanciato (troncato e non arrotondato) come tale:
taglia_t taglia = (taglia_t)(f);// funziona
Questo viene comunemente segnalato come un bug, ma in realtà è una funzionalità che consente agli ottimizzatori di lavorare in modo più efficiente.
In gcc puoi disabilitarlo con un'opzione del compilatore.Credo che -fno_strict_aliasing.
È un cattivo codice C :-)
La parte problematica è che si accede a un oggetto di tipo float trasformandolo in un puntatore intero e dereferenziandolo.
Ciò infrange la regola dell'aliasing.Il compilatore è libero di presupporre che i puntatori a tipi diversi come float o int non si sovrappongano in memoria.Hai fatto esattamente questo.
Ciò che vede il compilatore è che calcoli qualcosa, lo memorizzi nel float f e non vi accedi mai più.Molto probabilmente il compilatore ha rimosso parte del codice e l'assegnazione non è mai avvenuta.
Il dereferenziamento tramite il puntatore size_t in questo caso restituirà della spazzatura non inizializzata dallo stack.
Puoi fare due cose per risolvere questo problema:
usa un'unione con un float e un membro size_t ed esegui il casting tramite il gioco di parole.Non carino ma funziona.
usa memcopy per copiare il contenuto di f nel tuo size_t.Il compilatore è abbastanza intelligente da rilevare e ottimizzare questo caso.
Perché dovresti pensare che t dovrebbe essere 0?
O, più precisamente, "Perché dovresti pensare che la rappresentazione binaria di uno zero in virgola mobile sia la stessa della rappresentazione binaria di uno zero intero?"
Questo è un codice C errato.Il tuo cast infrange le regole di aliasing C e l'ottimizzatore è libero di fare cose che infrangono questo codice.Probabilmente scoprirai che GCC ha programmato la lettura size_t prima della scrittura in virgola mobile (per nascondere la latenza della pipeline fp).
È possibile impostare l'opzione -fno-strict-aliasing oppure utilizzare un'unione o un reinterpret_cast per reinterpretare il valore in modo conforme agli standard.
A parte gli allineamenti del puntatore, ti aspetti che sizeof(size_t)==sizeof(float).Non penso che lo sia (su Linux a 64 bit size_t dovrebbe essere 64 bit ma float 32 bit), il che significa che il tuo codice leggerà qualcosa non inizializzato.
-O3 non è considerato "sano", -O2 è generalmente la soglia superiore tranne forse per alcune app multimediali.
Alcune app non possono nemmeno arrivare così lontano e muoiono se vai oltre -O1 .
Se hai un GCC abbastanza nuovo (qui sono alla 4.3), potrebbe supportare questo comando
gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
Se stai attento, probabilmente sarai in grado di esaminare l'elenco e trovare la singola ottimizzazione che stai abilitando e che causa questo bug.
Da man gcc
:
The output is sensitive to the effects of previous command line options, so for example it is possible to find out which
optimizations are enabled at -O2 by using:
-O2 --help=optimizers
Alternatively you can discover which binary optimizations are enabled by -O3 by using:
gcc -c -Q -O3 --help=optimizers > /tmp/O3-opts
gcc -c -Q -O2 --help=optimizers > /tmp/O2-opts
diff /tmp/O2-opts /tmp/O3-opts | grep enabled
Ho testato il tuo codice con:"i686-apple-darwin9-gcc-4.0.1 (GCC) 4.0.1 (Apple Inc.costruire 5465)"
e non c'era nessun problema.Produzione:
t should be 0 but is 0
Quindi non c'è un bug nel tuo codice.Ciò non significa che sia un buon codice.Ma aggiungerei il returnType della funzione principale e il "return 0;" Alla fine della funzione.