Questa ottimizzazione in virgola mobile è consentita?


90

Ho provato a controllare dove floatperde la capacità di rappresentare esattamente grandi numeri interi. Quindi ho scritto questo piccolo snippet:

int main() {
    for (int i=0; ; i++) {
        if ((float)i!=i) {
            return i;
        }
    }
}

Questo codice sembra funzionare con tutti i compilatori, tranne clang. Clang genera un semplice ciclo infinito. Godbolt .

È permesso? Se sì, è un problema di QoI?


@geza sarei interessato a sentire il numero risultante!
nada

5
gccesegue la stessa ottimizzazione dei cicli infiniti se si compila con -Ofast, quindi è un'ottimizzazione che gccritiene non sicura, ma può farlo.
12345ieee

3
g ++ genera anche un ciclo infinito, ma non ottimizza il lavoro al suo interno. Puoi vedere che fa ucomiss xmm0,xmm0per confrontare (float)ise stesso. Questo è stato il tuo primo indizio che la tua fonte C ++ non significa quello che pensavi che facesse. Stai affermando di avere questo ciclo da stampare / restituire 16777216? Con quale compilatore / versione / opzioni era quello? Perché sarebbe un bug del compilatore. gcc ottimizza correttamente il tuo codice jnpcome ramo del loop ( godbolt.org/z/XJYWeu ): continua a eseguire il loop finché gli operandi != non erano NaN.
Peter Cordes

4
In particolare, è l' -ffast-mathopzione implicitamente abilitata da -Ofastche consente a GCC di applicare ottimizzazioni in virgola mobile non sicure e quindi generare lo stesso codice di Clang. MSVC si comporta esattamente allo stesso modo: senza /fp:fast, genera un mucchio di codice che si traduce in un ciclo infinito; con /fp:fast, emette una singola jmpistruzione. Presumo che senza attivare esplicitamente ottimizzazioni FP non sicure, questi compilatori si bloccano sui requisiti IEEE 754 relativi ai valori NaN. Piuttosto interessante il fatto che Clang non lo faccia, in realtà. Il suo analizzatore statico è migliore. @ 12345ieee
Cody Grey

