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

Lesezeit: 9 Minuten

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.)

  • 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 -O3ich 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

Benutzer-Avatar
Ich werde nicht existieren Ich werde nicht existieren

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 xAusser 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.0und 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.0wenn x

  • ist nicht -0.0dann x selbst ist ein akzeptabler Ausgabewert.
  • ist -0.0dann der Ausgangswert muss sein +0.0die 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 +/- infinitydann ist das Ergebnis +/- infinity des gleichen Vorzeichens.
  • ist NaNdann 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.0dann 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 NaNdann gelten §6.3p1 und §6.2.3 ähnlich wie für Addition und Multiplikation.
  • ist +/- infinitydann ist das Ergebnis +/- infinity des gleichen Vorzeichens.
  • ist eine (sub)normale Zahl, x-0.0 == x stets.
  • ist -0.0dann 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.0dann 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 xihr 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

  1. Befolgen Sie die Empfehlung, die Nutzlast unverändert weiterzugeben x wenn es ein NaN ist.
  2. Lassen Sie das Vorzeichenbit eines NaN-Ergebnisses unverändert durch * 1.0.
  3. 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) -> xdie Flagge -ffast-math muss an Clang oder GCC übergeben werden.

  • Vorbehalt: Was ist, wenn es sich um ein signalisierendes NaN handelt? (Ich dachte eigentlich, das könnte irgendwie der Grund gewesen sein, aber ich wusste nicht wirklich wie, also fragte ich.)

    – Benutzer541686

    22. Oktober 2015 um 4:39 Uhr


  • @Mehrdad: Anhang F, der (optionale) Teil des C-Standards, der die Einhaltung von IEEE 754 durch C spezifiziert, deckt die Signalisierung von NaNs ausdrücklich nicht ab. (C11 F.2.1., erste Zeile: „Diese Spezifikation definiert nicht das Verhalten von Signalisierungs-NaNs.“) Implementierungen, die die Konformität mit Anhang F erklären, bleiben frei, mit Signalisierungs-NaNs zu tun, was sie wollen. Der C++-Standard hat seine eigene Behandlung von IEEE 754, aber was auch immer es ist (ich bin nicht vertraut), ich bezweifle, dass es auch das Signalisierungsverhalten von NaN spezifiziert.

    – Benutzer2357112

    22. Oktober 2015 um 6:06 Uhr

  • @Mehrdad: sNaN ruft gemäß dem Standard undefiniertes Verhalten auf (aber es ist wahrscheinlich von der Plattform gut definiert), sodass das Squashing des Compilers hier zulässig ist.

    – Josua

    22. Oktober 2015 um 15:33 Uhr

  • Oh, schau, eine Frage, die berechtigterweise sowohl für C als auch für C++ gilt und die genau beantwortet wird beide Sprachen durch einen Verweis auf einen einzigen Standard. Wird sich dadurch die Wahrscheinlichkeit verringern, dass sich Leute über Fragen beschweren, die sowohl mit C als auch mit C++ gekennzeichnet sind, selbst wenn sich die Frage auf eine sprachliche Gemeinsamkeit bezieht? Leider glaube ich nicht.

    – Kyle Strand

    28. Oktober 2015 um 19:05 Uhr

  • Wenn Sie sagen, dass x-0. gleich x ist, wäre es sinnvoll, ganz klar zu spezifizieren, dass dies beim Runden nicht zutrifft, da 0.-0. ist -0. in diesem Fall (und tatsächlich weigert sich gcc zu vereinfachen, wenn Sie -frounding-math verwenden).

    – Marc Glisse

    14. Mai 2017 um 6:31 Uhr

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.

  • Ich habe das tatsächlich gepostet, nachdem ich es hatte nur lesen Sie warum x += 0.0 ist kein No-Op, aber ich dachte, das ist wahrscheinlich nicht der Grund, weil die gesamte Schleife so oder so optimiert werden sollte. Ich kann es kaufen, es ist nur nicht so ganz überzeugend, wie ich gehofft hatte …

    – Benutzer541686

    22. Oktober 2015 um 3:47 Uhr


  • Angesichts der Neigung objektorientierter Sprachen, Nebenwirkungen zu erzeugen, würde ich mir vorstellen, dass es schwierig wäre, sicher zu sein, dass der Optimierer das tatsächliche Verhalten nicht ändert.

    – Robert Harvey

    22. Oktober 2015 um 3:50 Uhr


  • Könnte der Grund sein, da mit long long Die Optimierung ist in Kraft (habe es mit gcc gemacht, das sich für dasselbe verhält doppelt wenigstens)

    – Déjà-vu

    22. Oktober 2015 um 3:57 Uhr

  • @ringö: long long ist ein integraler Typ, kein IEEE754-Typ.

    – MSalter

    22. Oktober 2015 um 15:41 Uhr

  • Wie wäre es mit x -= 0ist es das Gleiche?

    – Viktor Mellgren

    23. Oktober 2015 um 8:56 Uhr


1019530cookie-checkWarum optimiert Clang x * 1.0 weg, aber NICHT x + 0.0?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy