Warum optimiert Clang x * 1.0 weg, aber NICHT x + 0.0?

Warum optimiert Clang x * 1.0 weg, aber NICHT x + 0.0?


Warum optimiert Clang die Schleife in diesem Code weg


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

aber nicht die Schleife in diesem Code?


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

(Kennzeichnung sowohl als C als auch als C++, weil ich gerne wissen würde, ob die Antwort für beide unterschiedlich ist.)


Antworten:


Warum das so ist, beantworten der IEEE 754-2008 Standard for Floating-Point Arithmetic und der ISO/IEC 10967 Language Independent Arithmetic (LIA) Standard, Part 1.



Der Fall der Addition


Im standardmäßigen Rundungsmodus (Runden zum nächsten, Unentschieden zum Geraden) , sehen wir diesen x+0.0 erzeugt x , AUSSER wenn x ist -0.0 :In diesem Fall haben wir eine Summe von zwei Operanden mit entgegengesetzten Vorzeichen, deren Summe Null ist, und §6.3 Absatz 3 regelt, dass diese Addition +0.0 erzeugt .


Seit +0.0 ist nicht bitweise identisch mit dem Original -0.0 , und das -0.0 ein legitimer Wert ist, der als Eingabe auftreten kann, muss der Compiler den Code einfügen, der potenzielle negative Nullen in +0.0 umwandelt .


Die Zusammenfassung:Im Standard-Rundungsmodus in x+0.0 , wenn x



  • nicht -0.0 , dann x selbst ist ein akzeptabler Ausgabewert.

  • ist -0.0 , dann muss der Ausgabewert sein +0.0 , die nicht bitweise identisch mit -0.0 ist .


Der Fall der Multiplikation


Im standardmäßigen Rundungsmodus , tritt dieses Problem bei x*1.0 nicht auf . Wenn x :



  • ist eine (sub)normale Nummer, x*1.0 == x immer.

  • ist +/- infinity , dann ist das Ergebnis +/- infinity mit demselben Vorzeichen.

  • ist NaN , dann nach



    was bedeutet, dass der Exponent und die Mantisse (jedoch nicht das Vorzeichen) von NaN*1.0 werden empfohlen gegenüber der Eingabe NaN unverändert bleiben . Das Vorzeichen ist gemäß §6.3p1 oben nicht spezifiziert, aber eine Implementierung kann es so spezifizieren, dass es mit der Quelle NaN identisch ist .


  • ist +/- 0.0 , dann ist das Ergebnis ein 0 mit seinem Vorzeichenbit XORed mit dem Vorzeichenbit von 1.0 , in Übereinstimmung mit §6.3p2. Da das Vorzeichenbit von 1.0 ist 0 , der Ausgangswert ist gegenüber dem Eingang unverändert. Also x*1.0 == x auch wenn x ist eine (negative) Null.


Der Fall der Subtraktion


Im standardmäßigen Rundungsmodus , die Subtraktion x-0.0 ist auch ein No-Op, da es x + (-0.0) entspricht . Wenn x ist



  • ist NaN , dann gelten §6.3p1 und §6.2.3 ähnlich wie für Addition und Multiplikation.

  • ist +/- infinity , dann ist das Ergebnis +/- infinity mit demselben Vorzeichen.

  • ist eine (sub)normale Nummer, x-0.0 == x immer.

  • ist -0.0 , dann haben wir nach §6.3p2 "[...] das Vorzeichen einer Summe oder einer als Summe x + (−y betrachteten) Differenz x − y, unterscheidet sich von höchstens einem der Summanden' Zeichen; ". Dies zwingt uns, -0.0 zuzuweisen als Ergebnis von (-0.0) + (-0.0) , weil -0.0 unterscheidet sich im Vorzeichen von kein der Summanden, während +0.0 unterscheidet sich im Vorzeichen von zwei der Nachträge, die gegen diese Klausel verstoßen.

  • ist +0.0 , dann reduziert sich dies auf den Additionsfall (+0.0) + (-0.0) oben in Der Fall der Addition betrachtet , der nach §6.3p3 +0.0 ergibt .


Da in allen Fällen der Eingabewert als Ausgabe zulässig ist, darf x-0.0 berücksichtigt werden ein No-Op und x == x-0.0 eine Tautologie.


Wertverändernde Optimierungen


Der IEEE 754-2008 Standard hat das folgende interessante Zitat:



Da alle NaNs und alle Unendlichkeiten denselben Exponenten teilen, und das korrekt gerundete Ergebnis von x+0.0 und x*1.0 für endlich x hat genau die gleiche Größenordnung wie x , ihr Exponent ist derselbe.


sNaNs


Signalisierungs-NaNs sind Gleitkomma-Trap-Werte; Sie sind spezielle NaN-Werte, deren Verwendung als Gleitkommaoperand zu einer ungültigen Operationsausnahme (SIGFPE) führt. Wenn eine Schleife, die eine Ausnahme auslöst, herausoptimiert würde, würde sich die Software nicht mehr so ​​verhalten.


Wie jedoch user2357112 in den Kommentaren darauf hinweist , lässt der C11-Standard das Verhalten von signalisierenden NaNs explizit undefiniert (sNaN ), sodass der Compiler davon ausgehen darf, dass sie nicht auftreten, und dass die von ihnen ausgelösten Ausnahmen ebenfalls nicht auftreten. Der C++11-Standard lässt die Beschreibung eines Verhaltens zur Signalisierung von NaNs aus und lässt es daher auch undefiniert.


Rundungsmodi


Bei alternativen Rundungsmodi können sich die zulässigen Optimierungen ändern. Zum Beispiel unter Round-to-Negative-Infinity Modus, die Optimierung x+0.0 -> x zulässig, aber x-0.0 -> x wird verboten.


Um zu verhindern, dass GCC standardmäßige Rundungsmodi und -verhalten annimmt, wird das experimentelle Flag -frounding-math kann an GCC weitergegeben werden.


Schlussfolgerung


Clang und GCC, sogar bei -O3 , bleibt IEEE-754-kompatibel. Das heißt, es muss sich an die oben genannten Regeln des IEEE-754-Standards halten. x+0.0 ist nicht bitidentisch bis x für alle x unter diesen Regeln, aber x*1.0 kann so gewählt werden :Nämlich, wenn wir



  1. Beachten Sie die Empfehlung, die Payload von x unverändert weiterzugeben wenn es ein NaN ist.

  2. Lassen Sie das Vorzeichenbit eines NaN-Ergebnisses unverändert durch * 1.0 .

  3. Befolgen Sie den Befehl zum XOR des Vorzeichenbits während eines Quotienten/Produkts, wenn x ist nicht ein NaN.


Zum Aktivieren der IEEE-754-unsicheren Optimierung (x+0.0) -> x , das Flag -ffast-math muss an Clang oder GCC übergeben werden.


Einige Code-Antworten


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