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


125

Perché Clang ottimizza 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] *= 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);
}

(Contrassegnando come C e C ++ perché vorrei sapere se la risposta è diversa per ciascuno.)


2
Quali flag di ottimizzazione sono attualmente attivi?
Iwillnotexist Idonotexist,

1
@IwillnotexistIdonotexist: ho appena usato -O3, non so come controllare ciò che attiva però.
user541686,

2
Sarebbe interessante vedere cosa succede se aggiungi -ffast-math alla riga di comando.
lavaggio:

static double arr[N]non è consentito in C; constle variabili non contano come espressioni costanti in quella lingua
MM

1
[Inserisci un commento accattivante su come C non sia C ++, anche se l'hai già chiamato.]
user253751

Risposte:


164

Lo standard IEEE 754-2008 per l'aritmetica in virgola mobile e lo standard ISO / IEC 10967 per l'aritmetica indipendente dal linguaggio (LIA), parte 1, rispondono al perché.

IEEE 754 § 6.3 Il bit di segno

Quando un input o un risultato è NaN, questo standard non interpreta il segno di un NaN. Si noti, tuttavia, che le operazioni sulle stringhe di bit - copia, negazione, abs, copiaSegna - specificano il bit di segno di un risultato NaN, a volte basato sul bit di segno di un operando NaN. Il predicato logico totalOrder è anche influenzato dal bit di segno di un operando NaN. Per tutte le altre operazioni, questo standard non specifica il bit di segno di un risultato NaN, anche quando esiste un solo NaN di input o quando il NaN viene prodotto da un'operazione non valida.

Quando né gli input né i risultati sono NaN, il segno di un prodotto o quoziente è l'OR esclusivo dei segni degli operandi; il segno di una somma, o di una differenza x - y considerata come una somma x + (−y), differisce al massimo da uno dei segni degli addend; e il segno del risultato di conversioni, l'operazione di quantizzazione, le operazioni roundTo-Integral e roundToIntegralExact (vedi 5.3.1) è il segno del primo o unico operando. Queste regole si applicano anche quando operandi o risultati sono zero o infiniti.

Quando la somma di due operandi con segni opposti (o la differenza di due operandi con segni simili) è esattamente zero, il segno di tale somma (o differenza) deve essere +0 in tutti gli attributi di direzione di arrotondamento ad eccezione di roundTowardNegative; in base a tale attributo, il segno di una somma (o differenza) zero esatta deve essere −0. Tuttavia, x + x = x - (−x) mantiene lo stesso segno di x anche quando x è zero.

Il caso di aggiunta

Sotto la modalità di arrotondamento predefinita (Round-to-più vicino, Ties-to-Even) , vediamo che x+0.0produce x, TRANNE quando xè -0.0: In quel caso abbiamo una somma di due operandi con segni opposti la cui somma è zero e §6.3 paragrafo 3 regole prodotte da questa aggiunta +0.0.

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

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

  • non lo è -0.0 , quindi esso xstesso è un valore di output accettabile.
  • è -0.0 , quindi il valore di output deve essere +0.0 , che non è identico a bit bit a -0.0.

Il caso della moltiplicazione

Nella modalità di arrotondamento predefinita , non si verifica questo problema x*1.0. Se x:

  • è un numero (sub) normale, x*1.0 == xsempre.
  • è +/- infinity, quindi il risultato è +/- infinitydello stesso segno.
  • è NaN, quindi secondo

    IEEE 754 § 6.2.3 Propagazione NaN

    Un'operazione che propaga un operando NaN al suo risultato e ha un singolo NaN come input dovrebbe produrre un NaN con il payload dell'input NaN se rappresentabile nel formato di destinazione.

    il che significa che NaN*1.0si raccomanda che l'esponente e la mantissa (sebbene non il segno) di siano invariati dall'input NaN. Il segno non è specificato in conformità con §6.3p1 sopra, ma un'implementazione può specificare che sia identico alla fonte NaN.

  • è +/- 0.0, quindi il risultato è a 0con il suo bit di segno XORed con il bit di segno di 1.0, in accordo con §6.3p2. Poiché il bit di segno di 1.0è 0, il valore di output rimane invariato rispetto all'input. Pertanto, x*1.0 == xanche quando xè uno zero (negativo).

Il caso della sottrazione

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

  • è NaN, quindi §6.3p1 e §6.2.3 si applicano più o meno allo stesso modo dell'aggiunta e della moltiplicazione.
  • è +/- infinity, quindi il risultato è +/- infinitydello stesso segno.
  • è un numero (sub) normale, x-0.0 == xsempre.
  • è -0.0, quindi con §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 dei segni degli addend; ". Questo ci obbliga ad assegnare -0.0come risultato di (-0.0) + (-0.0), perché -0.0differisce nel segno da nessuno dei dipendenti, mentre +0.0differisce nel segno da due dei dipendenti, in violazione di questa clausola.
  • è +0.0, quindi questo si riduce al caso di aggiunta (+0.0) + (-0.0)considerato sopra in The Case of Addition , che in base al §6.3p3 è stato deciso di dare +0.0.

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

Ottimizzazioni che cambiano valore

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

IEEE 754 § 10.4 Significato letterale e ottimizzazioni che cambiano valore

[...]

Le seguenti trasformazioni che cambiano valore, tra le altre, preservano il significato letterale del codice sorgente:

  • Applicando la proprietà dell'identità 0 + x quando x non è zero e non è un NaN di segnalazione e il risultato ha lo stesso esponente di x.
  • Applicando la proprietà identità 1 × x quando x non è un NaN di segnalazione e il risultato ha lo stesso esponente di x.
  • Modifica del payload o firma un bit di un NaN silenzioso.
  • [...]

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

SNAN segnalano

I NaN di segnalazione sono valori di trap a virgola mobile; Sono valori NaN speciali il cui uso come operando a virgola mobile comporta un'eccezione di operazione non valida (SIGFPE). Se un loop che innesca un'eccezione fosse ottimizzato, il software non si comporterebbe più nello stesso modo.

Tuttavia, come sottolinea user2357112 nei commenti , lo standard C11 lascia esplicitamente indefinito il comportamento della segnalazione di NaN ( sNaN), quindi al compilatore è consentito assumere che non si verifichino, e quindi anche le eccezioni che sollevano. Lo standard C ++ 11 omette di descrivere un comportamento per la segnalazione di NaN e quindi non lo definisce.

Modalità di arrotondamento

In modalità di arrotondamento alternativo, le ottimizzazioni consentite possono cambiare. Ad esempio, nella modalità Infinito da rotondo a negativo , l'ottimizzazione x+0.0 -> xdiventa consentita, ma x-0.0 -> xdiventa proibita.

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

Conclusione

Clang e GCC , anche a -O3, rimangono conformi a IEEE-754. Ciò significa che deve attenersi alle regole precedenti dello standard IEEE-754. nonx+0.0 è un po 'identico a xtutti per xquelle regole, ma x*1.0 può essere scelto per essere così : vale a dire, quando noi

  1. Rispettare la raccomandazione di passare invariato il carico utile di xquando si tratta di un NaN.
  2. Lascia invariato il bit di segno di un risultato NaN * 1.0 .
  3. Rispettare l'ordine di XOR il bit di segno durante un quoziente / prodotto, quando nonx è un NaN.

Per abilitare l'ottimizzazione IEEE-754 non sicura (x+0.0) -> x, il flag -ffast-mathdeve essere passato a Clang o GCC.


2
Avvertenza: cosa succede se si tratta di un NaN di segnalazione? (In realtà ho pensato che potesse essere stato il motivo in qualche modo, ma non sapevo davvero come, quindi ho chiesto.)
user541686

6
@Mehrdad: l'Allegato F, la parte (facoltativa) della norma C che specifica l'adesione C all'IEEE 754, non copre esplicitamente le NaN di segnalazione. (C11 F.2.1., Prima riga: "Questa specifica non definisce il comportamento della segnalazione di NaN.") Le implementazioni che dichiarano la conformità all'allegato F rimangono libere di fare ciò che vogliono con la segnalazione di NaN. Lo standard C ++ ha una propria gestione di IEEE 754, ma qualunque cosa sia (non ho familiarità), dubito che specifichi anche il comportamento di NaN.
user2357112 supporta Monica il

2
@Mehrdad: sNaN invoca un comportamento indefinito secondo lo standard (ma è probabilmente ben definito dalla piattaforma), quindi è permesso lo squash del compilatore qui.
Giosuè,

1
@ user2357112: La possibilità di intercettare errori come effetto collaterale per calcoli altrimenti inutilizzati generalmente interferisce con molta ottimizzazione; se il risultato di un calcolo viene talvolta ignorato, un compilatore potrebbe utilmente rinviare il calcolo fino a quando non può sapere se il risultato verrà utilizzato, ma se il calcolo avrebbe prodotto un segnale importante, ciò può essere negativo.
supercat,

2
Oh guarda, una domanda che si applica legittimamente sia al C sia al C ++ a cui viene data una risposta precisa per entrambe le lingue con un riferimento a un singolo standard. Ciò renderà le persone meno propense a lamentarsi delle domande taggate sia in C che in C ++, anche quando la domanda riguarda una comunanza linguistica? Purtroppo, penso di no.
Kyle Strand,

35

x += 0.0non è un NOOP se lo xè -0.0. L'ottimizzatore potrebbe comunque eliminare l'intero ciclo poiché i risultati non vengono utilizzati. In generale, è difficile dire perché un ottimizzatore prende le decisioni che prende.


2
In realtà l'ho pubblicato dopo aver appena letto perché x += 0.0non è una no-op, ma ho pensato che probabilmente non è il motivo perché l'intero ciclo dovrebbe essere ottimizzato in entrambi i modi. Posso comprarlo, non è del tutto convincente come speravo ...
user541686

Data la propensione per i linguaggi orientati agli oggetti a produrre effetti collaterali, immagino che sarebbe difficile essere sicuri che l'ottimizzatore non stia cambiando il comportamento reale.
Robert Harvey,

Potrebbe essere la ragione, dal momento che con long longl'ottimizzazione è in vigore (lo ha fatto con gcc, che si comporta allo stesso modo per il doppio almeno)
e2-e4

2
@ ringø: long longè un tipo integrale, non un tipo IEEE754.
Salterio,

1
Che dire x -= 0, è lo stesso?
Viktor Mellgren,
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.