1
@geza: se il codice ha fatto quello che volevi, controllando quando il valore matematico di (float) idifferiva dal valore matematico di i, il risultato (il valore restituito returnnell'istruzione) sarebbe 16.777.217, non 16.777.216.
Eric Postpischil

Risposte:


49

Come ha sottolineato @Angew , l' !=operatore necessita dello stesso tipo su entrambi i lati. (float)i != isi traduce anche nella promozione della RHS a galleggiare, quindi abbiamo (float)i != (float)i.


g ++ genera anche un ciclo infinito, ma non ottimizza il lavoro al suo interno. Puoi vedere che converte int-> float con cvtsi2sse fa il ucomiss xmm0,xmm0confronto (float)icon se stesso. (Questo è stato il tuo primo indizio che la tua fonte C ++ non significa quello che pensavi che piacesse la risposta di @ Angew spiega.)

x != xè vero solo quando è "non ordinato" perché xera NaN. ( INFINITYconfronta uguale a se stesso in matematica IEEE, ma NaN no. NAN == NANè falso, NAN != NANè vero).

gcc7.4 e versioni precedenti ottimizzano correttamente il codice jnpcome ramo del ciclo ( https://godbolt.org/z/fyOhW1 ): continua a eseguire il ciclo finché gli operandi x != x non erano NaN. (gcc8 e versioni successive verificano anche jeun'interruzione del ciclo, non riuscendo a ottimizzare in base al fatto che sarà sempre vero per qualsiasi input non NaN). x86 FP confronta set PF su non ordinato.


E a proposito, ciò significa che anche l'ottimizzazione di clang è sicura : deve solo CSE (float)i != (implicit conversion to float)icome essere lo stesso, e dimostrare che i -> floatnon è mai NaN per la gamma possibile di int.

(Anche se dato che questo ciclo raggiungerà UB con overflow con segno, è consentito emettere letteralmente qualsiasi asm desideri, inclusa ud2un'istruzione illegale o un ciclo infinito vuoto indipendentemente da quale fosse effettivamente il corpo del ciclo.) Ma ignorando l'UB di overflow con segno. , questa ottimizzazione è ancora legale al 100%.


GCC non riesce a ottimizzare il corpo del ciclo anche -fwrapvper rendere ben definito l'overflow di interi con segno (come avvolgimento del complemento di 2). https://godbolt.org/z/t9A8t_

Anche l'abilitazione -fno-trapping-mathnon aiuta. (L'impostazione predefinita di GCC è sfortunatamente di abilitare
-ftrapping-mathanche se l'implementazione di GCC è difettosa / buggy .) La conversione int-> float può causare un'eccezione inesatta FP (per numeri troppo grandi per essere rappresentati esattamente), quindi con eccezioni eventualmente smascherate è ragionevole non farlo ottimizzare il corpo del ciclo. (Perché la conversione 16777217in float potrebbe avere un effetto collaterale osservabile se l'eccezione inesatta viene smascherata.)

Ma con -O3 -fwrapv -fno-trapping-math, è un'ottimizzazione mancata al 100% per non compilare questo in un ciclo infinito vuoto. Senza #pragma STDC FENV_ACCESS ON, lo stato dei flag permanenti che registrano le eccezioni FP mascherate non è un effetto collaterale osservabile del codice. No int-> la floatconversione può risultare in NaN, quindi x != xnon può essere vero.


Questi compilatori sono tutti ottimizzati per le implementazioni C ++ che utilizzano IEEE 754 a precisione singola (binary32) floate 32 bit int.

Il ciclo corretto da bug(int)(float)i != i avrebbe UB su implementazioni C ++ con 16 bit stretti inte / o più larghi float, perché avresti raggiunto UB con overflow del numero intero firmato prima di raggiungere il primo numero intero che non era esattamente rappresentabile come file float.

Ma UB in un diverso insieme di scelte definite dall'implementazione non ha conseguenze negative quando si compila per un'implementazione come gcc o clang con l'ABI System V x86-64.


BTW, potresti calcolare staticamente il risultato di questo ciclo da FLT_RADIXe FLT_MANT_DIG, definito in <climits>. O almeno puoi in teoria, se in floatrealtà si adatta al modello di un float IEEE piuttosto che a qualche altro tipo di rappresentazione in numero reale come un Posit / unum.

Non sono sicuro di quanto lo standard ISO C ++ definisca il floatcomportamento e se un formato che non fosse basato su campi di esponente e significato a larghezza fissa sarebbe conforme agli standard.


Nei commenti:

@geza sarei interessato a sentire il numero risultante!

@nada: è 16777216

Stai affermando di avere questo ciclo da stampare / restituire 16777216?

Aggiornamento: poiché quel commento è stato cancellato, credo di no. Probabilmente l'OP sta solo citando la floatprima del primo numero intero che non può essere rappresentato esattamente come un 32 bit float. https://en.wikipedia.org/wiki/Single-precision_floating-point_format#Precision_limits_on_integer_values, ovvero cosa speravano di verificare con questo codice difettoso.

La versione con bugfix sarebbe ovviamente stampata 16777217, il primo intero che non è esattamente rappresentabile, piuttosto che il valore precedente.

(Tutti i valori in virgola mobile più alti sono numeri interi esatti, ma sono multipli di 2, poi 4, poi 8, ecc. Per i valori di esponente superiori alla larghezza del significato. Possono essere rappresentati molti valori interi più alti, ma 1 unità nell'ultimo posto (del significante) è maggiore di 1, quindi non sono numeri interi contigui. Il finito più grande floatè appena inferiore a 2 ^ 128, che è troppo grande per pari int64_t.)

Se un compilatore uscisse dal ciclo originale e lo stampasse, sarebbe un bug del compilatore.


3
@SombreroChicken: no, ho imparato prima l'elettronica (da alcuni libri di testo che mio padre aveva in giro; era un professore di fisica), poi la logica digitale e poi sono entrato in CPU / software. : P Quindi, praticamente, mi è sempre piaciuto capire le cose da zero, o se inizio con un livello più alto, mi piace imparare almeno qualcosa sul livello inferiore che influenza il modo / perché le cose funzionano nel livello in cui sono pensando a. (ad esempio, come funziona asm e come ottimizzarlo è influenzato dai vincoli di progettazione della CPU / roba sull'architettura della CPU. Che a sua volta deriva dalla fisica e dalla matematica.)
Peter Cordes

1
GCC potrebbe non essere in grado di ottimizzare anche con frapw, ma sono sicuro che GCC 10 è -ffinite-loopsstato progettato per situazioni come questa.
MCCCS

64

Si noti che l'operatore integrato !=richiede che i suoi operandi siano dello stesso tipo e lo raggiungerà utilizzando promozioni e conversioni, se necessario. In altre parole, la tua condizione è equivalente a:

(float)i != (float)i

Questo non dovrebbe mai fallire, e quindi il codice finirà per traboccare i, dando al tuo programma un comportamento indefinito. Qualsiasi comportamento è quindi possibile.

Per controllare correttamente ciò che vuoi controllare, dovresti restituire il risultato a int:

if ((int)(float)i != i)

8
@ Džuris È UB. Non v'è nessun risultato definitivo. Il compilatore potrebbe rendersi conto che può terminare solo in UB e decidere di rimuovere completamente il ciclo.
Finanzia la causa di Monica il

4
@opa intendi static_cast<int>(static_cast<float>(i))? reinterpret_castè ovvio UB lì
Caleth

6
@NicHartley: Stai dicendo che (int)(float)i != iè UB? Come lo concludi? Sì, dipende dalle proprietà definite dall'implementazione (perché floatnon è necessario che sia IEEE754 binary32), ma su qualsiasi implementazione data è ben definito a meno che non floatpossa rappresentare esattamente tutti i intvalori positivi , quindi otteniamo UB di overflow con numeri interi con segno. ( en.cppreference.com/w/cpp/types/climits definisce FLT_RADIXe FLT_MANT_DIGdetermina questo). In generale la stampa di cose definite dall'implementazione, come std::cout << sizeof(int)non è UB ...
Peter Cordes

2
@Caleth: reinterpret_cast<int>(float)non è esattamente UB, è solo un errore di sintassi / mal formato. Sarebbe bello se quella sintassi consentisse il gioco di parole di tipo float to intcome alternativa a memcpy(che è ben definito), ma reinterpret_cast<>penso che funzioni solo sui tipi di puntatore.
Peter Cordes

2
@ Peter Solo per NaN, x != xè vero. Guarda dal vivo su coliru . Anche in C.
Deduplicatore
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.