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.)
Der IEEE 754-2008-Standard für Gleitkommaarithmetik und die ISO/IEC 10967 Standard für sprachunabhängige Arithmetik (LIA), Teil 1 beantworten, warum das so ist.
IEEE 754 § 6.3 Das Vorzeichenbit
Wenn entweder eine Eingabe oder ein Ergebnis NaN ist, interpretiert dieser Standard das Vorzeichen eines NaN nicht. Beachten Sie jedoch, dass Operationen an Bitfolgen – kopieren, negieren, abs, kopierenSignieren – das Vorzeichenbit eines NaN-Ergebnisses spezifizieren, manchmal basierend auf dem Vorzeichenbit eines NaN-Operanden. Das logische Prädikat totalOrder wird auch durch das Vorzeichenbit eines NaN-Operanden beeinflusst. Für alle anderen Operationen spezifiziert dieser Standard nicht das Vorzeichenbit eines NaN-Ergebnisses, selbst wenn es nur eine Eingangs-NaN gibt oder wenn die NaN aus einer ungültigen Operation erzeugt wird.
Wenn weder die Eingaben noch das Ergebnis NaN sind, ist das Vorzeichen eines Produkts oder Quotienten das exklusive ODER der Vorzeichen der Operanden; das Vorzeichen einer Summe oder einer als Summe x + (−y) betrachteten Differenz x − y unterscheidet sich von höchstens einem der Vorzeichen der Summanden; und das Vorzeichen des Ergebnisses von Konvertierungen, der Quantisierungsoperation, den roundTo-Integral-Operationen und dem roundToIntegralExact (siehe 5.3.1) ist das Vorzeichen des ersten oder einzigen Operanden. Diese Regeln gelten auch dann, wenn Operanden oder Ergebnisse null oder unendlich sind.
Wenn die Summe von zwei Operanden mit entgegengesetzten Vorzeichen (oder die Differenz von zwei Operanden mit gleichen Vorzeichen) genau Null ist, muss das Vorzeichen dieser Summe (oder Differenz) in allen Rundungsrichtungsattributen außer roundTowardNegative +0 sein; Unter diesem Attribut muss das Vorzeichen einer exakten Nullsumme (oder -differenz) −0 sein. x + x = x − (−x) behält jedoch dasselbe Vorzeichen wie x, selbst wenn x null ist.
Der Additionsfall
Unter dem Standardrundungsmodus (Runden-auf-Nächste, Unentschieden-auf-Gerade)wir sehen das x+0.0
produziert 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 Regeln, die diese Addition erzeugt +0.0
.
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 umwandelt +0.0
.
Die Zusammenfassung: Unter dem Standard-Rundungsmodus, in x+0.0
wenn x
- ist nicht
-0.0
dann x
selbst ist ein akzeptabler Ausgabewert.
- ist
-0.0
dann der Ausgangswert muss sein +0.0
die nicht bitweise identisch mit ist -0.0
.
Der Fall der Multiplikation
Unter dem Standardrundungsmodustritt dieses Problem nicht auf x*1.0
. Wenn x
:
- ist eine (sub)normale Zahl,
x*1.0 == x
stets.
- ist
+/- infinity
dann ist das Ergebnis +/- infinity
des gleichen Vorzeichens.
-
ist NaN
dann gem
IEEE 754 § 6.2.3 NaN-Ausbreitung
Eine Operation, die einen NaN-Operanden an ihr Ergebnis weitergibt und eine einzelne NaN als Eingabe hat, sollte eine NaN mit den Nutzdaten der Eingabe-NaN erzeugen, wenn dies im Zielformat darstellbar ist.
was bedeutet, dass der Exponent und die Mantisse (aber nicht das Vorzeichen) von NaN*1.0
sind empfohlen gegenüber der Eingabe unverändert bleiben NaN
. Das Zeichen ist in Übereinstimmung mit §6.3p1 oben nicht spezifiziert, aber eine Implementierung kann es so spezifizieren, dass es mit der Quelle identisch ist NaN
.
- ist
+/- 0.0
dann ist das Ergebnis a 0
mit seinem Vorzeichenbit XORed mit dem Vorzeichenbit von 1.0
, in Übereinstimmung mit §6.3p2. Da das Zeichen etwas von 1.0
ist 0
, der Ausgangswert ist gegenüber dem Eingang unverändert. Daher, x*1.0 == x
sogar wenn x
ist eine (negative) Null.
Der Fall der Subtraktion
Unter dem Standardrundungsmodusdie Subtraktion x-0.0
ist auch ein No-Op, weil es äquivalent ist x + (-0.0)
. 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
des gleichen Vorzeichens.
- ist eine (sub)normale Zahl,
x-0.0 == x
stets.
- 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 Vorzeichen der Summanden;“. Dies zwingt uns zur Zuordnung -0.0
Als Ergebnis von (-0.0) + (-0.0)
Weil -0.0
weicht im Vorzeichen ab keiner der Summanden, während +0.0
weicht im Vorzeichen ab zwei der Nachträge unter Verstoß gegen diese Klausel.
- ist
+0.0
dann reduziert sich dies auf den Additionsfall (+0.0) + (-0.0)
oben betrachtet in Der Additionsfalldie nach §6.3p3 zu geben ist +0.0
.
Da für alle Fälle der Eingabewert als Ausgabe zulässig ist, ist eine Betrachtung zulässig x-0.0
ein no-op, und x == x-0.0
eine Tautologie.
Wertverändernde Optimierungen
Der IEEE 754-2008 Standard hat das folgende interessante Zitat:
IEEE 754 § 10.4 Wörtliche Bedeutung und wertverändernde Optimierungen
[…]
Unter anderem die folgenden wertverändernden Transformationen bewahren die wörtliche Bedeutung des Quellcodes:
- Anwenden der Identitätseigenschaft 0 + x, wenn x nicht Null und kein signalisierendes NaN ist und das Ergebnis denselben Exponenten wie x hat.
- Anwenden der Identitätseigenschaft 1 × x, wenn x kein signalisierendes NaN ist und das Ergebnis denselben Exponenten wie x hat.
- Ändern der Nutzlast oder des Vorzeichenbits eines stillen NaN.
- […]
Da alle NaNs und alle Unendlichkeiten den gleichen Exponenten teilen, ergibt sich 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.
Allerdings als user2357112 weist in den Kommentaren darauf hinlä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 Auf negative Unendlichkeit runden Modus, die Optimierung x+0.0 -> x
zulässig ist, aber x-0.0 -> x
wird verboten.
Um zu verhindern, dass GCC standardmäßige Rundungsmodi und Verhaltensweisen annimmt, wird das experimentelle Flag -frounding-math
können an GCC weitergegeben werden.
Fazit
Kling und GCCsogar 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 zu x
für alle x
unter diesen Regeln, aber x*1.0
kann so gewählt werden: Nämlich, wenn wir
- Befolgen Sie die Empfehlung, die Nutzlast unverändert weiterzugeben
x
wenn es ein NaN ist.
- Lassen Sie das Vorzeichenbit eines NaN-Ergebnisses unverändert durch
* 1.0
.
- Befolgen Sie den Befehl, das Vorzeichenbit während eines Quotienten/Produkts mit XOR zu verknüpfen, wenn
x
ist nicht ein NaN.
Um die IEEE-754-unsichere Optimierung zu aktivieren (x+0.0) -> x
die Flagge -ffast-math
muss an Clang oder GCC übergeben werden.
x += 0.0
ist kein NOOP if x
ist -0.0
. Der Optimierer könnte trotzdem die gesamte Schleife entfernen, da die Ergebnisse nicht verwendet werden. Im Allgemeinen ist es schwer zu sagen, warum ein Optimierer die Entscheidungen trifft, die er trifft.
Welche Optimierungs-Flags sind derzeit aktiv?
– Ich werde nicht existieren Ich werde nicht existieren
22. Oktober 2015 um 3:40 Uhr
@IwillnotexistIdonotexist: Ich habe gerade verwendet
-O3
ich weiß jedoch nicht, wie ich überprüfen soll, was das aktiviert.– Benutzer541686
22. Oktober 2015 um 3:42 Uhr
Es wäre interessant zu sehen, was passiert, wenn Sie -ffast-math zur Befehlszeile hinzufügen.
– Plugwash
22. Oktober 2015 um 14:47 Uhr
static double arr[N]
ist in C nicht erlaubt;const
Variablen zählen in dieser Sprache nicht als konstante Ausdrücke– MM
23. Oktober 2015 um 7:08 Uhr
[Insert snarky comment about how C is not C++, even though you already called it out.]
– Benutzer253751
23. Oktober 2015 um 10:40 Uhr