Perché Clang ottimizza via x * 1.0 ma NON x + 0.0?

Perché Clang ottimizza via x * 1.0 ma NON x + 0.0?


Perché Clang ottimizza il loop in questo codice


#include <time.h>
#include <stdio.h>
static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };
int main()
{
clock_t const start = clock();
for (int i = 0; i < N; ++i) { arr[i] *= 1.0; }
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

ma non il ciclo in questo codice?


#include <time.h>
#include <stdio.h>
static size_t const N = 1 << 27;
static double arr[N] = { /* initialize to zero */ };
int main()
{
clock_t const start = clock();
for (int i = 0; i < N; ++i) { arr[i] += 0.0; }
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}

(Tagging sia come C che come C++ perché vorrei sapere se la risposta è diversa per ciascuno.)


Risposte:


Lo standard IEEE 754-2008 per l'aritmetica in virgola mobile e lo standard ISO/IEC 10967 Language Independent Arithmetic (LIA), parte 1, spiegano perché è così.



Il caso di aggiunta


Nella modalità di arrotondamento predefinita (Round-to-Nearest, Ties-to-Even) , vediamo che x+0.0 produce x , TRANNE quando x è -0.0 :In tal caso abbiamo una somma di due operandi di segno opposto la cui somma è zero, e §6.3 paragrafo 3 regola questa aggiunta produce +0.0 .


Dal +0.0 non è bit per bit identico all'originale -0.0 e quel -0.0 è un valore legittimo che può verificarsi come input, il compilatore è obbligato a inserire il codice che trasformerà potenziali zeri negativi in ​​+0.0 .


Il riepilogo:nella modalità di arrotondamento predefinita, in x+0.0 , se x



  • non lo è -0.0 , quindi x di per sé è un valore di output accettabile.

  • è -0.0 , quindi il valore di output deve essere +0.0 , che non è bit per bit identico a -0.0 .


Il caso della moltiplicazione


Nella modalità di arrotondamento predefinita , nessun problema del genere si verifica con x*1.0 . Se x :



  • è un numero (sub)normale, x*1.0 == x sempre.

  • è +/- infinity , il risultato è +/- infinity dello stesso segno.

  • è NaN , quindi secondo



    il che significa che l'esponente e la mantissa (sebbene non il segno) di NaN*1.0 sono consigliati essere invariato rispetto all'input NaN . Il segno non è specificato in conformità con §6.3p1 sopra, ma un'implementazione può specificarlo in modo che sia identico al NaN sorgente .


  • è +/- 0.0 , il risultato è un 0 con il suo bit di segno XORed con il bit di segno di 1.0 , in accordo con §6.3p2. Dal bit di segno di 1.0 è 0 , il valore di uscita è invariato rispetto all'input. Pertanto, x*1.0 == x anche quando x è uno zero (negativo).


Il caso della sottrazione


Nella modalità di arrotondamento predefinita , la sottrazione x-0.0 è anche un no-op, perché equivale a x + (-0.0) . Se x è



  • è NaN , quindi §6.3p1 e §6.2.3 si applicano più o meno allo stesso modo dell'addizione e della moltiplicazione.

  • è +/- infinity , il risultato è +/- infinity dello stesso segno.

  • è un numero (sub)normale, x-0.0 == x sempre.

  • è -0.0 , allora per §6.3p2 abbiamo "[...] il segno di una somma, o di una differenza x − y considerata come una somma x + (−y), differisce al massimo da uno degli addendi' segni; ". Questo ci obbliga ad assegnare -0.0 come risultato di (-0.0) + (-0.0) , perché -0.0 differisce nel segno da nessuno degli addendi, mentre +0.0 differisce nel segno da due delle integrazioni, in violazione di questa clausola.

  • è +0.0 , quindi questo si riduce al caso di addizione (+0.0) + (-0.0) considerato sopra in Il caso di aggiunta , che da §6.3p3 è regolato per dare +0.0 .


Poiché in tutti i casi il valore di input è legale come output, è lecito considerare x-0.0 un no-op e x == x-0.0 una tautologia.


Ottimizzazioni che cambiano valore


Lo standard IEEE 754-2008 ha la seguente citazione interessante:



Poiché tutti i NaN e tutti gli infiniti condividono lo stesso esponente e il risultato arrotondato correttamente di x+0.0 e x*1.0 per x finito ha esattamente la stessa grandezza di x , il loro esponente è lo stesso.


sNaN


I NaN di segnalazione sono valori di trap in virgola mobile; Sono valori NaN speciali il cui utilizzo come operando a virgola mobile determina un'eccezione di operazione non valida (SIGFPE). Se un ciclo che attiva un'eccezione fosse ottimizzato, il software non si comporterebbe più allo stesso modo.


Tuttavia, come user2357112 sottolinea nei commenti , lo Standard C11 lascia esplicitamente indefinito il comportamento delle NaN di segnalazione (sNaN ), quindi il compilatore può presumere che non si verifichino e quindi che non si verifichino anche le eccezioni che sollevano. Lo standard C++11 omette di descrivere un comportamento per la segnalazione di NaN e quindi lo lascia indefinito.


Modalità di arrotondamento


In modalità di arrotondamento alternative, le ottimizzazioni consentite possono cambiare. Ad esempio, in Round-to-Negative-Infinity modalità, l'ottimizzazione x+0.0 -> x diventa consentito, ma x-0.0 -> x diventa proibito.


Per impedire a GCC di assumere modalità e comportamenti di arrotondamento predefiniti, il flag sperimentale -frounding-math può essere passato a GCC.


Conclusione


Clang e GCC, anche a -O3 , rimane conforme a IEEE-754. Ciò significa che deve attenersi alle regole di cui sopra dello standard IEEE-754. x+0.0 è non bit-identico a x per tutti i x secondo quelle regole, ma x*1.0 può essere scelto per esserlo :Vale a dire, quando noi



  1. Rispetta la raccomandazione di passare invariato il payload di x quando è un NaN.

  2. Lascia il bit di segno di un risultato NaN invariato da * 1.0 .

  3. Obbedisci all'ordine di XOR il bit di segno durante un quoziente/prodotto, quando x è non a NaN.


Per abilitare l'ottimizzazione IEEE-754-non sicura (x+0.0) -> x , la bandiera -ffast-math deve essere passato a Clang o GCC.


Alcune risposte al codice


#include <time.h>
#include <stdio.h>
static size_t const N = 1 <<
27;
static double arr[N] = { /* initialize to zero */ };
int main() {
clock_t const start = clock();
for (int i = 0;
i <
N;
++i) { arr[i] *= 1.0;
}
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}
#include <time.h>
#include <stdio.h>
static size_t const N = 1 <<
27;
static double arr[N] = { /* initialize to zero */ };
int main() {
clock_t const start = clock();
for (int i = 0;
i <
N;
++i) { arr[i] += 0.0;
}
printf("%u ms\n", (unsigned)(clock() - start) * 1000 / CLOCKS_PER_SEC);
}